【集合】手寫ArrayList集合與源碼分析

背景:首先集合大家是非常熟悉的,不管是個人日常編碼還是公司項目,都是經常打交道的好朋友。但是我們要摸清楚她的底細,畢竟好朋友就是要知根知底~  

 那麼來認識認識集合中的List集合,List集合中的ArrayList集合,四捨五入就是集合中的集合。

首先,簡單陳述ArrayList集合特性,底層使用動態數組實現,隨機查詢效率非常快(元素下標),但插入和刪除需要移動整個數組、效率低,ArrayList不是線程安全的、 Vector是線程安全的集合。ArrayList時間複雜度低,佔用內存較多,順序是連接的,空間複雜度高。

然後我們開始源碼分析,以JDK1.8爲演示,這裏插一句,雖然JDK都是1.8版本的,但是如果後綴版本不一樣,源碼是會有大同小異的,我曾看過兩個小版本不同的1.8集合源碼中有一個是提取了公共方法,有一個是沒有提取的,當然是不會有什麼其他影響。

序:案例源碼碼雲地址:

地址:https://gitee.com/yiang-hz/gather

PS:這裏着重於實現思路與流程,建議對着源碼看分析。

一、定義List集合接口

這裏定義常用的四個方法  增、刪、查與大小。這裏貼代碼就不貼註釋了,註釋在源碼中很詳細。

public interface YiangList<E> {
    int size();
    E get(int index);
    boolean add(E e);
    E remove(int index);
}

二、ArrayList屬性變量解析

我們來說說ArrayList實現類的一些變量以及它的作用。

1.(數據容器)elementData

list是由數組實現的,那麼需要有數組來存儲數據值。故elementData爲數據容器(數組容器)。默認爲空。

2.(默認容量)DEFAULTCAPACITY_EMPTY_ELEMENTDATA

數組容器需要一個容量大小,那麼該屬性即爲初始大小。數組容器默認爲空,並且在構造函數中加載該變量。賦予容器初始默認大小。

3.(默認初始容量)DEFAULT_CAPACITY

由於ArrayList默認是在添加值的時候纔會觸發擴容機制,所以在擴容是需要有一個默認初始容量,那麼該值就是在List添加值擴容時進行賦值給數組。

4.(默認最大值)MAX_ARRAY_SIZE

我們要知道不管什麼容器,都有它最大的負載量,ArrayList也不例外。她的最大值爲Integer的最大值 - 8。在Integer源碼中有這麼一句話 

/**
     * A constant holding the maximum value an {@code int} can
     * have, 2<sup>31</sup>-1.
     */
public static final int   MAX_VALUE = 0x7fffffff;

那麼究竟是多少呢?答案爲2的31次冪,也就是 2 147 483 648,是嗎?不是,錯了。

在這源碼上有段註釋,註釋大致告訴你是 2^31 - 1 所以是  2 147 483 647。

5.(默認數據大小值)size

該值就比較明顯了,是list.size()方法獲取時返回的count數,count數的疊加是在add方法中進行累加的。也就是說每調用一次add()方法,就會進行一次 size++。爲數組容器中數據量的總和

6.(線程安全控制)modcount 

由於ArrayList是非線程安全的,所以在使用迭代器遍歷的時候,用來檢查列表中的元素是否發生結構性變化(列表元素數量發生改變)了,主要在多線程環境下需要使用,防止一個線程正在迭代遍歷,另一個線程修改了這個列表的結構。

相關異常:ConcurrentModificationException。(併發修改異常)

通俗點說,就是迭代的時候,檢查防止另外一個線程改變集合中元素。所以從這裏就能體現爲什麼遍歷很多人吹集合建議用迭代器的原因之一了。

實現代碼塊:

public class YiangArrayList<E> implements YiangList<E> {

    /**
     * 默認初始容量
     */
    private static final int DEFAULT_CAPACITY = 10;

    /** 默認最大值 Integer最大值 -8 (HZ)*/
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * 存放集合所有的數據,transient表示不能被序列化。
     */
    transient Object[] elementData;

    protected transient int modCount = 0;

    /**
     * 數組容量默認爲空
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 數組大小默認爲0
     */
    private int size;

