Java 集合源码详解 —— Collection

396 阅读12分钟

Java Collection集合源码详解 —— Collection


作者:shiwyang

Java版本:1.8

  • 深入了解集合源码与数据结构的关系,了解集合内部不同子类的共性与区别
  • 一共分为三个部分:Collection Map ConcurrentHashMap
  • 这是第一个部分 Collection 集合部分,主要分为 List 和 Set 两个模块

集合概述

image-20220224192132619.png

  • 集合主要是两组(单列,K-V)
  • Collection接口由两个重要的子接口 List Set ,他们的实现子类都是单列集合
  • Map接口的实现子类 是双列集合,存放K-V的

List 接口

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

List中主要使用的时ArrayList Vector LinkedList 三个类,通过学习三个类的作用以及内部实现源码,我们可以了解List接口的共性和三个类之间的特性。

ArrayList

  • ArrayList的底层是一个Object[]数组用于存储元素,当数组内元素已满之后,要再添加元素的时候就会触发ArrayList的扩容机制。

  • ArrayList 是一个线程不安全的,在多线程的时候不要使用这个类。

  • ArrayList 可以添加任意元素。

ArrayList扩容机制

**说明:**ArrayList有一个自动扩容机制,通过阅读源码,来理解这个扩容机制是怎么实现的。

构造函数:ArrayList 有两个构造函数,分别是无参构造,和int 对象的参数构造。ArrayList的初始长度就是参数携带值的长度,如果是无参的话,就默认为10

**扩容机制:**当ArrayList内需要添加新元素,但是List满了的话,就会触发这个扩容机制。每次扩容为ArrayList当前数组的1.5倍长度。

源码解读:

ArrayList的底层实现是通过一个elementData的Object[]数组来存储元素的,提供了一个默认的长度值:10。

注意:transient字段是指在 序列化过程中需要屏蔽这个值。

// 默认长度大小 : 10
private static final int DEFAULT_CAPACITY = 10;

// ArrayList 存储元素的Object[]数组
transient Object[] elementData; // non-private to simplify nested class access

**第一步:**ArrayList提供了两个构造函数,规定了ArrayList构造的时候可以放入一个 >= 0的int参数,作为ArrayList的初始长度。

这里使用不同的两个构造函数会出现两种情况:

  • 使用无参的构造函数,会赋予一个默认的值,但是Object[]的长度还是0,要直到放入第一个元素的时候才会将Object[]数组扩容到10
  • 使用有参的构造函数,就会直接把Object[]数组创建为对应的数值。
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
      	//创建一个新的Object[]数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

