Java 集合总结

7月18日更:

hashMap 是懒加载 只有put的时候 才创建数组 线程不安全主要体现在rehash,也就是扩容时候出现链表成环。链表插入的时候(jdk1.8是在尾部插入,1.8之前是头部插入)

hashMap的存入过程: Object--->hashCode(hashCode ^ hashCode>>>16 (无符号右移16位 相当于除以2的16次方))高16位和hash自身异或--->hash-->index (hash&(length-1))

    扩容时,变成原来的2倍,那么新索引要么和原来的索引一样,要么增加原数组长度,JDK1.8中,判断hash值新增的那一位(扩大2倍,相当于左移一位)是0还是1, 是0新索引不变,是1增加原数组长度  为什么容量是2的次幂?答:减少hash冲突,2的次幂-1 后面每一位都是1  比如8-1 (1000 ——>  0111) 这样h&(length-1)也就是h&0111可以使数组每个位置都能添加到元素,假设是h&0101 那么第二个位置就一定是0 所以对应的数组这个位置也一直为空。

    取余(%)操作中如果除数是2的幂次 则等同于与其除数减一的与(&)操作。位操作效率比算数运算效率高

    当数组中链表长度大于8,并且数组size大于64,链表变成红黑树,当树长度小于6,恢复成链表

Java集合是Java基础里面最重要的知识点之一,刚接触时觉得用的地方不多,而且很累赘,但随着写的代码量的增加,越来越觉得集合类是最重要的基础,做算法题的时候,合理利用集合可以更快更有效的AC,因为它提供了很多的方法,只需直接调用即可。做项目的时候,会频繁的用到集合,比如参数的传递,实体的转化等等,都需要map,list等。想深入理解最好还是去看看源码。这里综合网上的很多文章对常用集合做一个总结。

Java集合可以分为两大类,实现Collection接口的类和实现Map的类,

Collection接口提供的方法有:增(add,addAll),删(remove,removeAll),查(size,contains)       Map提供的方法有:增(put,putAll),删(remove),查(size,get,KeySet,ValueSet,EntrySet,contains)

(实际上Collection也继承了Iterable这个接口),实现Collection的又有三个接口,List,Set,Queue。所以集合类基本就分成四部分,Map,list,Set,Queue.

1.List接口:list表示一个列表,可以是数组,链表,栈,队列,元素可重复     常用方法:

  ArrayList:   底层维护动态数组,允许为值为null 查找复杂度是 O(1),通过下标可以直接查找,增加或删除时需要移动大量元素

       默认初始长度是10,每次通过Arrays.copyOf()方法扩充为原来的1.5倍,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量)

       Arrays。copyOf()最底层调用了本地方法System.arraycopy(),最终调用了C语言的memmove()函数,因此它可以保证同一个数组内元素的正确复制和移动,比一般的复制方法的实现效率要高很多

        每次增加元素时,都会调用ensureCapacity方法确保足够容量

       线程不安全,可以通过Collections.synchronized(new ArrayList())方法转换为线程安全的

                  ArrayList在迭代的时候如果同时对其进行修改就会抛出java.util.ConcurrentModificationException异常 原因是调用list.remove()方法导致modCount和expectedModCount的值不一致。expectedModCount:表示对ArrayList修改次数的期望值,它的初始值为modCount。modCount是AbstractList类中的一个成员变量 初始为0 所以迭代器移除某个元素的时候 用iterator.remove(),而不是list.remove 因为 iteartor.remove 有 expectedModCount = modCount;

      线程不安全 主要表现在:1 多个线程进行add操作时可能会导致elementData数组越界

           原因是 :add方法分两步 1 检查容量 需要是扩容 2 设置值 索引+1。 如果有两个线程同时检查了扩容 判断都不需要扩容(假设数组容量是10 此时长度是9) 一个线程set值之后 刚好达到最大容量 另一个线程再set就会越界。

       2. 一个线程的值覆盖另一个线程添加的值。因为 

  1. elementData[size] = e;  2 size = size + 1;   这两步不是原子操作。A,B线程同时add一个元素,导致的结果可能是size = 1 但是B线程覆盖了A线程添加的值。

     代码如下