    public YiangArrayList(){
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /** 返回集合當前大小 (HZ)*/
    @Override
    public int size() {
        return size;
    }

    /**
     * 通過強制轉換E,返回對應對象,使用@SuppressWarnings註解避免強轉警告,
     * @param index 索引
     * @return 元素
     */
    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }

    /**
     * 獲取數組中元素
     * @param index 索引
     * @return 元素
     */
    @Override
    public E get(int index) {
        //調用返回方法
        return elementData(index);
    }

    @Override
    public boolean add(E e) {
        //1.數組擴容
        ensureCapacityInternal(size + 1);
        //2.數組變量賦值
        elementData[size++] = e;
        return true;
    }

    /**
     *  刪除方法
     * <p>
     *     System.arrayCopy解析
     * @see  com.yiang.MyList#testArrayCopy()
     * </p>
     * @param index 元素對象下標地址
     * @return 被刪除的對象
     */
    @Override
    public E remove(int index) {
        //監測下標是否越界
        rangeCheck(index);

        modCount++;
        //此處獲取被刪除的數據返回出去
        E oldValue = elementData(index);

        //計算移動位置 集合大小 - 移動的下標位置 - 1
        int numMoved = size - index - 1;
        //如果移動的位置>0,也就是代表移除的不是最後一位的情況下,進行移動覆蓋算法。
        if (numMoved > 0) {
            System.arraycopy(elementData, index + 1, elementData, index,
                    numMoved);
        }
        //置空元素最後一位的同時將size減去1,這裏真的很棒。
        // 至於清除這裏源碼英文註釋翻譯是 “清除,讓GC做它的工作”
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

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

    private String outOfBoundsMsg(int index) {
        return "Index: " + index + ", Size: " + size;
    }

    /**
     * 判斷數組是否爲初始情況
     * 如果數組擴容值小於默認值10則返回默認值,如果大於默認值,那則返回該值
     * @param elementData 數組
     * @param minCapacity 最小容量 = 數組大小 N + 擴容的值 1
     * @return 10 > N ? 10 : N 初始返回爲10
     */
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        //如果擴容時,數組爲空那麼
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //max爲三元運算,如果初始容量大於當前容量,那麼賦值爲初始容量,如果小於則以當前容量計算
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //返回容量
        return minCapacity;
    }

    /**
     * 對計算是否擴容方法進行調用,同時判斷是否爲初始情況
     * {@code minCapacity} 初始值爲1
     * @param minCapacity 最小容量 = 數組大小 N + 擴容的值 1
     */
    private void ensureCapacityInternal(int minCapacity) {
        //調用計算是否擴容方法
        ensureExplicitCapacity(
                //抽取出來的公共方法,源碼有所變化。
                calculateCapacity(elementData, minCapacity)
        );
    }

    /**
     * 判斷數組是否需要進行擴容
     * @param minCapacity 容量值
     */
    private void ensureExplicitCapacity(int minCapacity) {
        //控制線程安全之類的。
        modCount++;

        // overflow-conscious code
        //如果容量值大於數組長度,那麼進行grow擴容方法。 此處初始默認情況爲:10 - 0 > 0
        if (minCapacity - elementData.length > 0) {
            grow(minCapacity);
        }
    }

    /**
     * 數組擴容方法
     * @param minCapacity 容量值 初始爲10
     */
    private void grow(int minCapacity) {
        // 當前數組長度,原來的容量  默認爲0
        int oldCapacity = elementData.length;
        //新的長度 = 舊的長度 + 二進制算法(舊長度除以2) 也就是原有長度 + 原有長度的二分之一
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //初始情況爲 0 + 0/2  - 10  所以 新長度會默認等於傳遞的長度10
        if (newCapacity - minCapacity < 0) {
            //作用:第一次對數組初始化容量操作
            newCapacity = minCapacity;
        }
        //判斷最大值 MAX_ARRAY_SIZE = Integer最大值-8
        //目的是爲了做個限制,實際上是用不到的
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            newCapacity = hugeCapacity(minCapacity);
        }
        // minCapacity is usually close to size, so this is a win:
        //開始對我們的數組進行擴容
        //複製當前數組,並將複製後的數組的長度設置爲  newCapacity 第一次擴容 10 -> 15
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    /**
     * 如果超過了默認設置的集合最大值,那麼就使用Integer最大值
     * @param minCapacity 最小值
     * @return 集合最大值或者Integer最大值
     */
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) { // overflow
            throw new OutOfMemoryError();
        }
        return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
    }

}

