java集合源碼分析(一):Collection 與 AbstractCollection

概述

我們知道,java 中容器分爲 Map 集合和 Collection 集合,其中 Collection 中的又分爲 Queue,List,Set 三大子接口。

其中, List 應該是日常跟我們打交道最頻繁的接口了,按照 JavaDoc 的說明,List 是一種:

有序集合(也稱爲序列)。此接口的用戶可以精確控制列表中每個元素的插入位置。用戶可以通過其整數索引(在列表中的位置)訪問元素,並在列表中搜索元素。

我們以 List 下 Vector,ArrayList,LinkedList 三大實現爲主,下面是他們之間的一個關係圖。其中,紅色表示抽象類,藍色表示接口。

List集合的實現類關係圖

根據上圖的類關係圖,我們研究一下源碼中,類與類之間的關係,方法是如何從抽象到具體的。

一、Iterable 接口

Iterable 是最頂層的接口,繼承這個接口的類可以被迭代。

Iterable 接口的方法

  • iterator():用於獲取一個迭代器。

  • forEach() :JDK8 新增。一個基於函數式接口實現的新迭代方法。

    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
    
  • spliterator():JDK8 新增。用於獲取一個可分割迭代器。默認實現返回一個IteratorSpliterator類。

    這個跟迭代器類似,但是是用於並行迭代的,關於具體的情況可以參考一下掘金的一個討論:Java8裏面的java.util.Spliterator接口有什麼用?

二、Collection 接口

Collection 接口的方法

Collection 是集合容器的頂級接口,他繼承了 Iterable 接口,即凡是 Collection 的實現類都可以迭代,List 也是 Collection 的子接口,因此也擁有此特性。

可以看到, Collection 接口提供了十九個抽象方法,這些方法的命名都很直觀的反應的這些方法的功能。通過這些方法規定了 Collection的實現類的一些基本特性:可迭代,可轉化爲數組,可對節點進行添加刪除,集合間可以合併或者互相過濾,可以使用 Stream 進行流式處理。

1.抽象方法

我們可以根據功能簡單的分類介紹一下 Collection 接口提供的方法。

判斷類:

  • isEmpty():判斷集合是否不含有任何元素;
  • contains():判斷集合中是否含有至少一個對應元素;
  • containsAll():判斷集合中是否含另一個集合的所有元素;

操作類:

  • add():讓集合包含此元素。如果因爲除了已經包含了此元素以外的任何情況而不能添加,則必須拋出異常;
  • addAll():將指定集合中的所有元素添加到本集合;
  • remove():從集合移除指定元素;
  • removeAll():刪除也包含在指定集合中的所有此集合的元素;
  • retainAll:從此集合中刪除所有未包含在指定集合中的元素;
  • clear():從集合中刪除所有元素;

輔助類:

  • size():獲取集合的長度。如果長度超過 Integer.MAX_VALU 就返回 Integer.MAX_VALU;

  • iterator():獲取集合的迭代器;

  • toArray():返回一個包含此集合中所有元素的新數組實例。因爲是新實例,所以對原數組的操作不會影響新數組,反之亦然;

    它有一多態方法參數爲T[],此時調用 toArray()會將內部數組中的元素全部放入指定數組,如果結束後指定數組還有剩餘空間,那剩餘空間都放入null。

2.JDK8 新增抽象方法

此外,在 JDK8 中新增了四個抽象方法,他們都提供了默認實現:

  • removeIf:相當於一個filter(),根據傳入的函數接口的匿名實現類方法來判斷是否要刪除集合中的某些元素;
  • stream():JDK8 新特性中流式編程的靈魂方法,可以將集合轉爲 Stream 流式進行遍歷,配合 Lambda 實現函數式編程;
  • parallelStream():同 stream() ,但是是生成並行流;
  • spliterator():重寫了 Iterable 接口的 iterator()方法。

3.equals 和 hashCode

值得一提的是 Collection 還重寫了 Object 的 equals()hashCode() 方法(或者說變成了抽象方法?),這樣實現 Collection 的類就必須重新實現 equals()hashCode() 方法

