阿嘉的Java日志 [第七章:集合]

150 阅读8分钟

集合继承结构图

20200830230517798.png

20200830230539537.png

Collection集合删除元素的注意事项

使用迭代器之后,再添加元素,会出异常

因为当集合结构发生改变时,迭代器必须重新获取,如果还是用之前的迭代器,会出现异常

在循环体删除元素之后,集合的结构发生了变化,应该重新去获取迭代器

但是下次循环并没有重新获取迭代器,所以会出现异常

出现异常的根本原因:集合中元素删除了,但是没有更新迭代器(迭代器不知道集合变化了,导致迭代器的快照和集合状态不同)

使用迭代器去删除,会自动更新迭代器,并且更新集合(删除集合中的元素)

迭代器删除的一定是迭代器指向的当前元素

重点:在迭代集合元素的过程中,不能调用集合对象的remove方法删除元素。

List的常用方法

void add(int index,Object element) 增
Object set(int index, Object element) 改动
Object get(int index) 获取
int indexOf(Object obj) 查找第一次出现的元素下标
int lastIndexOf(Object obj) 查找最后一次出现的元素下标
Object remove(int index)

ArrayList集合

  • 默认初始化容量10(底层先创建了一个长度为0的数组,当添加一个元素的时候,初始化容量10)

  • 集合的底层是一个Object数组

  • 构造方法:

    new ArrayList()

    new ArrayList(int initialCapacity) initialCapacity是指定数组容量

    new ArrayList(Collection<? extends E> c)

  • ArrayList集合的扩容

    增长到原容量的1.5倍

    ArrayList集合底层是数组,怎么优化?老办法!尽可能减少扩容,初始化一个合适的容量

  • 数组优点

    检索效率高(每个元素占用空间大小相同,内存地址又是连续的,知道首元素的地址,又知道下标,通过数学表达式计算出元素的内存地址所以检索效率最高)

  • 数组缺点

    随机增删元素效率较低

    无法储存大数据量(内存里很难找到一块非常巨大的连续的内存空间)

  • 面试官常问问题

    这么多集合中,你使用哪个集合最多?

    • ArrayList集合,因为一般往数组末尾添加元素,效率不受影响,另外,我们检索某个元素的效率高

ArrayList 数组可以添加 null 元素

LinkList集合

  • 链表的优点

    由于链表上的元素在空间存储内存地址不连续

    所以随机增删元素的时候不会有大量元素位移,因此随机增删效率较高

    在以后的开发中,如果遇到随机增删集合中元素的业务比较多时,建议采用LinkList

  • 链表的缺点

    不能通过数学表达式计算被查找元素的内存地址。每一次查找都是从头结点开始遍历,直到找到为止,所以LinkList集合检索查找的效率较低

ArrayList:把检索发挥到极致

LinkList:把随机增删发挥到极致

加元素一般都是在末尾加,所以ArrayList用的比LinkList多

可以添加 null

vector集合

所带的方法都是线程安全的

扩容是扩容2倍,初始容量是10(用无参构造器创建,默认容量是10)

似乎可以根据 " capacityIncrement " 这个变量(扩容增量)自定义扩容,因为底层扩容算法是:

newCapacity = oldCapacity + ( (capacityIncrement > 0) ? capacityIncrement : oldCapacity ) ;

但是一般做多线程开发,不用 Vector ,用 JUC 包中的集合,因为 Vector 中的方法例如:add,remove 等方法不是原子操作,Vector 是相对线程安全,而不是绝对线程安全

HashSet集合

Set 集合,无下标。不能用普通 for 循环依靠下标来遍历

无序,不可重复

无序的底层是依靠哈希算法来排序,无序是指添加和取出的顺序不一致。但每次的运行结果都一样,是固定的

TreeSet集合

Collections 集合工具类

//要想把别的集合(比如ArrayList)转成线程安全,可以用java工具
//java.util.Collections包的功能,具体用法:
List a = ArrayList();
a.add(100);
a.add(10);
a.add(200);
a.add(150);
//1、把List转换成线程安全
Collections.synchronizeList(a);
​
//2、把List集合排序
//值得注意的是,使用集合工具类排序
//集合元素是自定义类的话,要实现Comparable接口然后重写compareTo方法
Collections.sort(a);
//Set集合是无序的,那怎么排序呢?
  Set<String > set = new HashSet();
  set.add("9");
  set.add("2");
  set.add("3");
  set.add("1");
  set.add("5");
//将Set集合转换成List集合
  List<String> list = new ArrayList(set); //这个是ArrayList的第三个构造方法
  Collections.sort(list);
  for (String s : list) {
      System.out.println(s);
  }

泛型

JDK5.0之后推出的新特性

  • 泛型这种语法机制只在编译阶段起作用

  • 泛型的好处是什么?

第一:集合中存储的元素统一了

第二:从集合中取出的元素类型是泛型指定的类型,不需要进行大量的向下转型

  • 泛型的缺点是什么?

导致集合中存储的元素缺乏多样性

  • 大多数业务中,集合中的元素类型还是统一的。所以这种泛型特性被大家所认可