三、ArrayList重點方法解析

上面說完了變量值後,相信大家都飢渴難耐的想要了解類的方法實現原理了。馬上來揭祕她!

3.1、獲取集合大小 list.size()

返回當前集合的大小,也就是將屬性值size返回。由於size在add方法中會進行size++操作,所以size與數組中實際元素的大小是一致的。注意size是數組容器內實際元素數量,而不是數組容器的大小。 

3.2、獲取集合元素 list.get(int index)

獲取集合元素,實際上就是通過下標,在數組容器中找到對應的數據返回即可。

3.3、集合添加元素 list.add(Object o)

添加元素中涉及到的知識點就比較多了。先擴容、再賦值。

1.擴容:PS:擴容由於涉及內容太多,故將其作爲第四大點放在下文。

2.賦值:代碼:

elementData[size++] = e;

不得不說這裏很巧妙。打個比方現在的容器大小爲5,已有數據量size爲3,那麼在[]內計算size++就是 3+1 = 4。那麼得到新添加的元素下標爲4,同時size也進行了累加。在使用數組賦值,將該下標賦值爲新添加的元素e。一句代碼實現了,size累加。下標定位。數據賦值。

 

3.4、爲什麼ArrayList刪除元素要慢? list.remove(int index)

首先在刪除下標時,會通過監測方法rangeCheck(int index),監測下標是否越界,原理就是判斷index是否大於等於size。

由於下標是 0開始,size是1開始,故判斷是判斷大於等於,而不僅僅是大於。

其次是刪除時調用的方法,數組複製算法。(慢的主要原因)

src:源數組;

srcPos:源數組要複製的起始位置;

dest:目的數組;

destPos:目的數組放置的起始位置;

length:複製的長度。

System.arraycopy(elementData, index + 1, elementData, index,
                    numMoved);

這裏畫圖說說它實現的原理 。複製時是將 1234  如果是刪除 1 那麼就是將 234往前複製,然後就變成了 2344。

其次源碼裏最後會將數組最後一位數賦值爲NULL完成刪除。

如果是刪除2,複製數組結果就是 1344,然後把最後一位賦值爲NULL。得出134。

如果是刪除3,複製數組結果就是1244,然後把最後一位賦值爲NULL。得出124。

綜合以上,我們就能知道爲什麼ArrayList在刪除時會要慢,因爲每次刪除都需要重新複製一遍數組容器的數據進行覆蓋。

四、ArrayList擴容機制

4.1、擴容判斷機制方法 ensureCapacityInternal

擴容機制調用方法:ensureCapacityInternal。

/**
     * 對計算是否擴容方法進行調用,同時判斷是否爲初始情況
     * {@code minCapacity} 初始值爲1
     * @param minCapacity 最小容量 = 數組大小 N + 擴容的值 1
     */
    private void ensureCapacityInternal(int minCapacity) {
        //調用計算是否擴容方法
        ensureExplicitCapacity(
                //抽取出來的公共方法,源碼有所變化。
                calculateCapacity(elementData, minCapacity)
        );
    }

首先它會調用ensureExplicitCapacity方法來進行判斷是否需要進行擴容。如果容量值大於數組長度,那麼進行擴容。

參數組成爲:當前數組大小 N + 擴容的值 X 這裏填的1。

/**
     * 判斷數組是否需要進行擴容
     * @param minCapacity 容量值
     */
    private void ensureExplicitCapacity(int minCapacity) {
        //控制線程安全之類的。
        modCount++;

        // overflow-conscious code
        //如果容量值大於數組長度,那麼進行grow擴容方法。 此處初始默認情況爲:10 - 0 > 0
        if (minCapacity - elementData.length > 0) {
            grow(minCapacity);
        }
    }

判斷數組是否需要擴容方法的參數容量值爲經過 calculateCapacity(elementData, minCapacity)方法判斷數據後得出的值。

那麼calculateCapacity方法實際上就是通過判斷數組是否爲空(是否第一次擴容),如果爲空則判斷擴容的值和默認容量10,擴容容量當前爲1,那麼返回的值就是10。如果不爲空,那麼就證明不是第一次擴容,直接返回數組當前容量。

