ArrayList 原始碼分析 -- 擴容問題及序列化問題

niaonao發表於2018-08-17

 目錄

一、前言
二、ArrayList 的繼承與實現關係
    2.1 ArrayList.java
    2.2 抽象類AbstractList.java
    2.3 介面List.java
    2.4 介面RandomAccess.java
    2.5 介面Cloneable
    2.6 介面Serializable
三、ArrayList 關於陣列和集合的討論
    3.1 ArrayList 是陣列還是集合問題說明
    3.2 從構造方法分析ArrayList
    3.1 確認ArrayList 是集合
四、ArrayList 初始容量是0 還是10 問題的確認
    4.1 從構造方法看初始容量
    4.2 從add() 方法看初始容量
    4.3 確定ArrayList 的初始容量
五、ArrayList 的擴容問題探索
    5.1 擴容問題說明
    5.2 通過add() 方法探索擴容問題
    5.3 擴容演算法
    5.4 模擬擴容演示
六、ArrayList 的序列化問題補充

一、前言

    這裡主要研究到以下問題,通過原始碼閱讀分析探索以下問題的答案。本文不牽涉到更多問題,所以原始碼只貼出與這些問題直接聯絡的關鍵程式碼塊。當然原始碼中必要的全域性常量、方法會貼出。

  • ArrayList 的繼承與實現關係;
  • ArrayList 關於陣列和集合的討論;
  • ArrayList 初始容量是0還是10問題的確認;
  • ArrayList 的擴容問題探索;
  • ArrayList 的序列化問題補充;

二、ArrayList 的繼承與實現關係

2.1 ArrayList.java

    ArrayList類通過extends關鍵字繼承AbstractList抽象類,通過關鍵字implements實現List集合介面、RandomAccess標記介面、Cloneable克隆介面、Serializable序列化介面。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}複製程式碼

圖2-1、ArrayList 繼承與實現關係圖:

ArrayList 原始碼分析 -- 擴容問題及序列化問題

2.2 抽象類AbstractList.java

    抽象類AbstractList繼承一個AbstractCollection集合同樣實現了集合List介面。

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {}複製程式碼

2.3 介面List.java

    List直接繼承於底層集合Collection,List是一個集合,誰贊成?誰反對?

public interface List<E> extends Collection<E> {}複製程式碼

2.4 介面RandomAccess.java

    此介面的主要目的是允許一般的演算法更改其行為,從而在將其應用到隨機或連續訪問列表時能提供良好的效能。
    介面RandomAccess 是一個標記介面,實現該介面的集合List 支援快速隨機訪問。List 集合儘量要實現RandomAccess 介面,如果集合類是RandomAccess 的實現,則儘量用for(int i = 0; i < size; i++) 來遍歷效率高,而不要用Iterator迭代器來遍歷(如果List是Sequence List,則最好用迭代器來進行迭代)。

2.5 介面Cloneable

    關於深拷貝與淺拷貝應寫一篇部落格去說明。想深入瞭解可以參考知乎問答深拷貝與淺拷貝
實現介面的目的是重寫java.lang.Object的clone()的方法,實現淺拷貝。深拷貝和淺拷貝針對像 Object, Array 這樣的複雜物件的。淺拷貝只複製一層物件的屬性,而深拷貝則遞迴複製了所有層級。

  • 淺拷貝
        被複制(拷貝)物件的所有變數都含有與原來的物件相同的值,而所有的對其他物件的引用仍然指向原來的物件。淺拷貝即新建一個物件,複製原物件的基本屬性,一級屬性到新的儲存空間,不拷貝原物件的物件引用元素,新物件的物件引用指向原來的儲存空間,修改物件引用的元素,那麼拷貝物件和原物件都會變化。
  • 深拷貝
        深拷貝是一個整個獨立的物件拷貝,深拷貝會拷貝所有的屬性,並拷貝屬性指向的動態分配的記憶體。當物件和它所引用的物件一起拷貝時即發生深拷貝。深拷貝相比於淺拷貝速度較慢並且花銷較大。深拷貝新建一個物件,不僅拷貝物件,還拷貝物件引用;深拷貝就是我們平常理解的複製,將物件的全部屬性及物件引用複製到新的儲存空間,不會指向原來的物件,修改新物件的任意元素都不會影響原物件。

2.6 介面Serializable

    該介面無繼承實現關係,實現該介面的類支援序列化。因此ArrayList 支援序列化。

  • 序列化:可以將一個物件的狀態寫入一個Byte 流裡;
  • 反序列化:可以從其它地方把該Byte 流裡的資料讀出來。

