以CURD角度手撕ArrayList源碼

針對Java開發者而言,源碼分析是必備的技能,有這麼幾種境界

1. 看的懂源碼所實現的功能以及其邏輯

2. 看的懂源碼如何設計,如何架構

3. 能在源碼水平上進行擴展或者二次開發

鑑於筆者水平不足,經驗不夠,但還是希望能出一些源碼分析系列的文檔,希望藉助此係列文檔能夠更好地幫助新手快速的理清或者學習源代碼,達到第一個層次。因此筆者會盡量使用CURD操作的角度來分析JDK的源碼。是因爲多數程序員都是CURD階段,當然最熟悉的還是CURD操作,因此以CURD的角度來看JDK的源碼,相信會容易不少,畢竟萬物皆可CURD,萬物離不開CURD

注1:開發工具爲IntelliJ IDEA 2020.1.2 x64 

注2:  JDK版本爲JDK8  版本號爲 jdk1.8.0_191


talk is cheap,show me the code ---undefined


代碼段如下,有清晰明瞭的註釋

import java.util.ArrayList;
import java.util.List;

public class ArrayListTest  {

    public static void main(String[] args) {

        //無參構造函數new一個ArrayList
        List arraylist=new ArrayList();

        //ArrayList的add操作
        arraylist.add("hello world");
        arraylist.add("world hello");
        System.out.println("當前ArrayList的容量大小"+arraylist.size());

        //循環增長,查看動態grow函數
        for (int i=0;i<10;i++)
        {
            arraylist.add(i);
        }
        System.out.println("當前ArrayList的容量大小"+arraylist.size());

        //Arraylist的getter setter操作
        System.out.println("getter"+arraylist.get(3));
        System.out.println("setter"+arraylist.set(3,"huhu"));
        System.out.println("當前ArrayList的容量大小"+arraylist.size());

        //arraylist的addall操作
        arraylist.addAll(arraylist);
        System.out.println("當前ArrayList的容量大小"+arraylist.size());

        //arraylist的index操作
        System.out.println("這是索引"+arraylist.indexOf("world hello"));
        System.out.println("這是最後索引"+arraylist.lastIndexOf("world"));
        System.out.println("當前ArrayList的容量大小"+arraylist.size());
        System.out.println("是否包含有"+arraylist.contains("huhu"));
        System.out.println("是否包含所有"+arraylist.containsAll(arraylist));
        System.out.println("當前ArrayList的容量大小"+arraylist.size());

        //arraylist的remove操作
        System.out.println("刪除元素"+arraylist.remove("world hello"));
        System.out.println("刪除元素"+arraylist.remove(4));
        System.out.println("當前ArrayList的容量大小"+arraylist.size());

        //arraylist的retain操作
        arraylist.retainAll(arraylist);
        arraylist.removeAll(arraylist);
        System.out.println("當前ArrayList的容量大小"+arraylist.size());

    }


}

OK,下面我們試着斷點分析來看看ArrayList的源碼

到此步,請保留你的耐心,因爲看源碼是一個非常枯燥和非常花時間的過程,如果你覺得看不下去,可以休息一會兒,再繼續。畢竟看源碼,如同登山,當你在山頂時,相信看的風景會非常不一樣的。

1. 看new ArrayList發生了什麼

         //無參構造函數new一個ArrayList
        List arraylist=new ArrayList();

此行打斷點(ctrl+F8),啓用單步調試(F7)或者強制進入(Alt+Shift+F7)可以看到

        public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;         //無參構造函數初始化容量爲10的空數組,目前代碼只是初始化爲空數組,並沒有指定容量?什麼時候指定的容量?
    }

可以看見new ArrayList()調用了無參構造函數,在此構造函數中ArrayList將之前初始化過後的靜態變量  DEFAULTCAPACITY_EMPTY_ELEMENTDATA  (初始化構造了一個默認容量爲10的空數組列表)並將其引用賦值給了ArrayList的本地真實的存儲結構即elementData。

注3:無參僅僅初始化了一個空的數組列表,並沒有指定數組的大小 (此爲無參構造函數做的事)那麼是什麼時候指定了默認容量爲10吶?是在calculateCapacity方法中,當elementData爲默認容量的空數組列表時,返回一個10的值表示爲列表的容量大小(後文會敘述)