/**
     * 判斷數組是否爲初始情況
     * 如果數組擴容值小於默認值10則返回默認值,如果大於默認值,那則返回該值
     * @param elementData 數組
     * @param minCapacity 最小容量 = 數組大小 N + 擴容的值 1
     * @return 10 > N ? 10 : N 初始返回爲10
     */
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        //如果擴容時,數組爲空那麼
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //max爲三元運算,如果初始容量大於當前容量,那麼賦值爲初始容量,如果小於則以當前容量計算
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //返回容量
        return minCapacity;
    }

故這裏傳遞給判斷擴容方法(ensureExplicitCapacity)的值。若是第一次擴容則爲默認值10,若是第二次擴容則爲數組當前容量也就是第一次擴容後的10。如果目前是無需擴容,那麼直接返回。結束方法。如果是需要擴容,那麼進入grow方法,開始擴容機制。

4.2、擴容方法grow

 這裏傳入的值有兩種情況,第一種是第一次擴容,默認爲初始值DEFAULT_CAPACITY也就是10。

第二次擴容則是數組當前的容量大小。也就是第一次擴容後的10

首先它會獲取原來的長度oldCapacity,然後新的長度等於 原來長度的1.5倍,源碼採用二進制計算,總所周知二進制運算是速度更快的。所以第二次擴容後數組大小爲15。

然後判斷數組大小是否超過設定最大值,如果超過了則默認爲最大值。

最後通過Arrays提供的copyOf方法指定需要複製的數組與長度,再將原來的數組覆蓋完成擴容。

/**
     * 數組擴容方法
     * @param minCapacity 容量值 初始爲10
     */
    private void grow(int minCapacity) {
        // 當前數組長度,原來的容量  默認爲0
        int oldCapacity = elementData.length;
        //新的長度 = 舊的長度 + 二進制算法(舊長度除以2) 也就是原有長度 + 原有長度的二分之一
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //初始情況爲 0 + 0/2  - 10  所以 新長度會默認等於傳遞的長度10
        if (newCapacity - minCapacity < 0) {
            //作用:第一次對數組初始化容量操作
            newCapacity = minCapacity;
        }
        //判斷最大值 MAX_ARRAY_SIZE = Integer最大值-8
        //目的是爲了做個限制,實際上是用不到的
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            newCapacity = hugeCapacity(minCapacity);
        }
        // minCapacity is usually close to size, so this is a win:
        //開始對我們的數組進行擴容
        //複製當前數組,並將複製後的數組的長度設置爲  newCapacity 第一次擴容 10 -> 15
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

以上即是擴容機制。

擴容機制總結:

默認初始化容量爲0,首次添加默認擴容爲10,可設置值計算公式爲三元 N > 10 ? N : 10。最大容量爲 [2的32次冪-1] 2^32-1

擴容會進行細節判斷,區分old與new兩個大小。計算公式爲 old + old/2。也就是常說的1.5倍

通過Arrays.copyOf來進行最後的數組擴張。


本文總結:

回顧文章開篇說的List特性:

底層使用動態數組實現,隨機查詢效率非常快(元素下標),但插入和刪除需要移動整個數組、效率低,ArrayList不是線程安全的、 Vector是線程安全的集合。ArrayList時間複雜度低,佔用內存較多,順序是連接的,空間複雜度高。

論證:

1.擴容機制?底層是包含數組,默認擴容大小爲10,擴容倍數爲 1.5倍,首次擴容大小與擴容倍數機制均可自定義。

2.爲什麼慢?由於增加需要擴容,刪除需要複製數組,打亂數組原有順序,所以導致整個數組出現複製操作,空間複雜度高,由此效率變低,故效率低。

3.爲什麼Vector就安全了?源碼加了個鎖

4.時間複雜度低是什麼?爲什麼查詢快?查詢快是因爲底層默認使用數組,查詢元素只需要通過下標找到對應元素即可,但維持快速度查詢的是需要保證順序絕對性,故佔用內存,所以空間複雜度高。

備註:拉取代碼通過運行test方法即可調試擴容信息。


後記:目前也是在複習集合知識,通過博客來將知識梳理供大家學習,也提升自己的印象。

如有不足之處,歡迎指正,如有共同維護該集合項目或分享更多學習知識想法,可以加我好友聯繫。

 

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