public static void main(String[] args) throws InterruptedException{
       final List<Integer> list = new ArrayList<>();

        new Thread(() -> {
            for (int i = 0 ; i < 1000 ;i ++){
                list.add(i);
                try {
                    Thread.sleep(1);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(() -> {
            for (int i = 1000 ; i < 2000 ;i ++){
                list.add(i);
                try {
                    Thread.sleep(1);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        }).start();

        Thread.sleep(1000);

        for( int i = 0 ; i < list.size();i++){
            System.out.println("" + (i+1) + "个元素是 " + list.get(i));
        }


    }
//最后会出现 第 X 个元素 是 null的结果

 如果想要用线程安全的arrayList  , 可以使用CopyOnWriteArrayList。内部实现是 在进行add操作的时候 使用 Lock 加锁。

如果想要使用线程安全的linkedList 可以使用 ConcurrentLinkedQueue。

       可被序列化,随机访问,能被克隆

      参考博客:http://blog.csdn.net/ns_code/article/details/35568011

  LinkedList:  1.底层维护双向链表(1.6是双向循环链表,1.7取消了循环 ),允许值为null,在头尾增加删除操作时很方便,内部只维护size,first,last三个变量或指针,当栈 push,pop,peek;  当队列,offer,poll,peek(peek会自动根据添加元素时的方法判断栈还是队列,默认队列)

        2.可用作栈,队列,双端队列

        3.线程不安全,可被序列化,能被克隆

        4.内部单元是Node<E> 

           

 E item;
        Node<E> next;
        Node<E> prev;

 

         参考博客:http://blog.csdn.net/ns_code/article/details/35787253

      vector:   1 和ArrayList一样,线程安全,同Synchronized关键字提供了方法粒度级别的同步机制

        2. 不支持序列化,可以克隆

   Stack :栈  push ,pop ,peek empty 

2.set接口:  无序且不重复

   HashSet: 可存储null(占一个位置),线程不安全,适合添加,查询

        底层采用hashMap实现,hashSet存储元素实际为hashMap中的Key,map中key不可重复,所以决定了set中元素不可重复,value为一个静态Object对象

        可用foreach ,iterator 迭代

   TreeSet: 有序集,基于TreeMap,线程不安全

        默认排序是升序

        treeSet需要额外的红黑树算法来维护集合的次序,性能最次。

        方法:first,last,lower(E e):返回小于e的元素中最大元素,higher(E e)同理   subSet(E from, E to)

   LinkedHashSet:基于链表的哈希表,具有迭代顺序的Set集合,保证存储顺序和取出顺序一致,线程不安全

           迭代访问时效率高

           更适合于插入、删除以及遍历操作

   EnumSet:  为枚举类设计的集合类,所有元素都必须是指定枚举类型的枚举值,元素有序

        使用EnumSet.of方法新建

   EnumSet性能>HashSet性能>LinkedHashSet>TreeSet性能

3.queue接口:队列,FIFO Deque接口是Queue接口的子接口,它代表一个双端队列

   PriorityQueue:从小到大排列,本质上动态数组,维护最小堆,顶点是最小值,与ArrayList一致,默认初始容量是11

            非线程安全,不允许插入Null

   ArrayDeque:底层是循环数组,默认长度16

4.map接口:维护k,V键值对

    HashMap:  内部是数组,数组的每个元素是一条单链表的头结点,链表是用来解决冲突的,同的key映射到了数组的同一位置处,就将其放入单链表中,key是唯一的,重复插入同一个键,value会被覆盖,Java8是用红黑树代替链表处理冲突(链表长度大于8,否则仍是链表)

          初始容量是16,系统默认加载因子为0.75,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 resize 操作

          HashMap中key和value都允许为null,get方法:如果key为null,则直接从哈希表的第一个位置table[0]对应的链表上查找。

          记住,key为null的键值对永远都放在以table[0]为头结点的链表中,当然不一定是存放在头结点table[0]中。

          如果key不为null,则先求的key的hash值,根据hash值找到在table中的索引,在该索引对应的单链表中查找是否有键值对的key与目标key相等,有就返回对应的value,没有则返回null

          put方法:如果key为null,则将其添加到table[0]对应的链表中,key不为null,求出key的hash,遍历对应的单链表,如果单链表中存在与目标key相等的键值对,

          则将新的value覆盖旧的value,比将旧的value返回,如果找不到与目标key相等的键值对,或者该单链表为空,则将该键值对插入到改单链表的头结点位置

          每次put键值对的时候,总是将新的该键值对放在table[bucketIndex]处(即头结点处)。

          length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。

          h&(length-1)的方法来代替取模,同样实现了均匀的散列

          内部结构为Node<K,V>

          

 final int hash;
        final K key;
        V value;
        Node<K,V> next;

          参考博客:http://blog.csdn.net/ns_code/article/details/36034955

  hashtable: 线程安全,支持序列化,能被克隆

        HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂

          Hashtable中key和value都不允许为null,而HashMap中key和value都允许为null(key只能有一个为null,而value则可以有多个为null)

          Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。

        Hashtable计算hash值,直接用key的hashCode(),而HashMap重新计算了key的hash值,Hashtable在求hash值对应的位置索引时,用取模运算,而HashMap在求位置索引时,则用与运算

        参考博客:http://blog.csdn.net/ns_code/article/details/36191279

  TreeMap; 基于红黑树实现的

        TreeMap是根据key进行排序的,它的排序和定位需要依赖比较器或覆写Comparable接口,也因此不需要key覆写hashCode方法和equals方法,

        就可以排除掉重复的key,而HashMap的key则需要通过覆写hashCode方法和equals方法来确保没有重复的key。

        查询、插入、删除效率均没有HashMap高,一般只有要对key排序时才使用TreeMap

        TreeMap的key不能为null,而HashMap的key可以为null

        参考博客:http://blog.csdn.net/ns_code/article/details/36421085

  LinkedHashMap:  与HashMap有着同样的存储结构,但它加入了一个双向链表的头结点,将所有put到LinkedHashmap的节点一一串成了一个双向循环链表,

            因此它保留了节点插入的顺序,可以使节点的输出顺序与输入顺序相同。

          非线程安全的

          HashMap和LinkedList两个集合类的存储结构的结合。在LinkedHashMapMap中,

          所有put进来的Entry都保存在哈希表中,但它又额外定义了一个以head为头结点的空的双向循环链表,

          每次put进来Entry,除了将其保存到对哈希表中对应的位置上外,还要将其插入到双向循环链表的尾部。

          说白了就是用两个数据结构保存同一个对象

          参考博客:http://blog.csdn.net/ns_code/article/details/37867985

  

原文地址:https://www.cnblogs.com/team42/p/6960117.html