Q1:ArrayList有幾個構造函數?分別對應哪種情況?

Q2:爲什麼默認容量爲10?

2. 看 ArrayList的add操作發生了什麼

        //ArrayList的add操作
        arraylist.add("hello world");
        arraylist.add("world hello");
        System.out.println("當前ArrayList的容量大小"+arraylist.size());

        //循環增長,查看動態grow函數
        for (int i=0;i<10;i++)
        {
            arraylist.add(i);
        }
        System.out.println("當前ArrayList的容量大小"+arraylist.size());

在第一個add方法代碼行出打一個斷點,操作如同上文,在循環中的add又打一個斷點(是爲了方便查看ArrayList的動態擴容機制),啓用單步調試(F7)或者強制進入(Alt+Shift+F7)可以看到


    public boolean add(E e) {                           //此函數將指定的元素放在列表末尾(以追加的方式)
        ensureCapacityInternal(size + 1);  // Increments modCount!!     //以當前大小+1爲最小容量值爲參數來判斷所需要的容量大小
        elementData[size++] = e;
        return true;
    }


    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {         //當elementData是默認容量數組列表時,則返回默認容量大小10
            return Math.max(DEFAULT_CAPACITY, minCapacity);             //在這兒返回指定了elementData的默認容量大小10
        }
        return minCapacity;                                             //當elementData數組元素時候。則返回最小的容量大小
    }


    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;                                 //結構性變化+1

        // overflow-conscious code               //此代碼可能會造成溢出
        if (minCapacity - elementData.length > 0)              //如果最小容量大小都大於當前數組列表的大小則調用增長函數
            grow(minCapacity);
    }


    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);
    }

代碼比較複雜也比較多,所以需要耐心,不要慌,我們按照方法的調用順序來看看,add操作都發生了什麼?

add->ensureCapacityInternal->ensureExplicitCapacity->calculateCapacity

1. calculateCapacity是計算ArrayList的最小容量的

如果最開始是初始化的空數組列表,則返回10的初始化容量,如果不是,則返回代入的參數即最小容量值

注4: 在看此源碼的過程中,需要分清這麼幾個概念,通常概念不清,就看源碼比較困難

長度指的是ArrayList的長度,表示是ArrayList可以容納的元素多少個數

最小容量值指的是ArrayList在結構性變化的過程中所需要的容納空間

大小指的是ArrayList實際上所擁有的元素個數的大小即size方法所返回的size值,也即ArrayList中的size屬性

2. ensureExplicitCapacity是用來調用grow方法和針對modCount自增的

注5: modCount表示針對ArrayList的結構性變化的指針標識,如add,remove等

3. grow實現了ArrayList的動態擴容機制(重點)(面試常問)

在grow方法體內我們可以看到

舊容量爲原來的ArrayList的長度大小

而新容量則是舊容量的1.5倍

若新容量都小於所需要的最小容量,則將最小容量賦值給新容量

若新容量比極限容量要大,那麼就將極限容量和最大整型數當中的最大值賦值給新容量

完成以上的步驟後,就根據新容量來初始化一個新的數組,將原來的elementData的所有元素都等位的複製到新的數組當中,然後將新的數組引用賦值給elementData,這樣就完成了ArrayList的動態擴容機制

Q3:ArrayList的動態擴容機制是怎麼回事?

Q4:什麼時候會觸發ArrayList的擴容?

Q5:爲什麼新容量會設置成舊容量的1.5倍?爲什麼會採取位運算而不是簡單的乘除運算?

Q6:若需要的最小容量都達到了極限容量值,是否還需要擴容?是否會報異常?

Q7: grow方法會被哪些方法給調用?哪些是顯式調用?哪些是隱式調用?

Q8:ArrayList的擴容機制的時間複雜和空間複雜度是?頻繁的動態擴容會導致什麼樣的問題?

4. 經過了以上的動態擴容之後

elementData就是新容量的數組列表了,然後執行了size++,並將這個位置上的元素填上所傳入的參數值。

注6: add操作是在數組列表末尾直接追加元素,所以才使用了size++,並元素賦值

