Java集合详解(含JDK1.8源码)(完结)

ArrayList 集合

ArrayList 扩容机制

  1. ArrayList 中维护了一个 Object 类型的数组 elementData.

    transient Object[] elenentData;//transient表示瞬间,短暂的,表示该属性不会被序列号

  2. 当创建ArrayList对象时,如果使用的是无参构造器,则初始 elementData 容量为 0,第1次添加,则扩容 elementData 到 10;如需要再次扩容,则扩容 elementData 为1.5倍
  3. 如果使用的是指定大小的构造器,则初始 elementData 容量为指定大小,如果需要扩容则直接扩容 elementData 为1.5倍

ArrayList 扩容底层源码

public class ArrayListSource {
    public static void main(String[] args) {
        // 先来一段简单的代码
        ArrayList list = new ArrayList();

        for (int i = 1; i <= 10; i++) {
            list.add(i);
        }

        for (int i = 11; i <= 15; i++) {
            list.add(i);
        }

        list.add(100);
        list.add(200);
        list.add(null);

    }
}

我们来一步一步看它的流程。

无参构造器创建 ArrayList 对象

        首先在创建调用 ArrayList 无参构造器的时候(new ArrayList()时,返回的是一个默认的空的数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA)

    public ArrayList() {
        // 无参构造器返回空数组
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

ArrayList对象 调用 add() 方法

  1. 首先判断是否需要扩容,调用 ensureCapacityInternal() 方法
  2. 然后进行赋值操作
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 确定是否需要扩容
        elementData[size++] = e; // 将值赋值进数组中
        return true;
    }

确定是否需要扩容(minCapacity为最小扩容长度)

  1. 首先将特殊的情况,数组为空时,也就是 ArrayList 无参构造器返回的空数组进行判断,调用的是 calculateCapacity() 方法。
  2. 然后进行 需要的数组长度和目前的数组长度 对比,按需求进行扩容。调用的是 ensureExplicitCapacity() 方法。
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

特殊判断是否为默认的空数组 

        在处理特殊情况时,也就是空数组的第一次扩容,进行了特殊判断,如果是第一次扩容则返回10。也就是将空数组扩容到10。

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // DEFAULT_CAPACITY = 10;
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

确定是否扩容

        将特殊情况处理完成之后,得到了真正需要的数组长度传入 ensureExplicitCapacity() 方法进行最后的判断,按照需求进行扩容处理

    private void ensureExplicitCapacity(int minCapacity) {
        // 记录集合被修改的次数,可以检查线程安全问题
        modCount++;

        // 如果最小需要的长度大于目前数组的长度,则进行扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

扩容

        扩容方法 grow() ,当确定扩容的时候,首先会将需要扩充的长度计算出来,也就是原数组的 1.5 倍(空数组第一次扩容到10),然后使用 Arrays 库的 copyOf() 方法进行扩容。

  1. 确定扩容大小
  2. 进行扩容
    private void grow(int minCapacity) {
        // 将数组长度赋值给变量
        int oldCapacity = elementData.length;
        // 按照当前数组长度的 1.5 倍进行扩容,求得新的数组长度
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 如果是空数组的第一次扩容,则直接将 最小的需求长度赋值给 扩充的长度
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 扩容操作
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

总结

        在添加方法 add() 的数组扩容中,需要对空数组进行特殊判断。然后先计算出最小的需求的数组长度(minCapacity),如果目前的数组长度小于这个长度的情况,就使用 Arrays.copyOf() 方法进行扩容。

Vector 集合

Vector 扩容机制

  1. Vector 同样也是使用 数组 作为底层的数据结构来进行数据的处理。同样维护了一个Object类型的数组 elementData。可以看的出来 Vector 真的和 ArrayList 很像。

  2. 当创建 Vector 对象时,如果使用的是无参构造器,则初始 elementData 容量为 10,容量扩充属性 capacityIncrement 为 0.

  3. 当使用 add() 方法时,在数组空间不够的情况下会进行扩容。如果 capacityIncrement 大于 0,则每一次扩容按照该值扩容;如果为 0,则按照当前数组容量进行翻倍扩容

Vector 扩容底层源码

public class Vector_ {
    public static void main(String[] args) {
        Vector vector = new Vector();


        for (int i = 1; i < 11; i++) {
            vector.add(i);
        }

        vector.add(11);


    }
}

我们继续一步一步看它的流程。

无参构造器创建对象

  1. 在使用无参构造器的时候,会默认调用有参构造器并传入参数 10,也就是我们的默认初始容量
  2. 在调用了有参构造器后,有参构造器会再次调用另一个有参构造器,并将初始容量和 0 作为参数传入。0 也就是我们的容量扩充属性 capacityIncrement。
  3. 在完成上面两步之后,开始真正的创建一个数组,并返回。

源码如下:👇

    // JDK1.8.0_33 源码
    public Vector() {
        this(10);
    }

    public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }

    public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }

add() 方法扩容

        因为Vector对象默认就是 10 的初始数组容量,所以前面添加十个元素没什么好说的。在添加第十一个元素时,因为需求的大小是 11 大于了默认的初始容量 10,触发了扩容机制。

  1. 首先 add() 方法去判断是否需要扩容
  2. 然后将数据存储
    public synchronized boolean add(E e) {
        // 为修改集合的次数
        modCount++;

        // 确定是否扩容
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

确定是否需要扩容(minCapacity为最小扩容长度)

        如果最小的需求容量大于了当前数组的长度,则进行扩容处理。

    private void ensureCapacityHelper(int minCapacity) {
        // minCapacity 为最小的需求容量
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

扩容

  1. 先计算出扩容后的数组的长度。默认按照 2倍 扩容,如果 容量扩充(capacityIncrement)大于 0 的话,则每次扩容增加 其值 的长度。
  2. 计算出新数组的长度后,使用 Arrays.copyOf() 方法进行扩容。
    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        // 确定新的数组长度
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

LinkedList 集合

        和上述三个集合不同的是 LinkedList 集合使用的是链表作为最底层的数据结构实现的一个双向的链表集合结构。这就在结构上简化了添加和删除节点操作,不用考虑数组的扩容问题,故添加删除效率很高

添加元素 add()

    public boolean add(E e) {
        linkLast(e);
        return true;
    }

    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        // 将添加的节点设置为尾节点
        last = newNode;

        // 如果添加前 集合为空,则将头节点也设置为新添加的节点
        // 否则头节点不动,将之前的尾节点的后向指针指向新节点
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

        在添加元素的核心代码部分,因为 LinkedList 是一个双向链表结构,需要保证其 “双向” 的一个特点。既然是双向的,那么在添加时需要知道前后的元素,也就是得考虑集合是否为空这个问题。

  • 添加新节点,需要添加在末尾。也就是 集合对象 的 last 属性得指向这个新节点;
  • 在集合为空时,就得保证 集合对象 的 first 属性指向新添加的节点;
  • 在集合不为空时,就得保证 原先尾节点得指向这个新的节点;

移除元素 remove()

    public E remove() {
        return removeFirst();
    }

    // 判断如果集合为空就抛出异常
    public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }

    // 真正移除首节点的代码
    private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        final E element = f.item;
        final Node<E> next = f.next;
        f.item = null;
        // 将f节点垃圾化,在 GC 算法下发现其没有指向将其当作垃圾处理
        f.next = null; // help GC
        // 将集合的first属性指向第二个节点
        first = next;

        // 如果集合为空了,则将尾节点也设为null
        // 否则尾节点不动,将头节点的前向指针设置为null
        if (next == null)
            last = null;
        else
            next.prev = null;
        // 更新节点个数和修改次数
        size--;
        modCount++;
        // 返回首节点的元素内容
        return element;
    }

        在移除节点的核心代码部分,同样得保证其双向链表的结构,故也得考虑集合是否为空的问题。(头节点举例)

  • 移除头节点,需要将该节点的next设置为null。让其不再指向这个集合;
  • 如果在删除后集合为空,则需要将尾节点也设置为空。也就是last属性为null;
  • 删除之后集合不为空,则需要将之前指向这个节点的指针不再指向它

HashSet 集合(HashMap)

描述

  • HashSet 实现了 Set 接口
  • HashSet 实际上是 HashMap,JDK1.8源码如下👇
  • 可以存放 null 值,但是只能有一个 null(因为 Set 接口保证元素不重复)
  • HashSet 不保证元素是有序的,取决于 hash 后,再确定索引的结果

扩容机制

  1. HashSet 的底层就是 HashMap 集合
  2. 在添加一个元素时,先得到 hash 值,并将 hash 值 处理 转化为 索引值
  3. 找到存储数据表 table,判断这个索引位置是否已经存放了元素
  4. 如果没有,直接添加进 table
  5. 如果有,调用该对象的 equals 方法比较,如果相同,则放弃添加(Set特性);如果不相同,遍历该节点的链表 next,都不相同则存放。
    1. 所以在添加两个 new String("aaa")时,只能添加进一个 "aaa" 字符串
    2. 因为虽然创建了两个 String 对象,它们的地址是不相同的,但是 在String类 中,equals 比较的是 String 对象的内容。所以在添加进 HashSet 时,hashSet 调用了 equals 方法后 放弃添加了第二个一样的 String 对象。
  6. 在 Java8 中,如果 HashMap 的某一个节点下 挂载的元素个数(链表长度)到达TREEIFY_THRESHOLD(默认是 8)时,会调用 treeifyBin() 方法尝试 树化(将 链表 转化为 红黑树 结构)。如果 table 的大小 >= MIN_TREEIFY_CAPACITY(默认 64),就会将底层数组存储转化为 红黑树;如果没有达到这个临界值,那么 HashMap 会使用 扩容数组 resize() 方法 进行解决链表过长的问题。

加载因子(DEFAULT_LOAD_FACTOR)

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

加载因子就是表示Hash表中元素的填满程度。

加载因子 = 填入表中的元素个数 / 散列表的长度

加载因子越大,填满的元素越多,空间利用率越高,但发生冲突的机会变大了;

