Java集合总结(含源码分析)

Java集合总结

image-20201226095900455

上图有些错误,Deque是继承Queue的,而不是Collection;且LinkedList没有继承Deque。

image-20201211144153730
image-20201226095932197

一、概述及常用集合API一览

  1. Iterable

    Iterable里面有Iterator迭代器接口,Iterator接口有如下两个方法:

    boolean hasNext();
    
    E next();
    

    ​ 在Java中,有很多的数据容器,对于这些的操作有很多的共性。Java采用了迭代器来为各种容器提供了公共的操作接口。这样使得对容器的遍历操作与其具体的底层实现相隔离,达到解耦的效果。

  2. Collection (注意与Collections区分,Collections是集合工具类,类似于Arrays)

    Collection接口继承了Iterable接口

    image-20201211143220287
    image-20201211143246450
  3. Collection旗下有List、Queue、Dequeue、Set接口

    • List(对付顺序的好帮手):存储元素有序可重复

    • Set(独一无二的性质):存储元素无序且不可重复

    • Deque(双端队列)继承于Queue


  1. List接口

    • ArrayList:Object[ ] 数组,线程不安全

      image-20201211145237436
      image-20201211145313100
    • Vector(基本上被弃用):Object[ ]数组,线程安全

    • LinkedList:双向链表(JDK1.6之前为循环链表,1.7取消了循环)线程不安全

      image-20201211150839321
      image-20201211150909453

      ​ 其中offer、poll、peekFirst/peekLast与add、remove、getLast/getFirst功能一样,只是前面三种更加符合队列这个特殊场景。

  2. Deque接口

    ​ 主要有ArrayDeque这个双端队列(用数组实现)线程不安全,注意区别用链表实现的双端队列LinkedList。API类似LinkedList

  3. Set接口

    • HashSet(无序-->由hash值决定,唯一)线程不安全:基于HashMap实现,底层采用HashMap来保存元素

      image-20201211153302731

      可以通过Iterator或者增强for来遍历HashSet,下图是Iterator方式遍历

      image-20201211153417233
    • LinkedHashSet (有序的HashSet)线程不安全 它自己没有独特的API,都是继承于HashSet。遍历方法同HashSet

    • TreeSet 线程不安全 底层使用红黑树,可以进行自然排序和定制排序,从而按照顺序来遍历


  1. Map接口 (用key来搜索)存储有映射关系的集合

    • HashMap 线程不安全 JDK1.8之前由数组+链表组成(拉链法解决冲突),JDK1.8后采用链表+数组、红黑树来实现。

      image-20201211162831878
      image-20201211162919819

      遍历HashMap的方式:

      image-20201211163756095
      image-20201211163829274
      image-20201211163844875
      image-20201211163859988
    • HashTable 线程安全 但基本上已被弃用,因为它是锁住了整个Map,并发性很差

    • LinkedHashMap 线程不安全 在HashMap上加了一个双向链表,使得能够维持插入的顺序。遍历方法:

      image-20201211165049164
    • TreeMap 线程不安全

      TreeMap继承结构

      ​ 实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索能力;实现 SortMap 使 TreeMap 有了对集合中的元素根据键排序的能力(搜索+排序)。

      ​ 相比于HashMap来说,TreeMap主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索能力。

    • ConcurrentHashMap 线程安全 并发场景下使用,只是锁了Map中的一部分,而HashTable锁了整个Map,因此并发性比较好

二、集合的底层原理

Ⅰ ArrayList

public class ArrayList extends AbstractList
        implements List, RandomAccess, Cloneable, java.io.Serializable
{
    ·····
}