三、AbstractCollection 抽象類

AbstractCollection 是一個抽象類,他實現了 Collection 接口的一些基本方法。JavaDoc 也是如此描述的:

此類提供了Collection接口的基本實現,以最大程度地減少實現此接口所需的工作。

通過類的關係圖,AbstractCollection 下面還有一個子抽象類 AbstractList ,進一步提供了對 List 接口的實現。 我們不難發現,這正是模板方法模式在 JDK 中的一種運用。

0.不支持的實現

在這之前,需要注意的是,AbstractCollection 中有一些比較特別的寫法,即實現了方法,但是默認一調用立刻就拋出 UnsupportedOperationException異常:

public boolean add(E e) {
    throw new UnsupportedOperationException();
}

如果想要使用這個方法,就必須自己去重寫他。這個寫法讓我糾結了很久,網上找了找也沒找到一個具體的說法。

參考 JDK8 新增的接口方法默認實現這個特性,我大膽猜測,這應該是針對一些實現 Collection 接口,但是又不想要實現 add(E e)方法的類準備的。在 JDK8 之前,接口沒有默認實現,如果抽象類還不提供一個實現,那麼無論實現類是否需要這個方法,那麼他都一定要實現這個方法,這明顯不太符合我們設計的初衷。

1.isEmpty

非常簡短的方法,通過判斷容器 size 是否爲0判斷集合是否爲空。

public boolean isEmpty() {
    return size() == 0;
}

2.contains/containsAll

判斷元素是否存在。

public boolean contains(Object o) {
    Iterator<E> it = iterator();
    // 如果要查找的元素是null
    if (o==null) {
        while (it.hasNext())
            if (it.next()==null)
                return true;
    } else {
        while (it.hasNext())
            if (o.equals(it.next()))
                return true;
    }
    return false;
}

containsAll()就是在contains()基礎上進行了遍歷判斷。

public boolean containsAll(Collection<?> c) {
    for (Object e : c)
        if (!contains(e))
            return false;
    return true;
}

3.addAll

addAll()方法就是在 for 循環裏頭調用 add()

public boolean addAll(Collection<? extends E> c) {
    boolean modified = false;
    for (E e : c)
        if (add(e))
            modified = true;
    return modified;
}

4.remove/removeAll

remove()這個方法與 contains()邏輯基本一樣,因爲做了null判斷,所以List是默認支持傳入null的

public boolean remove(Object o) {
    Iterator<E> it = iterator();
    if (o==null) {
        while (it.hasNext()) {
            if (it.next()==null) {
                it.remove();
                return true;
            }
        }
    } else {
        while (it.hasNext()) {
            if (o.equals(it.next())) {
                it.remove();
                return true;
            }
        }
    }
    return false;
}

5.removeAll/retainAll

removeAll()retainAll()的邏輯基本一致,都是通過 contains()方法判斷元素在集合中是否存在,然後選擇保存或者刪除。由於 contains()方法只看是否存在,而不在意有幾個,所以如果目標元素有多個,會都刪除或者保留。

public boolean removeAll(Collection<?> c) {
    Objects.requireNonNull(c);
    boolean modified = false;
    Iterator<?> it = iterator();
    while (it.hasNext()) {
        if (c.contains(it.next())) {
            it.remove();
            modified = true;
        }
    }
    return modified;
}
public boolean retainAll(Collection<?> c) {
    Objects.requireNonNull(c);
    boolean modified = false;
    Iterator<E> it = iterator();
    while (it.hasNext()) {
        if (!c.contains(it.next())) {
            it.remove();
            modified = true;
        }
    }
    return modified;
}

5.toArray(擴容)

用於將集合轉數組。有兩個實現。一般常用的是無參的那個。

public Object[] toArray() {
    // 創建一個和List相同長度的數字
    Object[] r = new Object[size()];
    Iterator<E> it = iterator();
    for (int i = 0; i < r.length; i++) {
        // 如果數組長度大於集合長度
        if (! it.hasNext())
            // 用Arrays.copyOf把剩下的位置用null填充
            return Arrays.copyOf(r, i);
        r[i] = it.next();
    }
    // 如果數組長度反而小於集合長度,就擴容數組並且重複上述過程
    return it.hasNext() ? finishToArray(r, it) : r;
}