5. 成功之後,返回真值

3. 看ArrayList的getter setter操作發生了什麼

           //Arraylist的getter setter操作
        System.out.println("getter"+arraylist.get(3));
        System.out.println("setter"+arraylist.set(3,"huhu"));
        System.out.println("當前ArrayList的容量大小"+arraylist.size());

按照上面的方法同樣的打斷點進入調試,可以看見

    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

    
    public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }


    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }



    // Positional Access Operations

    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }

此源碼可以分3部分看,一部分是getter setter操作,一部分是rangeCheck,一部分是elementData

先看getter,可以看到

1. 先調用了rangecheck

步入rangeCheck,發現是針對傳入的索引值進行合法性檢查,若傳入的索引值大於或者等ArrayList的大小,就拋出了 IndexOutOfBoundsException 說明了是索引溢出邊界異常,在上面的註釋可以發現,若索引值是負數的,就拋出 ArrayIndexOutOfBoundsException 說明了是數組索引溢出邊界異常

2. 若傳入索引值合法

則調用elementData方法,返回該索引位置上的元素

Q9:爲什麼會有兩個索引異常?一個是 IndexOutOfBoundsException 另外一個是 ArrayIndexOutOfBoundsException? 是針對什麼情況呢?針對索引值的檢查?如此代碼就足夠了嗎?

Q10:爲什麼要編寫一個elementData方法?通過這個方法來返回元素?而不是直接使用 e=elementData(index)?

再看setter,可以看到

1. 調用rangeCheck檢查索引合法性

2. 將索引值上的元素作爲舊元素返回

3. 傳入的新值賦值給索引值上的元素

此getter setter方法都比較簡單,就不多說了

4. 看addAll操作發生了什麼

           //arraylist的addall操作
        arraylist.addAll(arraylist);
        System.out.println("當前ArrayList的容量大小"+arraylist.size());

同上一樣的進入調試,可以發現


    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }


1. 將傳入的集合通過toArray方法轉爲object的數組對象列表

2. 調用ensureCapacityInternal方法來計算當前size+集合中的元素大小是否能夠有容納空間?如果有,就繼續,如果沒有就需要調用grow方法進行擴容,直到了有容納空間爲止(grow的具體操作可以參考上文)

3. 調用System.arraycopy方法完成一次數組之間的複製,將集合中的元素追加複製到elementData中

4. size+numNew完成size大小的更新

5. 返回boolean值

5. 看index操作和contain操作都發生了什麼

            //arraylist的index操作
        System.out.println("這是索引"+arraylist.indexOf("world hello"));
        System.out.println("這是最後索引"+arraylist.lastIndexOf("world"));
        System.out.println("當前ArrayList的容量大小"+arraylist.size());
        System.out.println("是否包含有"+arraylist.contains("huhu"));
        System.out.println("是否包含所有"+arraylist.containsAll(arraylist));
        System.out.println("當前ArrayList的容量大小"+arraylist.size());

同上一樣的進入調試,可以發現


    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }



    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }



    public int lastIndexOf(Object o) {
        if (o == null) {
            for (int i = size-1; i >= 0; i--)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = size-1; i >= 0; i--)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

先看indexOf,可以看出

1. 對傳入的object對象引用判空

2. 如果是空,就for循環來獲取整個ArrayList中第一次出現null的值,如果有,就返回該處索引值,如果循環完了,都沒有,就返回-1

3. 如果object不是空,一樣的for循環遍歷整個ArrayList,查找第一次出現該值相同的位置,並返回該值所處的索引值,若遍歷完了,都沒有,就返回-1

再看lastindexOf,可以看出

實現的邏輯是跟indexOf是差不多的,都是遍歷整個ArrayList來查找object,唯一不同是,遍歷是倒序遍歷,返回自然是最後一次出現該值的索引

再看contain,也可以發現

調用了indexOf,只不過是是通過indexOf來查找該元素的索引值,然後比對索引值是否大於等於0,若是就返回真,否則就是假,表示該元素在整個ArrayList中不存在

Q11:爲什麼需要對傳入的object判空?空值和非空值對判斷查找是否有區別?

6. 看remove操作發生了什麼


        //arraylist的remove操作
        System.out.println("刪除元素"+arraylist.remove("world hello"));
        System.out.println("刪除元素"+arraylist.remove(4));
        System.out.println("當前ArrayList的容量大小"+arraylist.size());

同上一樣,調試進入源代碼,可以看出


    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

   
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }




    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }


remove這個操作有兩個重載函數,一個是根據索引來remove,一個是根據對象來remove

先看根據索引來remove的代碼

1. 檢驗索引是否合法

2. 結構性標識變化加1

3. 計算需要開始移動的索引位置

4. 如果索引位置大於0,就調用System.arraycopy從需要移動的索引位置開始從後往前覆蓋複製

5. elementData的最後一個元素位置設置爲null

6. 返回舊值

Q12:爲什麼要對elementData最後一個末尾元素設置爲null?註釋解釋爲了讓gc能工作,那麼僅僅是爲了讓gc工作麼?gc又是什麼時候發生gc的呢?若對不需要的元素引用設置爲null,是否能馬上gc?

再看根據object來remove的代碼

1. 很顯然首先需要判斷object是否爲null

2. 根據是否爲null值,分兩種方法,循環遍歷查找對應的值

3. 調用fastremove方法

4. fastremove方法中的代碼跟上文的remove(int index)步驟下2到5一致,也就是2到5封裝成了一個fastremove方法

Q13:爲什麼步驟2到5封裝成了一個fastremove方法?是否能快速刪除?

7. 看retainAll和removeAll發生了什麼

        //arraylist的retain操作
        arraylist.retainAll(arraylist);
        arraylist.removeAll(arraylist);
        System.out.println("當前ArrayList的容量大小"+arraylist.size());

同上一樣,調試進入源代碼,可以發現

  
    public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    }



    public boolean retainAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, true);
    }


    private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData;
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }

1. 調用object.requireNonNull檢查元素object是否爲空

2. 不管是retainAll還是removeAll都調用了batchRemove方法,只不過傳入參數一個正一個負

3. 來看看batchRemove究竟發生了什麼

4. 可以看到batchRemove的大概邏輯是這樣的

4.1 將自身的elementData做爲一個緩衝區(可修改),設置r、w的讀寫指針,設置修改標誌變量

4.2 循環遍歷ArrayList列表元素,看集合c是否包含ArrayList集合元素

在retainall中,如果包含,就在elementData上w位置替換成r位置上的元素,然後w自增(實際上就是把包含的元素按照原順序在elementData中保留下來)

在removeall中,如果不包含,就在elementData上w位置替換成r位置上的元素,然後w也自增(實際上把不包含的元素按照原順序在elementData上保留下來)

注7:修改都是在elementData上修改的,任何改動都會修改到ArrayList實際上的數據元素

4.3 如果r不等於size,則通過System.arraycopy來複制完成elementData的元素修改

4.4 如果w不等於size,則將elementData中不需要的元素循環置空,方便gc回收

5. 返回成功與否標誌

Q14:爲什麼會有r!=size,w!=size的判斷?

Q15:爲什麼會直接在elementData上修改,而不另外單獨設立一個可供修改的緩衝區數組?

8. 其他常見的功能函數分析

8.1 size函數


    public int size() {
        return size;
    }

非常簡單,就是返回size的數值,size表示就是ArrayList的所擁有元素的大小

注8:size並非等於elementData的長度,也即size!=length,通常長度要比大小要大

8.2 isEmpty函數


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

也是非常簡單,常用的函數,表示ArrayList是否是空列表,如果是返回真值,若不是,返回假值

8.3 toArray函數


    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

    
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

將集合元素轉換成爲數組對象的函數,一個是無參的轉換函數,一個是有參的轉換函數,其實質都是調用了Array.copyOf的複製函數

8.4 clear函數

 
    public void clear() {
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

將集合中所有元素置空,然後讓gc回收的函數表達

9. ArrayList的迭代器分析

注9:針對以上提出的問題,筆者會單獨開闢一個專欄,是針對現在面試官經常問的問題以及我個人在學習源碼過程中所能想到一些思考問題,會進行一定的解答。碼字不容易,請讀者能點個贊,評論一下,便是足以了。

 

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