​ 其中RandomAccess是一个随机访问标识,继承它说明支持随机访问(因为底层由数组实现,可以随机访问)。

  1. ArrayList创建时是空数组,真正对数组操作的时候数组容量才变为10(惰性创建)

  2. ArrayList扩容机制

    ①add方法

     /**
         * 将指定的元素追加到此列表的末尾。
         */
        public boolean add(E e) {
       //添加元素之前,先调用ensureCapacityInternal方法
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            //这里看到ArrayList添加元素的实质就相当于为数组赋值!!!
            elementData[size++] = e;
            return true;
        }
    

    ②ensureCapacityInternal()方法

    //add进第一个元素时,minCapacity为1,经过max比较后变为10
    //得到最小扩容量
        private void ensureCapacityInternal(int minCapacity) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                  // 获取默认的容量和传入参数的较大值
                minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
            }
    
            ensureExplicitCapacity(minCapacity);
        }
    

    ③ensureExplicitCapacity()方法

    如果调用ensureCapacityInternal()方法就一定会进入此方法

    //判断是否需要扩容
        private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
    
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                //调用grow方法进行扩容,调用此方法代表已经开始扩容了
                grow(minCapacity);
        }
    
    image-20201211195023311

    grow()方法 扩容的核心方法

     /**
         * 要分配的最大数组大小
         */
        private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    
        /**
         * ArrayList扩容的核心方法。
         */
        private void grow(int minCapacity) {
            // oldCapacity为旧容量,newCapacity为新容量
            int oldCapacity = elementData.length;
            //将oldCapacity 右移一位,其效果相当于oldCapacity /2,
            //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍!!!
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
           // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
           //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
    

    ​ 注:每次扩容后容量都会变为原来的1.5倍左右 。因此为了减少扩容操作,最好能在ArrayList初始化时给明长度。或在add大量元素之前调用ensureCapacity()方法来减少扩容次数。

     /**
        如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。
         *
         * @param   minCapacity   所需的最小容量
         */
        public void ensureCapacity(int minCapacity) {
            int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                // any size if not default element table
                ? 0
                // larger than default for default empty table. It's already
                // supposed to be at default size.
                : DEFAULT_CAPACITY;
    
            if (minCapacity > minExpand) {
                ensureExplicitCapacity(minCapacity);
            }
        }
    
  3. 注意区分System.arraycopy()和Arrays.copyOf()

      /**
         * 在此列表中的指定位置插入指定的元素。
         *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大;
         *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。
         */
        public void add(int index, E element) {
            rangeCheckForAdd(index);
    
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            //arraycopy()方法实现数组自己复制自己
            //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量;
            System.arraycopy(elementData, index, elementData, index + 1, size - index);
            elementData[index] = element;
            size++;
        }
    
    /**
         以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 返回的数组的运行时类型是指定数组的运行时类型。
         */
        public Object[] toArray() {
        //elementData:要复制的数组;size:要复制的长度
            return Arrays.copyOf(elementData, size);
        }
    

Ⅱ LinkedList

​ LinkedList底层是双向链表实现(JDK1.6之前为循环链表,JDK1.7取消了循环)。它实现了List接口和Deque接口,支持高效的插入和删除操作。

public class LinkedList
    extends AbstractSequentialList
    implements List, Deque, Cloneable, java.io.Serializable
{
    ···
}
双向链表
双向循环链表

LinkedList内部结构图:

LinkedList内部结构
  1. 源码分析

    ①add()方法:将元素添加到链表尾部

     public boolean add(E e) {
             linkLast(e);//这里就只调用了这一个方法
             return true;
         }
    /**
         * 链接使e作为最后一个元素。
         */
        void linkLast(E e) {
            final Node l = last; //last即为尾结点指针
            final Node newNode = new Node<>(l, e, null);
            last = newNode;//新建节点
            if (l == null)
                first = newNode;
            else
                l.next = newNode;//指向后继元素也就是指向下一个元素
            size++;
            modCount++;
        }
    

Ⅲ HashMap(存储的是键值对entry)

​ JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突,JDK7采用头插法插入链表元素).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间,具体可以参考 treeifyBin方法(链表转化为红黑树的方法)。

  1. JDK1.8 HashMap类的属性

    public class HashMap extends AbstractMap implements Map, Cloneable, Serializable {
        // 序列号
        private static final long serialVersionUID = 362498820763181265L;    
        // 默认的初始容量是16
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;   
        // 最大容量
        static final int MAXIMUM_CAPACITY = 1 << 30; 
        // 默认的填充因子  0.75!!
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
        // 当桶(bucket)上的结点数大于这个值时会转成红黑树 !!
        static final int TREEIFY_THRESHOLD = 8; 
        // 当桶(bucket)上的结点数小于这个值时树转链表 !!
        static final int UNTREEIFY_THRESHOLD = 6;
        // 桶中结构转化为红黑树对应的table的最小大小 !!
        static final int MIN_TREEIFY_CAPACITY = 64;
        // 存储元素的数组,总是2的幂次倍
        transient Node[] table; 
        // 存放具体元素的集
        transient Set> entrySet;
        // 存放元素的个数,注意这个不等于数组的长度。
        transient int size;
        // 每次扩容和更改map结构的计数器
        transient int modCount;   
        // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
        int threshold;
        // 加载因子
        final float loadFactor;
    }
    
    • loadFactor 加载因子

      ​ 用来控制数组存放数据的疏密程度,趋近于1时数组中存放的数据(entry)越多(容易hash冲突),趋近于0时数组中存放的数据(entry)越少,越稀疏(不容易发生hash冲突)。

      ​ loadFactor太大则查找元素的效率低下,太小导致数组的利用率低,官方默认是0.75(根据统计分布得出)。

      ​ 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。

    • threshold(单词的意思是门槛、阈值)

      threshold = capacity * loadFactor当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准。

  2. Node节点(单链表结点)和TreeNode结点(红黑树结点)

    Node结点源码:

    // 继承自 Map.Entry
    static class Node implements Map.Entry {
           final int hash;// 哈希值,存放元素到hashmap中时用来与其他元素hash值比较
           final K key;//键
           V value;//值
           // 指向下一个节点
           Node next;
           Node(int hash, K key, V value, Node next) {
                this.hash = hash;
                this.key = key;
                this.value = value;
                this.next = next;
            }
            public final K getKey()        { return key; }
            public final V getValue()      { return value; }
            public final String toString() { return key + "=" + value; }
            // 重写hashCode()方法
            public final int hashCode() {
                return Objects.hashCode(key) ^ Objects.hashCode(value);
            }
    
            public final V setValue(V newValue) {
                V oldValue = value;
                value = newValue;
                return oldValue;
            }
            // 重写 equals() 方法
            public final boolean equals(Object o) {
                if (o == this)
                    return true;
                if (o instanceof Map.Entry) {
                    Map.Entry e = (Map.Entry)o;
                    if (Objects.equals(key, e.getKey()) &&
                        Objects.equals(value, e.getValue()))
                        return true;
                }
                return false;
            }
    }
    

    红黑树结点源码:

    static final class TreeNode extends LinkedHashMap.Entry {
            TreeNode parent;  // 父
            TreeNode left;    // 左
            TreeNode right;   // 右
            TreeNode prev;    // needed to unlink next upon deletion
            boolean red;           // 判断颜色
            TreeNode(int hash, K key, V val, Node next) {
                super(hash, key, val, next);
            }
            // 返回根节点
            final TreeNode root() {
                for (TreeNode r = this, p;;) {
                    if ((p = r.parent) == null)
                        return r;
                    r = p;
           }
    
  3. put() 方法

    v2-bb8ee0ee1c0cc51537ae5f8e02038102_1440w

    源码:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node[] tab; Node p; int n, i;
            //第一部分:如果数组空则通过resize方法创建新数组
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
            //第二部分:计算插入位置是否冲突i = (n - 1) & hash(取hash值的低m位,其中n=2^m!!!)。如果不冲突直接创建一个newNode,冲突则转到第三部分处理冲突
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            //第三部分:处理冲突
            else {
                Node e; K k;
                //第三部分第一小节:判断插入的key是否和首结点键相同,相同就先记录下首结点的值,后续在afterNodeAccess(e)中进行处理
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
                //第三部分第二小节:判断插入的数据结构是红黑树吗?是的话执行putTreeVal
                else if (p instanceof TreeNode)
                    e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
                
                //第三部分第三小节:插入的数据结构是链表(寻找链表中是否有和插入的entry相同的键)
                else {
                    for (int binCount = 0; ; ++binCount) {
                        //第三小节第一段:如果遍历到末尾时,先在尾部追加该元素结点(虽然hash冲突了,但是没有同样的key,只需添加到末尾并判断是否要转为红黑树);还要看当前阈值是否超过8,超过了要变红黑树
                        if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null);
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                            break;
                        }
                        //第三小节第二段: 如果找到与我们待插入的元素具有相同的hash和key值的结点,则停止遍历(在链表中找到了与插入的key相同的键)。此时e已经记录了该结点
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        //第三小节第三段
                        p = e;
                    }
                }
                //第三部分第四小节:表面记录到有相同元素的结点(即在链表中发现了key相同的情况)
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            ++modCount;
            //第四部分:插入成功后判断size是否大于阈值threshold,大于就要开始扩容
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }
       
    
  4. 扩容

    v2-f43eafe4e5d4d7616f885335fc185f3d_1440w

    扩容源码分析:

  final Node[] resize() {
           Node[] oldTab = table;
           int oldCap = (oldTab == null) ? 0 : oldTab.length;
           int oldThr = threshold;
           int newCap, newThr = 0;
           //第一部分:扩容。首先如果超过了数组的最大容量,那么就直接将阈值设置为整数最大值,但是无法扩容了;然后如果没有超过,那就扩容为原来的2倍,这里要注意是oldThr << 1,移位操作来实现的
           if (oldCap > 0) {
               if (oldCap >= MAXIMUM_CAPACITY) {
                   threshold = Integer.MAX_VALUE;
                   return oldTab;
               }
               else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                        oldCap >= DEFAULT_INITIAL_CAPACITY)
                   newThr = oldThr << 1; // double threshold
           }
           //第二部分:设置阈值
        //构造函数指定了initialCapacity,则table大小为threshold, 即大于指定initialCapacity的最小的2的整数次幂(可以通过构造函数得出)
           else if (oldThr > 0) 
               newCap = oldThr;
           else {            //通过构造函数没有指定initialCapacity,则赋予默认值(数组大小为16,加载因子为0.75)
               newCap = DEFAULT_INITIAL_CAPACITY;
               newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
           }
        //计算指定了initialCapacitty情况下的新的threshold
           if (newThr == 0) {
               float ft = (float)newCap * loadFactor;
               newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                         (int)ft : Integer.MAX_VALUE);
           }
           threshold = newThr; //为当前的容量阈值赋值
       
       //从以上操作我们知道, 初始化HashMap时, 
       //如果构造函数没有指定initialCapacity, 则table大小为16
       //如果构造函数指定了initialCapacity, 则table大小为threshold, 即大于指定    initialCapacity的最小的2的整数次幂
       
        // 从下面开始, 初始化table或者扩容, 实际上都是通过新建一个table来完成的
           @SuppressWarnings({"rawtypes","unchecked"})
               Node[] newTab = (Node[])new Node[newCap];
           table = newTab;
           //第三部分:旧数据保存在新数组里面
           if (oldTab != null) {
               for (int j = 0; j < oldCap; ++j) {
                   Node e;
                   if ((e = oldTab[j]) != null) {
                       oldTab[j] = null;
                       //只有一个节点,通过索引位置直接映射
                       if (e.next == null)
                           newTab[e.hash & (newCap - 1)] = e;
                       //如果是红黑树,需要进行树拆分然后映射
                       else if (e instanceof TreeNode)
                           ((TreeNode)e).split(this, newTab, j, oldCap);
                       else {
                            //如果是多个节点的链表,将原链表拆分为两个链表
                           Node loHead = null, loTail = null;
                           Node hiHead = null, hiTail = null;
                           Node next;
                           //我们首先准备了两个链表 lo 和 hi, 然后顺序遍历该存储桶上的链表的每个节点, 如果 (e.hash & oldCap) == 0, 我们就将节点放入lo链表, 否则, 放入hi链表.
                           do {
                               next = e.next;
                               if ((e.hash & oldCap) == 0) {
                                   if (loTail == null)
                                       loHead = e;
                                   else
                                       loTail.next = e;
                                   loTail = e;
                               }
                               else {
                                   if (hiTail == null)
                                       hiHead = e;
                                   else
                                       hiTail.next = e;
                                   hiTail = e;
                               }
                           } while ((e = next) != null);
                           
   //如果lo链表非空, 我们就把整个lo链表放到新table的j位置上;如果hi链表非空, 我们就把整个hi链表放到新table的j+oldCap位置上.综上我们知道, 这段代码的意义就是将原来的链表拆分成两个链表, 并将这两个链表分别放到新的table的 j 位置和 j+oldCap 上, j位置就是原链表在原table中的位置, 拆分的标准就是:(e.hash & oldCap) == 0.
                           
                           //链表1存于原索引
                           if (loTail != null) {
                               loTail.next = null;
                               newTab[j] = loHead;
                           }
                           //链表2存于原索引加上原hash桶长度的偏移量
                           if (hiTail != null) {
                               hiTail.next = null;
                               newTab[j + oldCap] = hiHead;
                           }
                       }
                   }
               }
           }
           return newTab;
       }

拆分链表示意图(下图hi和lo的位置反了):

2076773863-5b5e7d6916752_articlex
  1. HashMap遍历

    image-20201212112025529
    image-20201212112047728
    image-20201212112120244

你可能感兴趣的:(Java集合总结(含源码分析))