閱讀原始碼,從ArrayList開始

昌肅發表於2020-10-09

前言

為啥要閱讀原始碼?一句話,為了寫出更好的程式。
一方面,只有瞭解了程式碼的執行過程,我們才能更好的使用別人提供的工具和框架,寫出高效的程式。另一方面,一些經典的程式碼背後蘊藏的思想和技巧很值得學習,通過閱讀原始碼,有助於提升自己的能力。當然,功利的講,面試都喜歡問原始碼,閱讀原始碼也有助於提升通過面試的概率。

結合今天的主題,一個很簡單的問題,在剛學習集合時,我們都使用過如下程式碼,但是下面幾行程式碼有區別嗎?

List list1 = new ArrayList();
List list2 = new ArrayList(0);
List list4 = new ArrayList(10);

有人可能會說,沒指定初始值就按預設值,指定了初始值就按指定的值構造一個陣列。真的是這樣嗎?如果你對上面這個問題有疑惑,就說明你該看看原始碼了。

學習程式設計的過程千萬不要人云亦云,一定要親自看看。

如何閱讀原始碼,每個人的方式不同,這裡僅以自己習慣的方式來說。以今天的主題為例,ArrayList是幹嘛的?怎麼用?這就延伸到一條路線,先看類名及其繼承體系——它是幹嘛的,再看建構函式——如何造一個物件,當然,建構函式會用到一些變數,所以在此之前我們需要先了解下用到的常量值和變數值,最後,我們需要了解常用的方法以及它們是如何實現的。

對於閱讀大多數類基本都是按照:類名——>變數——>建構函式——>常用方法。

本文只會選取有代表性的一些內容,不會講到每一行程式碼。

類簽名

好像沒有類簽名這個說法,這裡是對照函式簽名來說的,簡單說就是一個類的類名以及它實現了哪些介面,繼承了哪些類,以及一些泛型要求。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

從上述程式碼可以看出,ArrayList實現了:

Cloneable, Serializable介面,具有克隆(注意深度拷貝和淺拷貝的區別)和序列化的能力,

RandomAccess介面,具有隨機訪問的能力,這裡說的隨機主要是基於陣列實現的根據陣列索引獲取值,後期結合LinkedList分析更容易理解。

List介面,表明它是一個有序列表(注意,此處的有序指的是儲存時的順序和取出時的順序是一致的,不是說元素本身的排序),可以儲存重複元素。

AbstractList已經實現了List介面,AbstractList中已經實現了一些常見的通用操作,這樣在具體的實現類中通過繼承大大減少重複程式碼,需要的時候也可以重寫其中方法。

變數

    //序列化版本號
    private static final long serialVersionUID = 8683452581122892189L;

    //常量,預設容量為10
    private static final int DEFAULT_CAPACITY = 10;

   	//常量,初始化一個空的Object型別陣列
    private static final Object[] EMPTY_ELEMENTDATA = {};

	//常量,本質也是一個空的Object型別陣列,與EMPTY_ELEMENTDATA用於區別初始化時指定容量0還是預設不指定
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

	//變數,真正用來儲存元素的陣列名
    transient Object[] elementData; 

	//陣列中實際儲存的元素數量,未初始化則預設為0
    private int size;

上述變數中的大部分值都比較好理解,令人疑惑的事EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA,除了變數名,其他都一樣,好在註釋和後續的方法為我們說明了,簡單說,就是針對初始化時,不同的建構函式選用不同的變數名,即

List list1 = new ArrayList(); //此時用DEFAULTCAPACITY_EMPTY_ELEMENTDATA
List list2 = new ArrayList(0); //此時用EMPTY_ELEMENTDATA

為啥搞這麼麻煩,是大神們閒得慌嗎?顯然不是,不信?請繼續往下看。

構造方法


//不指定初始容量的建構函式
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

//指定初始容量的建構函式
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
    }
}

//通過已有集合直接構造
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

如上所示,ArrayList有三個建構函式:

不指定容量的情況下,此時直接構造一個空的陣列,只有當新增第一個元素時,才會擴容為預設容量10。所以說並不是我們經常理解的直接構造一個容量為10的陣列,到此時我們才理解為啥很多時候一些規範建議我們指定初始容量,因為這樣可以減少一次擴容操作。注意,此時使用的是DEFAULTCAPACITY_EMPTY_ELEMENTDATA

