Java入門系列之集合ArrayList源碼分析(七)

前言

上一節咱們經過排隊類實現了相似ArrayList基本功能,固然還有不少欠缺考慮,只是爲了咱們學習集合而準備來着,本節咱們來看看ArrayList源碼中對於經常使用操做方法是如何進行的,請往下看。html

ArrayList源碼分析

上一節內容(傳送門《http://www.javashuo.com/article/p-zgvtuxem-kh.html》)咱們在控制檯實例化以下一個ArrayList,並添加一條數據,以下java

  ArrayList<Integer> list = new ArrayList<>();
  list.add(1);

初始化容量分析

首先實例化了ArrayList集合,上一節咱們寫了一個排隊類的基本操做,最終咱們經過優化,將數組容量放在構造函數中進行,若未給定數組容量則默認給定一個容量,接下來咱們來看看源碼中初始化了一個集合到底提早作了哪些準備工做呢?數組

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() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
.....

在咱們初始化集合且類型爲基本數據類型時,會有如上兩個函數,一個是默認的構造函數,一個是帶參數的構造函數。由於給出的例子並未包含參數,因此則是走下面一個構造函數,咱們再來看看ArrayList中定義的變量,以下:函數

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

    //數組空實例
    private static final Object[] EMPTY_ELEMENTDATA = {};

    //默認空數組實例
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    //被操做數組
    transient Object[] elementData; 

    //數組大小
    private int size;

    //數組最大容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

如上咱們未給定容量時,則初始化一個空數組實例,若咱們給定了容量,則走如上第一個構造函數,若是容量大於0則數組容量則爲咱們給定的容量,若是等於0則爲空數組實例,不然拋出容量非法。接下來到了第二步,當咱們添加元素2時,看看添加方法是如何操做的。源碼分析

添加元素分析

//添加元素實現
public boolean add(E e) {
        ensureCapacityInternal(size + 1);
        elementData[size++] = e;
        return true;
}

咱們繼續看看ensureCapacityInternal(size + 1)方法,此方法用來計算數組容量,看看最終方法實現,以下:學習

private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
    //計算容量
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        //當實例化集合時未給定數組容量或者指定容量爲0時,則此時數組爲空數組實例
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //此時minCapacity爲1,經過Math.max函數將minCapacity和DEFAULT_CAPACITY(默認容量)比較返回【10】
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //當實例化時給定數組容量大於0,則直接返回添加一個元素後的容量即(size+1)
        return minCapacity;
    }
    //判斷是否擴容
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 若計算事後的數組容量大於數組存儲長度時則擴容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
//擴容核心實現
private void grow(int minCapacity) {

        //被操做數組實際容量
        int oldCapacity = elementData.length;
        
        //新容量 = (實際容量 + 實際容量/2並去模)即1.5倍舊容量
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        
        //若新容量小於數組大小則以數組大小爲新容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        
        //若新容量大於定義的最大數組大小
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
            
        //擴容後的新數組
        elementData = Arrays.copyOf(elementData, newCapacity);
}


//計算數組最大容量
private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        
        //若數組大小大於定義的最大數組大小則新容量最大爲整數最大值,不然爲定義的最大數組大小        
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
}

如上紅色標記的是自動擴容定義的數組最大容量,這裏須要解釋下oldCapacity >> 1是啥意思,學校所學都還給了老師,查了資料才搞懂,這裏也作個備忘錄。>>在計算機表示右移,大部分狀況下咱們使用這種運算符比較少,可是這裏爲什麼不直接乘除呢?並且咱們還看的懂些,使用左右移,運算速度快,直接乘除須要cpu計算消耗內存。剛一開始看到這個我是懵逼的,其實很簡單。好比32,咱們有2進製表示則爲100000,怎麼計算來的呢,以下:優化

(1 * 2 ^ 5)+(0 * 2 ^ 4)+(0 * 2 ^ 3)+(0 * 2 ^ 2)+(0 * 2 ^ 1)+(0 * 2 ^ 0)= 32 + 0 + 0 + 0 + 0 + 0 = 32。

 

好了咱們知道32表示爲二進制則是【100000】,那麼32>>1則表示將十進制32轉換爲二進制後總體向右移動一位,將左邊空餘的補0,右邊多餘的剔除,若是是左移則相反(這裏需注意int爲32位,可是數字沒那麼大,因此左側確定所有爲0,這裏咱們省略了哦),以下:this

因此32>>1向右移動一位後如圖,那麼計算結果和上述第一張圖同樣,以下:spa

(0 * 2 ^ 5)+(1 * 2 ^ 4)+(0 * 2 ^ 3)+(0 * 2 ^ 2)+(0 * 2 ^ 1)+(0 * 2 ^ 0)= 0 + 16 + 0 + 0 + 0 + 0 = 16。

爲了驗證上述結果,咱們經過代碼來打印看看是否正確,以下:code

 System.out.println( 32 >> 1);