/**
 * Constructs an empty list with an initial capacity of ten.
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

第二步: 执行add方法时,判断数组是否需要扩容。

确认容量函数中,假如数组为空,就附默认值minCapacity = 10对应无参构造函数第一次添加元素的情况。

public boolean add(E e) {
  	// 确认容量函数
    ensureCapacityInternal(size + 1);  // Increments modCount!!
  	//size++ 的意思是先把e元素放进elementData[size] 之后,size++
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {‘
  	// 无参构造时第一次添加元素的情况。
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
		
    ensureExplicitCapacity(minCapacity);
}

**第三步:**通过执行ensureExplicitCapacity() 函数,记录ArrayList的操作数和扩容判断。

private void ensureExplicitCapacity(int minCapacity) {
  	// 记录操作数
    modCount++;
		
  	// 扩容判断
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
      	// 扩容
        grow(minCapacity);
}

第四步: 扩容的具体实现。

  • 第一个if对应的是当无参构造时,第一次添加元素的时候elementData.length = 0;minCapacity = 10;则该数组只需要扩容到默认值10。

  • 第二个if,是当扩容的后的数值大于int最大值时的具体处理。

  • 扩容使用的函数是Arrays里面的拷贝函数

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    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);
}

通过以上四步,可以实现对ArrayList的扩容,这样就可以在每次容量不够的时候都能够实现自动扩容了。

Vector

  • Vector的底层也是一个Object[]数组,和ArrayList一样,也有一个自动扩容机制。

  • Vector是线程同步的,即线程安全。

Vector 扩容机制

**说明:**Vector 有一个自动扩容机制,通过阅读源码,来理解这个扩容机制是怎么实现的。

构造函数:Vector 有两个构造函数,分别是无参构造,和int 对象的参数构造。Vector 的初始长度就是参数携带值的长度,如果是无参的话,就默认为10

扩容机制:当Vector 内需要添加新元素,但是List满了的话,就会触发这个扩容机制。每次扩容为Vector 当前数组的2倍长度,Vector还可以在构造函数里面设置默认扩容的大小参数。

当使用add函数的时候,和ArrayList 的处理方式基本一样,关键就是grow函数不同。

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

private void ensureCapacityHelper(int minCapacity) {
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

Grow函数:

函数中有一个三元判断式

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

当capacityIncrement 大于零时 取capacityIncrement 否则取oldCapacity

通过这个三元判断式子,就可以实现默认扩容两倍和自定扩容大小的功能。

private void grow(int minCapacity) {
    // overflow-conscious code
    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的底层结构是一个双向链表。
  • 可以添加任何元素
  • 线程不安全

LinkedList 源码解析

**构造函数:**默认的无参构造函数,什么都没做,因为没有元素,不需要维护链表。

public LinkedList() {
}

**add函数:**将元素链接到链表末尾。

public boolean add(E e) {
    linkLast(e);
    return true;
}
void linkLast(E e) {
    final Node<E> l = last;
  	// 创建一个新的结点,pre结点指向原链表的last,next指向空
    final Node<E> newNode = new Node<>(l, e, null);
  	// 将last改为最新的元素
    last = newNode;
  	// 当原先的last为空时,说明原链表为空,因此头尾都应该是新加入的结点。
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
  	// 修改链表的规模和操作数
    size++;
    modCount++;
}

remove函数

remove函数有三个重载:

  • 无参 删除头
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
  • 删除指定的元素e,需要遍历数组,找到相符合的元素删除即可
public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}
  • 删除指定的序号index,判断原链表是否有该index,如果有就删除对应的元素
public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

private void checkElementIndex(int index) {
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private boolean isElementIndex(int index) {
    return index >= 0 && index < size;
}

poll函数:poll函数的功能就是获取并删除最前面的元素,当头结点为空时,获取空元素。

public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

pop函数:pop和remove()的功能一样,和poll函数功能相近,跟poll不同的是,poll是基于队列实现的,当队列头为空时,取空,pop是基于栈实现的,当栈为空时抛出异常。

public E pop() {
    return removeFirst();
}
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

push函数:将元素推出栈顶,基于栈思想实现。

public void push(E e) {
    addFirst(e);
}

public void addFirst(E e) {
		linkFirst(e);
}
private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    // 当原链表的头结点为空时,说明链表为空,所以该元素加入后,原链表的尾结点也应该是新加入的元素
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

unlinkFirst函数:

这个函数有一个重点就是在清楚一个函数结点的时候,需要把该节点链接取消,使得jvm的GC能正常运行。

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.next = null; // help GC
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

Set接口

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

  • Set 主要使用的实现有HashSet LinkedHashSet

HashSet

  • 实现了Set 接口
  • HashSet 实际上是HashMap
  • 可以存放null,但只能有一个null
  • 不保证元素是有序的,取决于hash之后,再确定索引的结果
  • 不能有重复的对象

HashSet(HashMap) 介绍

  • HashSet 就是一个HashMap底层是一个数组 + 链表 + 红黑树 的结构。
  • 添加一个元素时,会先得到hash值,再转化为索引值
  • 找到存储数据表table ,看索引位置是否已经存放元素
  • 如果没有直接加入
  • 如果有,调用equals比较,相同就放弃添加,不同就添加到最后
  • 在Java8中,如果一个链表的元素个数超过某个值(默认为8),并且table的大小超过某个值(默认为64),就会发生树化(红黑树)

HashSet为什么能保证数据唯一性: 因为HashSet底层就是一个HashMap,K对应的是需要保存的对象,在HashMap中,K保证是唯一的(利用HashCode()计算hash值),所以可以保证存入的元素永远是唯一的。

HashSet 源码解析

构造函数: HashSet的构造函数是构造了一个HashMap。

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

add方法: add方法,调用的是HashMap的put方法,put方法调用的是putVal方法。

putVal方法的几个参数:

  • hash:key 的哈希值 -> 有一个哈希值计算函数 hash(key)
  • key: key值,对应的是需要放置的元素e
  • value:存放的是HashSet定义的默认PRESENT对象 Object(),只用于占位
  • onlyIfAbsent :假如为真,不改变Value
  • evict: 假如为假,
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

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

获取hash值函数: 利用Object携带的hashCode方法。当key不为空的时候,计算出key的hash值

hashCode是native方法,底层使用是c实现

右移16位是防止冲突,至于为什么能防止冲突,我也不太清楚

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

putVal 函数(重点)

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是HashMap自带的存放node结点的数组
  	// 当表为空时,初始化表,16个空间
    if ((tab = table) == null || (n = tab.length) == 0)
      	// resize()的作用是初始化或加倍表的大小。如果为空,则按照字段阈值中持有的初始容量目标分配。
        n = (tab = resize()).length;
  	// 根据hash值取计算Key 应该存放到table表的哪个索引位置i
  	// 并把这个这个对象赋给p
  	// 判断p 如果为null 表示还没有存放过元素,就创建一个Node 放在该位置,相当于数组[p]链表头
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k; // 辅助变量
      	// 如果当前索引位置对应的链表的第一个元素和准备添加的元素的hash值相同。
      	// 并且满足 下面两个条件之一,就不能加入hashMap中
      	// ((k = p.key) == key 是相同的对象
      	// (key != null && key.equals(k) key不等于空,且equals方法为true
      	// 这就可以解释,为什么存放相同的 new String("tom") 放不进去 但是new 自己建立的对象,放得进去,因为String类里面重写了Object的equals方法,可以使得文字内容相同时equals返回true
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
      	// 再判断p 是不是红黑树,如果是,就使用putTreeVal()。涉及了很多红黑树的算法
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
      	// 否则,遍历该数组元素的链表,查看该链表上,有没有相同的元素,如果有,就不加入,如果没有,就加到最后一个。
        else {
          	// 死循环,使用break; 跳出
            for (int binCount = 0; ; ++binCount) {
              	// 假如链表的下一个为空,说明链表遍历结束了,把新的元素添加到链表尾部,退出循环
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                  	// 当链表内元素大于TREEIFY_THRESHOLD(8)了,触发树化函数treeifyBin()
                  	// 这个树化函数有一个机制,就是要链表的长度大于8,且数组的长度大于64,如果数组的长度小于64,就进行扩容*2,不进行树化。
                  	// 数组扩容之后可能会导致原来的元素在数组上的位置发生改变
                    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;
            }
        }
      	// 对退出循环之后的临时变量e进行判断,假如e非空,说明这个元素在原来的hashMap上已经存在,所以,返回这个元素。
      	// 为什么e == null 说明链表中没有相同的元素呢? 因为在上面循环中 e一直等于p.next 假如p.next == null 说明链表已经遍历到最后了,也就是说前面的所有元素都跟要添加的元素不同,如果e!=null ,也就是说p.next != null 说明e这个元素触发了第二个if 退出了循环。
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                // 在HashSet中,使用的都是同一个Value对象,所以在HashSet里面这一句没意义
              	// 在HashMap中,这也成为了当key值相同时,为什么Value会替换成最新的value的原因
                e.value = value;
            afterNodeAccess(e);
          	// 表示添加的这个元素已经存在,并且返回改元素。
            return oldValue;
        }
    }
    ++modCount;
  	// threshold 是在resize()函数里面设置过的,就是0.75的hash数组的空间,一旦超过这个空间就对Hash数组扩容,防止多个线程大量范围的时候,发生阻塞
  	// 每加入一个结点size就会++ ,也就是说,不需要占满12个数组位置,只需要填入12个元素,就会触发数组扩容。
    if (++size > threshold)
        resize();
  	// 在这个类里,这个方法是一个空方法
    afterNodeInsertion(evict);
  	// 返回空代表添加的时候没有这个对象
    return null;
}

treeifyBin:函数

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
  	// 如果容量不够,扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
  	// 如果容量足够,将链表树化
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

注意: 在自定义类的方法需要判断的是否相同的时候,需要同时重写hashCode和equals方法.

  • hashCode 方法保证,元素相同时会计算出同一个hash值
  • equals方法,保证元素相同时返true,默认是对比元素的指针地址。

LinkedHashSet

  • LinkedHashSet是HashSet的子类,LinkedHashSet本质是HashSet的增强子类,目的就是为了记录元素的存储次序。
  • LinkedHashSet的底层是一个LinkedHashMap,底层维护了一个 数组 + 双向链表(散列链表)
  • LinkedHashSet 根据元素的HashCode值来决定元素的存储位置,同时用链表维护元素的次序,使得元素看起来是以插入顺序保存的
  • 不允许添加重复的元素(Set 共性)

LinkedHashSet和HashSet的异同

  • 相同点:
    • 都是数组 + 链表的存储结构
    • 都有Set 的特性
    • 查找的时候都利用HashCode 快速定位。
  • 不同点:
    • LinkedHashSet 的链表顺序是按照元素插入顺序,而HashSet是按照同一个数组位置的插入顺序
    • LinkedHashSet读数据是按照元素插入顺序来读取,HashSet不是
    • HashSet存放的是Node[]类型的。LinkedHashSet 存放的是 Entry类型的,继承了Node,因为双向链表需要保存一个before after。