List<Animal> myList = new ArrayList<Animal>();
Cat c = new Cat();
Bird b = new Bird();
myList.add(c);//添加元素
myList.add(b);
​
Iterator<Animal> it = myList.iterator();
while(it.hasNext()){
    //如果这里不采用泛型,iterator会返回一个Object类型,Object在调用子类特有方法时要进行强转。要instanceof
    Animal a = it.next();
    a.move();
    //但这里Animal调用子类特有方法【cat.catchMouse()】时依然还是要向下转型(强转)
}

增强for(foreach)

这个是JDK5之后的新特性

//用法是可以直接输出数组,但就不能指定长度了,也没有下标
//for(数据类型 变量名:数组){     }
for(String s:arr){
    System.out.println(s);
}

增强 for 的底层其实就是一个迭代器。输入联想快捷键:I(大写的 i )

Map的常用方法

    返回值                 方法
-------------------------------------------------------
     V              put(K key, v value)     向Map中添加键值对
     V              get(Object key)     通过key获得value
    void            clear();    清空Map集合
   boolean          containsKey(Object key)     判断Map中是否包含某个Key
   boolean          containsValue(Object value)     判断Map中是否包含某个value
   boolean          isEmpty()       判断Map集合中元素是否为0
     V              remove(object key)  通过key删除键值对
    int             size()      获取Map集合键值对个数
 Collection<v>      values()    获取Map集合中所有的value,返回一个Collection
   Set<k>           keySet()        获取Map集合所有的key(所有的键是个set集合)        
//注意一下这个方法↓↓↓
Set<Map.Entry<K,V>>  entrySet()     将Map集合转换成Set集合
        
假设有集合 Map叫做 map
方法是这样子用:Set set = map.entrySet();        
也就是用一个Set集合去接收,类似创建构造器,通过调用方法去创建。然后这个这个Set集合里面就有Map集合里面的所有key和value元素了
Map.Entry<K,V>   【 .Entry其实是Map里面的一个内部类,支持泛型<K,V>罢了】

HashMap

  • HashMap的数组上的单链表,数量若达到8(TREEIFY_THRESHODE:树形临界点),而且数组已经扩容到64,则自动变成二叉树(红黑树),当红黑树上的元素减少达到6时,会重新变成单链表(UNTREEIFY_THRESHODE)。扩容是0.75 f ,比如16的0.75是12,要达到13才会扩容

JDK 8的新特性

  • 一般情况下,数组同一个下标的单链表的元素,哈希值都相同。

  • 哈希碰撞:(上面结论的例外情况)同一个数组下标下的链表,元素的哈希值可能不同。也就是哈希值相同,下标一定相同,但反过来就不行

  • 在存、取过程中,先去调用hashCode( ) ,再去调用equals( )

  • HashMap的扩容是满 0.75 f 自动用位运算扩容

    newCap = oldCap << 1 ; 二进制左移一位,也就是乘以 2

  • K值不能重复,但是V值可以重复

HashMap 和 Hashtable 的区别

  • HashMap的<K , V>值可以传入null ,可以map.get(null) 求value值

    • Hashtable<K , V>值不能传入null ,底层源码很明显,会抛出空指针异常,因为K在调用hashCode时,<K , V>不能为空 (null
  • 初始化容量为11, 0.75 f ,扩容也不同,是 : 新 = (老 << 1) + 1

不过HashMap 和 Hashtable 的底层都是哈希表数据结构,Hashtable的方法都带有synchronized,但对线程处理效率较低,所以使用少。

Properties

Properties的<K , V>值都是String

掌握setProperties() 和 getProperties()

其他方法都是有关 IO 流的,后面讲。

TreeSet 和 TreeMap

遍历集合

Collection的遍历

方法一:用迭代器while循环遍历

方法二:用迭代器普通for遍历(用collection的size( ) )后面加判断就行

方法三:增强for

================

Collection的子类:List 可以用for循环特有的get( )

for (int i = 0; i < list.size(); i++){
 System.out.println(list.get(i));
}

Collection的子类:Set,没有get( ) 方法,就只能用Collection的那三种,其他后面再学

Map的遍历

方法一:用keySet遍历

方法二:用entrySet遍历

若只需要Value值的话,可以用map.values( ) 去获取所有的value值,保存在Collection中,遍历Collection即可

//创建Map集合并添加元素
     Map<Integer,String> map = new HashMap<>();
     map.put(1,"xiaoming1");
     map.put(2,"xiaoming2");
     map.put(3,"xiaoming3");
     map.put(4,"xiaoming4");
​
     //遍历Map集合   Method 1
     Set<Integer> set = map.keySet();//Set里面存的都是Key
     for (Integer key : set){
         System.out.println(key +"="+map.get(key) );
     }
​
     //遍历Map集合   Method 2
     Set<Map.Entry<Integer,String>> set1 = map.entrySet();
     for (Map.Entry<Integer,String> node:set1){
         System.out.println(node.getKey()+"--->"+node.getValue());
     }

集合的互相转换

集合的比较。常回顾