經過如上圖咱們很容易得出結論:若是是右移即>>,那麼用原數據除以2的位數次冪並捨去模,若是是左移即<<,那麼用原數據乘以2的位數次冪。好比11>>2,經過11除以2^2,立馬得出結果爲2。如果11<<2,則是11*2^2,結果將是44。分析源碼到這裏爲止,咱們可得出以下結論:

若未給定初始化容量,則默認初始化容量爲10且初始化默認容量的時機是在進行添加操做時。

自動擴容大小爲1.5倍原始容量。

容量最大爲Integer.MAX_VALUE即2147483647。

添加指定索引元素分析

上述咱們只是分析完了初始化集合實例以及添加元素,接下來咱們在指定索引位置添加元素看看,以下:

public static void main(String[] args) {

        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);

        //添加元素2到索引5
        list.add(5,2);
}

依據上述咱們所分析,由於在初始化集合時咱們並未指定容量,因此當咱們添加元素時,此時集合的容量默認爲10,接下來咱們在索引爲5的位置添加元素2,那麼是否是就能夠呢?

咱們覺得默認容量爲10,在指定索引爲5插入元素不會有問題,可是結果倒是拋出了異常,這說明不是以數組默認容量或提供的初始容量來做爲判斷依據,而是以數組實際大小來進行判斷,爲了證實咱們的觀點,咱們來分析在指定索引位置插入元素的方法,以下:

//添加指定索引元素
public void add(int index, E element) {

        //檢查索引範圍,確認是否添加
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
}
private void rangeCheckForAdd(int index) {

        //要添加的元素索引不能大於數組實際大小或小於0,不然拋出異常
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

有的人可能就問了,分析源碼有什麼意義或做用嗎?做用太多了,一是瞭解背後本質原理不會出現自認爲所謂的「坑」,二是經過學習並寫出高質量的代碼,三其餘等等。咱們有了對原理的瞭解,接下來咱們就來作一個題目,以下:

public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        list.add(6);
        list.add(7);
        list.add(8);
        list.add(9);
        list.add(10);
        list.add(6,2);
}

由於咱們知道默認初始化容量爲10,因此當添加元素到11時即上述在索引6的位置插入元素2,此時將自動擴容且容量大小爲15(若是仍是不懂,建議再重頭複習下本篇文章)。接下來咱們再來分析分析trimToSize方法。

trimToSize分析

首先咱們來看以下一段代碼:

public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>(20);
        list.add(1);
        list.add(2);

        list.add(6,2);
        list.trimToSize();
}

如上咱們提供初始化容量爲20,可是呢結果咱們實際僅僅只添加了三個元素,在數組中剩餘17個元素卻佔着坑,因此這個時候爲了解決這樣的問題就引入了trimToSize方法,旨在解決以下三個問題

將集合縮減到當前集合實際存儲大小

最小化集合實例的存儲

當咱們須要縮減集合並最小化存儲時

public void trimToSize() {
        modCount++;
        //若數組實際大小小於數組容量時
        if (size < elementData.length) {
            //若數組實際大小爲0時則數組爲空實例,不然複製數組到當前數組大小
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
}

remove分析

在java中能夠針對指定元素所在索引位置刪除,也能夠直接刪除元素,下面咱們首先來看看根據索引刪除元素,以下:

//刪除指定索引元素並返回刪除元素值
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; 

        return oldValue;
}

若咱們要直接元素,好比刪除上述添加的元素2,此時針對刪除方法尤爲重載,其參數是對象,因此咱們須要將元素2轉換爲包裝類,好比以下:

//刪除指定元素
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;
}

其實咱們看到源碼中不少操做方法內部都是採用複製的方法來進行,好比刪除、添加集合等等,同時咱們注意到在涉及到複製時都會存在好比上述設置爲空的狀況,下面咱們來稍微研究下這麼作的意義在哪裏?

public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        list.add(6);

        Integer[] array = list.toArray(new Integer[0]);

        System.arraycopy(array, 3, array, 2,
                3);

        for (int i = 0; i < array.length; i++) {
            System.out.println(array[i]);
        }
}

如上咱們經過調用系統提供的複製方法模擬刪除,咱們刪除數組中爲3的元素,而後打印數組中元素,以下:

根據調用複製來看,複製的起始位置爲索引3,而後將數組中元素四、五、6進行復制,可是將原有數組中的元素三、四、5進行了覆蓋,可是此時元素6沒有元素覆蓋,因此數組中依然有6個元素,因此爲了GC,咱們須要將元素6設置爲空,而且長度設置爲5,這樣纔是最優代碼,一樣也就達到了在刪除元素時elementData[--size] = null同等效果。

總結 

本節咱們詳細分析了ArrayList源碼,ArrayList的本質上是經過動態擴容一維數組來實現,同時介紹了比較經常使用的幾個方法,固然還有好比java 8中出現的經過lambda表達式進行遍歷沒有再詳細去一一解釋,後續在學習或作項目時用到了發現有須要補充的地方,我會回過頭來再進行研究,暫且到這裏爲止,下節咱們繼續學習其餘集合並分析源碼,感謝您的閱讀,下節見。

相關文章
相關標籤/搜索