指定容量時,小於0拋異常,大於0直接用指定的值構造一個陣列,等於0時,也是構造一個空陣列,但是此時使用的是EMPTY_ELEMENTDATA

有啥區別呢?關鍵在與擴容時的操作。繼續往下看。

記住,ArrayList的擴容操作只可能發生在新增元素時。

常用方法

ArrayList的常用方法非常多,這裡先排除一大批私有方法和內部類,看一下外部方法(尷尬,差一點一張圖截不下):

看起來很多,這裡只選取幾個常用的,其他的可以類比著看。

add(E e)

第一個最常用的方法,新增元素(add)

public boolean add(E e) {
    //檢查陣列容量是否充足,不夠則擴容
    ensureCapacityInternal(size + 1);  
    //注意,下方程式碼相當於elementData[size] = e; size++;
    elementData[size++] = e;
    return true;
}

可以看出,在新增元素時,第一步先檢查陣列容量是否充足,不夠的話進行擴容,add方法的關鍵在於檢查容量

檢查容量:ensureCapacityInternal(int minCapacity)

//檢查容量是否足夠,不夠則擴容
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

//比較實際儲存元素+1與陣列的容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
	//若構造時不指定容量,則返回預設容量10或者現有實際元素+1中的最大值
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
	//構造時指定了容量,不管是0還是大於0,都返回實際容量+1
    return minCapacity;
}

//如果實際容量+1超過了現有容量(陣列裝不下了),則擴容
private void ensureExplicitCapacity(int minCapacity) {
    //記錄修改次數,主要是為了遍歷元素時發生修改則快速失敗,此處不談。
    modCount++;

    // 如果現有元素+1大於陣列實際長度,則進行擴容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

關鍵來了,如何擴容

擴容方法:grow(int minCapacity)

private void grow(int minCapacity) {
    // 舊容量為陣列長度
    int oldCapacity = elementData.length;
    //新容量為舊容量的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //新容量小於實際元素+1,則按實際元素+1擴容
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //新容量大於陣列最大長度,根據實際選擇容量為Integer.MAX_VALUE或者MAX_ARRAY_SIZE;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 將舊陣列元素複製到新陣列
    elementData = Arrays.copyOf(elementData, newCapacity);
}

上述程式碼有一個關鍵方法Arrays.copyOf(elementData, newCapacity)用來複制集合中的元素,此處不再深入。

回到開始的問題

在建立ArrayList時,

不指定初始容量,即

List list1 = new ArrayList();
//此時,構造一個空的陣列,第一次新增元素時,將陣列擴容為10,並新增元素。

指定初始容量為0,即

List list2 = new ArrayList(0);
//此時,也構造一個空陣列,但變數名和上面不一樣。第一次新增元素時,將陣列擴容為1,並新增元素。

指定初始容量為10,即

List list4 = new ArrayList(10);
//直接構造一個容量為10的陣列,第一次新增元素時,不擴容。

所以說,如果我們大概確定將要使用的元素數量,應當在建構函式中指明,這樣可以減少擴容次數,一定程度上提升效率。

小結

到目前為止,只是簡單寫了下ArrayList的建構函式和add方法,大部分內容都還沒有深入。想要把每一個方法都寫到,其實很難,也沒必要。

通過上面的內容,回顧自己閱讀原始碼的過程,既要“不求甚解”,更要“觀其大略”,對於一些核心的過程,我們需要仔細分析;但是對沒有經驗的新手來說,弄清楚每個細節很難,有些內容現階段可能還沒法理解,把握整體結構很重要,先搞清楚大概,再對每一個細節深入。如果一開始就對某一細節一直深入,很可能迷失其中自己都走不出來了。

看到這裡,你問我是不是對ArrayList完全瞭解了,哈哈,顯然沒有。但是,寫到這裡的時候,我的理解又深刻了不少。

心裡覺得大概懂了不一定是真的理解,只有抱著把內容寫出來讓別人看明白的心態,才有可能加深理解。不知,你看明白了沒?

相關文章