加载因子越小,填满的元素越少,冲突发生的机会减小,但空间浪费了更多了,而且还会提高扩容rehash操作的次数;使得数组长度很大,扩容起来耗时更长。

冲突的机会越大,说明需要查找的数据还需要通过另一个途径(链表或红黑树)查找,这样查找的成本就会越高。因此,必须在“冲突的机会”与“空间利用率”之间,寻找一种平衡与折中。

所以我们在寻找这个平衡点时,就需要借助到数学的知识(如统计学、概率论等)。以便让元素均匀的填充数组容器,而不是塞满链表。 

在 Java 8 HashMap 源码中有一段这样下面的注释,它解释了默认的加载因子的考量。

翻译:

        作为一般规则,默认负载因子(0.75)在 时间 和 空间 成本之间提供了一个很好的 权衡

较大的值减少了空间开销,但增加了查找成本(反映在 HashMap 类的大多数操作中,包括 get put )。在设置 map 的初始容量时,应考虑 map 中的预期条目数及其负载因子,以尽量减少 rehash 操作的数量。如果初始容量大于最大条目数除以负载因子,则不会发生重散列操作。

HashSet 底层源码

构造器

        HashSet 构造器直接调用的 HashMap 的构造器。因为 HashSet 底层就是使用的 HashMap 做的数据存储。默认长度16,默认加载因子0.75

    public HashSet() {
        map = new HashMap<>();
    }

add() 添加方法

        PRESENT 对象是一个静态的成员变量,因此不管添加多少个该对象,都只会占用一份空间。

    (static) Object PRESENT = new Object();

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

执行 hashMap 中 put()

        具体的添加过程在上面的 扩容机制 里面写清楚了。

        计算 hash 值 ——> 处理成 index 下标 ——> 判断是否有元素 ——> 判断该元素或者其下面链表是否相同(使用 equals 方法进行对比) ——> 添加或不添加(Set 接口特性--去重)

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    // 得到对象的 hash 值,并做处理 降低 hash 冲突
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 初始化 table 数组
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        // 模板方法模式,留给其子类实现排序等操作,对于 HashMap 类本身 是一个空方法
        afterNodeInsertion(evict);
        return null;
    }

    // 空方法,用于子类继承重写(模板方法模式)
    void afterNodeInsertion(boolean evict) { }

LinkedHashSet 集合(LinkedHashMap)

描述

  1. LinkedHashSet HashSet 的子类
  2. LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个 数组 + 双向链表
  3. LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序,这事得元素看起来是以插入顺序保存的。但其实都是保存在 数组 或者 数组下面的链表的(与 HashSet 相同)
  4. LinkedHashSet 不允许添加重复元素

        LinkedHashSet 相比于 HashSet 多维护了一个 双向链表,也就是多出来的 Linked。因为多出来了这个 双向链表,所以 LinkedHashSet 也多了一些 HashSet 没有的属性也就是指向 双向链表 头和尾 的两个指针——headtail使用 两个指针 遍历时,有点像在用 LinkedList 的感觉,保证了插入顺序和遍历顺序一致。

        在添加元素时,LinkedHashSet 其实和 HashSet 一样 添加到数组 并在每一个节点上挂一个链表,因为它也是 HashSet 的一个子类 拥有父类的特性。不过在 HashSet 数组 链表 的基础上又增加了一个 双向链表,这个双向链表是根据 添加的顺序 一个一个按照顺序链接上的。即在父类 HashSet 集合的基础上,每一个节点多了一个 before after 属性,分别指向在它 之前 添加的节点和在它 之后 添加的节点。

 源码部分因为是继承的 HashSet,很多一样的看 HashSet 就好。

HashMap 

add底层源码(JDK1.8)

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;

        // 如果 table 数组表为空,则进行第一次扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

        // i 为求得的下标索引,当该下标对应的节点为 空时,则创建一个新节点放进数组
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;

            // 两个节点key相同情况,替换节点(value)。先比较hash值进行第一步筛选
            // 筛选完成之后剩下的比较地址 || 使用 equals 方法比较,通过则说明是一样的key
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

            // 如果是一颗树,按照树的方式添加,先不展开
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

            // 如果是一个链表,前面已经通过hash值确定了下标,且数组中的节点不相同
            // 则循环链表进行比对,如果有一样的则不添加,否则添加到末尾。
            else {
                for (int binCount = 0; ; ++binCount) {
                    // 到末尾了,进行添加节点,并根据链表的长度尝试进行树化
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 比较两个节点是否key相同。如果两个节点相同则退出循环,修改value值
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
                
            // 如果不为空,说明中途就退出循环了,则修改替换 对应 key 的 value
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        
        // 如果到达临界值(根据加载因子)(默认12 24 48...),则进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

关于树化 treeifyBin()

        在树化前,会进行判断如果是 数组为空 || 数组长度小于 64,则不会进行树化,而是通过扩容去解决链表过长的问题。

    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // 判断表是否为空,或者表(数组)的长度小于 64
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else 
            ......
    }

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值