其中,在 finishToArray(r, it) 這個方法裏涉及到了一個擴容的過程:

// 位運算,擴大當前容量的一半+1
int newCap = cap + (cap >> 1) + 1;
// 如果擴容後的大小比MAX_ARRAY_SIZE還大
if (newCap - MAX_ARRAY_SIZE > 0)
    // 使用原容量+1,去判斷要直接擴容到MAX_ARRAY_SIZE,Integer.MAX_VALUE還是直接拋OutOfMemoryError異常
    newCap = hugeCapacity(cap + 1);
r = Arrays.copyOf(r, newCap);

這裏的 MAX_ARRAY_SIZE 是一個常量:

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

這裏又通過hugeCapacity()方法進行了大小的限制:

private static int hugeCapacity(int minCapacity) {
    // 如果已經大到溢出就拋異常
    if (minCapacity < 0)
        throw new OutOfMemoryError
        ("Required array size too large");
    // 容量+1是否還是大於允許的數組最大大小
    return (minCapacity > MAX_ARRAY_SIZE) ?
        // 如果是,就把容量直接擴大到Integer.MAX_VALUE
        Integer.MAX_VALUE :
    // 否則就直接擴容到運行的數組最大大小
    MAX_ARRAY_SIZE;
}

6.clear

迭代並且刪除全部元素。

Iterator<E> it = iterator();
while (it.hasNext()) {
    it.next();
    it.remove();
}

7.toString

AbstractCollection 重寫了 toString 方法,這也是爲什麼調用集合的toStirng() 不是像數組那樣打印一個內存地址的原因。

public String toString() {
    Iterator<E> it = iterator();
    if (! it.hasNext())
        return "[]";

    StringBuilder sb = new StringBuilder();
    sb.append('[');
    for (;;) {
        E e = it.next();
        sb.append(e == this ? "(this Collection)" : e);
        if (! it.hasNext())
            return sb.append(']').toString();
        sb.append(',').append(' ');
    }
}

四、總結

Collection

Collection 接口類是 List ,Queue,Set 三大子接口的父接口,他繼承了 Iterable 接口,因而所有 Collection 的實現類都可以迭代。

Collection 中提供了規定了實現類應該實現的大部分增刪方法,但是並沒有規定關於如何使用下標進行操作的方法。

值得注意的是,他重規定了 equlas()hashCode()的方法,因此 Collection 的實現類的這兩個方法不再跟 Object 類一樣了。

AbstractCollection

AbstractCollection 是實現 Collection 接口的一個抽象類,JDK 在這裏使用了模板方法模式,Collection 的實現類可以通過繼承 AbstractCollection 獲得絕大部分實現好的方法。

在 AbstractCollection 中,爲add()抽象方法提供了不支持的實現:即實現了方法,但是調用卻會拋出 UnsupportedOperationException。根據推測,這跟 JDK8 接口默認實現的特性一樣,是爲了讓子類可以有選擇性的去實現接口的抽象方法,不必即使不需要該方法,也必須提供一個無意義的空實現。

AbstractCollection 提供了對添加複數節點,替換、刪除的單數和複數節點的方法實現,在這些實現裏,因爲做了null判斷,因此是默認是支持傳入的元素爲null,或者集合中含有爲null的元素,但是不允許傳入的集合爲null。

AbstractCollection 在集合轉數組的 toArrays() 中提供了關於擴容的初步實現:一般情況下新容量=舊容量 + (舊容量/2 + 1),如果新容量大於 MAX_ARRAY_SIZE,就會使用 舊容量+1去做判斷,如果已經溢出則拋OOM溢出,大於 MAX_ARRAY_SIZE 就使用 Integer.MAX_VALUE 作爲新容量,否則就使用 MAX_ARRY_SIZE。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章