三、ArrayList 關於陣列和集合的討論

3.1 ArrayList 是陣列還是集合問題說明

    ArrayList 是陣列還是集合,這也算問題?
    我們都知道List 是集合啊,ArrayList 繼承於List 也是集合。不過你或許會在某些文章上見過ArrayList 是陣列或者說ArrayList 是基於陣列的說法。

3.2 從構造方法分析ArrayList

    那我們首先看一下ArrayList 的構造方法;

    // 預設空陣列,final 關鍵字修飾
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // 空資料的共享空陣列例項
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 儲存ArrayList元素的陣列緩衝區,即ArrayList 存放資料的地方
     * ArrayList的容量是這個陣列緩衝區的長度
     * transient 關鍵字修飾,elementData 不支援序列化
     */
    transient Object[] elementData;

    /**
     * 預設無參構造方法
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 帶整型引數的構造方法
     * @param initialCapacity 初始化容量大小
     */
    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);
        }
    }

    /**
     * 泛型集合引數構造方法
     * @param c 集合型別引數
     */
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
複製程式碼

    無參構造方法中直接初始化Object[] elementData = {}; 即ArrayList 的儲存資料的容器是一個陣列。帶參構造方法也是new Object() 或者通過Arrays 的方法轉換為陣列物件。
ArrayList 實現了List 介面,重寫了集合的add(),size(),get(),remove(),toArray()等方法,多個方法的內部程式碼塊是基於陣列來處理資料的。

3.1 確認ArrayList 是集合

    因此ArrayList 是實現List 介面的集合,是基於陣列的集合,資料的儲存容器是陣列,集合方法是通過陣列實現的(比如泛型引數構造方法是將傳入的集合c 先轉化為陣列在進行處理的)。包括其內部類Itr implements Iterator 中重新Iterator 的方法也是基於陣列計算的。
    搞這個問題有意義嗎?有意義^_^

四、ArrayList 初始容量是0 還是10 問題的確認

4.1 從構造方法看初始容量

    從第三部分中的構造方法可以看出
    無參構造一個ArrayList 時儲存資料的容器elementData = {};此時儲存容器大小為0 ;
    帶整型引數的構造方法通過傳入的整型資料的大小來確認初始化儲存容器elementData 的大小,當initialCapacity == 0 時,還是賦值elementData = {};
    泛型集合引數構造方法,根據集合的大小來初始化elementData 的大小,將集合轉化為陣列,陣列的大小為0 的情況下,仍然賦值elementData = {};

4.2 從add() 方法看初始容量

    這個初始化和10 又有什麼關係???
    在ArrayList 中定義了一個預設的儲存容器的大小DEFAULT_CAPACITY 為10,用關鍵字final 修飾,註釋是預設初始容器大小,通過構造方法建立ArrayList 物件並沒有使用到這個常量,我們看看這個初始容器大小是怎麼初始化容器大小的。

    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;
    // 集合的邏輯大小,即儲存真實資料的數量
    private int size;
    public int size() {return size;}
    public boolean isEmpty() {return size == 0;}
    /**
     * 新增元素
     * @param e
     * @return
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    /**
     * 確認集合內部容量大小
     * @param minCapacity
     */
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    /**
     * 計算集合的容量
     * @param elementData
     * @param minCapacity
     * @return
     */
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
複製程式碼

    在集合add() 新增元素時,會將當前的size + 1 傳入ensureCapacityInternal() 方法確認當前elementData 陣列大小是否足夠
    足夠的話size自增一,size = size + 1直接新增的元素賦值給elementData[size];
    不足夠的話進行擴容,擴容問題下面涉及,這裡說擴容中特殊情況,對空集合的擴容,比如我們通過無參構造方法建立了集合物件,此時容器大小為0,然後呼叫add() 方法新增一個元素,此時elementData == {}即此時elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,滿足該條件在計算集合的容量方法calculateCapacity 中會進行”容器初始化”,其實是擴容而已;

這裡的”=” 是等於不是賦值
此時return Math.max(DEFAULT_CAPACITY, minCapacity);
minCapacity = size + 1 = 0 + 1 = 1
DEFAULT_CAPACITY = 10
minCapacity < DEFAULT_CAPACITY = 1 < 10
結果return 10;

    此時容器elementData 擴容為Object[10]

4.3 確定ArrayList 的初始容量

    從以上兩方面分析,所以ArrayList 的初始容量根據傳參確定,預設無參構造方法下新物件的容器初始大小為0。而10 是在空集合新增第一個元素時擴容時的預設容器大小。

五、ArrayList 的擴容問題探索

5.1 擴容問題說明

    集合擴容就是集合容量大小不能滿足需要儲存資料的數量,而需要將elementData 容器大小增大,以儲存更多的元素。

5.2 通過add() 方法探索擴容問題

    集合儲存容器elementData 的容量大小不小於真實儲存元素數量size

elementData.length > size 為真true
elementData.length = size 為真true
elementData.length < size 為假false

    集合在新增元素時會首先判斷當前容器是否能裝下第size + 1 個元素。不能的情況下會進行擴容,上面初始容量問題中談到當空集合擴容時會給該集合物件一個預設的容器大小10,即擴容到elementData.length == 10
這是一種特殊情況,給了一個預設值,並沒有真正涉及擴容核心演算法。
    下面看看ArrayList 是如何擴容的。

    // 集合最大容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    /**
     * 新增元素
     * @param e
     * @return
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    /**
     * 確認集合內部容量大小
     * @param minCapacity 新增元素後容器的最小容量
     */
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    /**
     * 計算集合的容量
     * @param elementData 儲存資料的容器
     * @param minCapacity 新增元素後容器的最小容量
     * @return
     */
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        return minCapacity;
    }
    /**
     * 確認明確的容量大小
     * @param minCapacity 新增元素後容器的最小容量
     */
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    /**
     * 擴容方法
     * @param minCapacity 新增元素後容器的最小容量
     */
    private void grow(int minCapacity) {
        // 擴容前容器大小
        int oldCapacity = elementData.length;
        // 擴容關鍵演算法,newCapacity 擴容後容器大小
        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) throw new OutOfMemoryError();
        // 超過最大值不合法,直接將容量大小定義為Intager 的最大值
        return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
    }
複製程式碼

    在新增元素的方法中依此呼叫容器大小判斷相關的方法,當容器大小不夠時,會進行擴容,呼叫grow() 方法進行擴容。擴容方法很簡單,拿到擴容前容器大小oldCapacity,進行擴容,判斷擴容後容量是否合法,是否溢位,然後進行處理為合理的大小。

5.3 擴容演算法

    擴容演算法是首先獲取到擴容前容器的大小。然後通過oldCapacity + (oldCapacity >> 1) 來計算擴容後的容器大小newCapacity。
這裡的擴容演算法用到了>> 右移運算。即將十進位制轉換為二進位制,每一位右移後得到的結果。oldCapacity >> 1即oldCapacity 對2 求摩,oldCapacity/2;

oldCapacity + (oldCapacity >> 1)即oldCapacity + (oldCapacity / 2)

    所以關鍵擴容演算法就是當容量不夠儲存元素時,在原容器大小size 基礎上再擴充size 的接近一半,即大約擴充原容器的一半。
    相對直白的嚴謹的擴容演算法如下:

擴容後容器大小newCapacity = size + size / 2

5.4 模擬擴容演示

    舉個栗子:原容器是10,elementData 已經儲存10 個元素了,再次呼叫add() 方法會走grow() 方法進行擴容。執行中截圖如下圖

10 / 2 =5
新的容器大小為 10 + 5 = 15
另:運算 “/” 的結果是整數,15/2 =7;9/2 = 4; 8/2 = 4;

圖5-1、擴容前容量大小圖:

圖5-2、擴容後容量大小圖:

圖5-3、擴容後elementData 容量大小圖:

六、ArrayList 的序列化問題補充

    集合的儲存容器elementData 使用transient 關鍵字修飾不支援序列化,但是我們知道ArrayList 是支援序列化的,那我們是怎麼序列化集合中的資料呢,這裡不直接序列化elementData,而是遍歷每個資料分別進行IO 流處理來實現儲存容器中物件的序列化的。

    // ArrayList 列表結構被修改的次數。    
    protected transient int modCount = 0;

    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // ArrayList 列表結構被修改的次數。
        int expectedModCount = modCount;
        s.defaultWriteObject();
        s.writeInt(size);
        // 對每一個物件進行IO 流的寫處理
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }

        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
複製程式碼

    這裡對儲存容器Object[] elementData 用transient 關鍵字修飾,考慮到容器的儲存空間在擴容後會產生很大閒置空間,擴容前容量越大這個問題越明顯;序列化時會將空的物件空間也進行序列化,而真實儲存的元素的數量為size,那樣處理的話效率很低,所以這裡不支援儲存容器直接序列化,而寫一個新的方法來只序列化size 個真實元素即可。

相關文章