ArrayList原始碼解析-JDK18

LemonDus發表於2024-12-05

引言

ArrayList在JDK1.7和1.8中的差距並不大,主要差距以下幾個方面:

JDK1.7

  • 在JDK1.7中,使用ArrayList list = new ArrayList()建立List集合時,底層直接建立了長度是10的Object[]陣列elementData;在接下來呼叫add()方法向集合中新增元素時,如果本次的新增導致底層elementData陣列容量不足,則呼叫 ensureCapacity(int minCapacity) 方法進行擴容。預設情況下,擴容為原來的1.5倍(>>1),同時將原來陣列中的所有資料複製到新的陣列中。
  • 故而,由此得到結論,在開發中,建議使用帶參構造器建立List集合:ArrayList list = new ArrayList(int capacity),預估集合的大小,直接一次到位,避免中間的擴容,提高效率。

JDK1.8

  • JDK 1.8和1.7中 ArrayList 最明顯的區別就是底層陣列在JDK1.8中,如果不指定長度,使用無參構造方法ArrayList list = new ArrayList()建立List集合時,底層的Object[] elementData初始化為{}(空的陣列),並沒有直接建立長度為10的陣列;
  • 而在第一次呼叫add()方法時,底層才建立了長度為10的陣列,並將本次要新增的元素新增進去(這樣做可節省記憶體消耗,因為在新增元素時陣列名將指標指向了新的陣列且老陣列是一個空陣列這樣有利於System.gc(),並不會一直佔據記憶體)。
  • 後續的新增和擴容操作與JDK1.7無異。

繼承與實現

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

具體繼承與實現結構如下圖所示:
array_jg

可以看到它繼承了AbstractList抽象類,ListRandomAccessCloneableSerializable介面。

HashMap中一樣,Cloneable和Serializable這兩個介面都是標記介面,Cloneable用於標記該類可以被克隆,只有實現這個介面後,然後在類中重寫Object中的clone方法,然後透過類呼叫clone方法才能進行克隆,而Serializable則是表示這個類可以被序列化。

與HashMap不同的是ArrayList還多了個RandomAccess介面,這個介面同樣是標記介面,它用於標記實現該介面的類可以進行隨機訪問。

在這裡還有一點需要注意,為什麼要先繼承AbstractList,而讓AbstractList先實現List?而不是讓ArrayList直接實現List?

這裡是有一個思想,介面中全都是抽象的方法,而抽象類中可以有抽象方法,還可以有具體的實現方法,正是利用了這一點,讓AbstractList是實現介面中一些通用的方法。

而具體的類,如ArrayList就繼承這個AbstractList類,拿到一些通用的方法,然後自己在實現一些自己特有的方法,這樣一來,讓程式碼更簡潔,就繼承結構最底層的類中通用的方法都抽取出來,先一起實現了,減少重複程式碼。

所以一般看到一個類上面還有一個抽象類,就是這個作用。

常量屬性

// 定義陣列的初始容量
private static final int DEFAULT_CAPACITY = 10;

// 定義一個空的陣列
private static final Object[] EMPTY_ELEMENTDATA = {};

// 定義一個預設的空陣列
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 定義儲存元素的陣列,transient:表述序列化的時候該修飾符修飾的屬性不被序列化
transient Object[] elementData; // non-private to simplify nested class access

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

// 陣列中元素的個數
private int size;

// 陣列被修改的次數,如新增刪除元素都會加 1
protected transient int modCount = 0;

構造方法

ArrayList向我們提供了三種構造器:

  • 無參構造器:public ArrayList()
  • 帶初始容量構造器:public ArrayList(int initialCapacity)
  • 帶集合引數的構造器:public ArrayList(Collection<? extends E> c)

無參構造

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

從原始碼中我們可以看出,如果我們不帶任何引數的去建立物件那麼其內部會直接將一個預設的空陣列賦值給ArrayList的陣列(elementData)

帶初始容量構造器

// 帶參構造,initialCapacity:傳入的初始容量
public ArrayList(int initialCapacity) {
    // 1. 判斷是否大於 0 
    if (initialCapacity > 0) {
        // 2. 建立一個對應大小的陣列
        this.elementData = new Object[initialCapacity];
        // 3. 是否等於 0 
    } else if (initialCapacity == 0) {
        // 4. 賦值一個空的陣列
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        // 5. 傳入的容量不合法
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    }
}

從上面兩個構造器來看:

  • 空參的時候ArrayList中的陣列是它:DEFAULTCAPACITY_EMPTY_ELEMENTDATA
  • 容量是 0 的時候ArrayList中的陣列是它:EMPTY_ELEMENTDATA

那麼問題就簡化到空容量0容量的問題了,有的人會說這不一樣的嘛

其實不是,這兩個的區別還是蠻大的,我們一貫的思維就是不傳值就是空容量陣列,傳值就是對應的容量陣列,那我們有沒有想過如果一個人他就是想建立一個容量為 0 的陣列,而不是一來就給我預設擴容到 10 這個容量。

怎麼樣是不是有點道理了。

所以我們可以得一個結論,DEFAULTCAPACITY_EMPTY_ELEMENTDATAEMPTY_ELEMENTDATA就是在擴容得時候區別出來到底是擴容為 10 還是從 0 開始一步步得擴容。

帶集合引數的構造器

public ArrayList(Collection<? extends E> c) {
    // 將傳入得集合變成陣列,賦值給ArrayList的陣列
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // 將型別轉為Object然後再次呼叫copyOf進行賦值
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 傳入的是空的集合,那麼賦值一個容量為EMPTY_ELEMENTDATA型別的空陣列
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

在以上原始碼中需要注意elementData 的二次賦值,也就是Arrays.copyOf那邊的邏輯,既然已經賦過值了elementData = c.toArray(),那為什麼還要二次賦值?

其實關鍵在於toArray()方法,它返回的不是一個 Object[] ,而是 E[] 型別,意味著如果不轉成 Object[] ,你想某個位置add一個Object的子類時,這個時候就會出現異常。

所以,該程式碼的功能就是將elementData陣列中的所有元素變為Object型別,防止在向ArrayList中新增資料的時候拋錯(ArrayStoreException)。

擴容方法

在介紹新增方法之前,先來介紹一下ArrayList最為重要的擴容方法。

ensureCapacityInternal(int minCapacity):陣列容量判斷,容量夠就不做處理,容量不足就進行相應的擴容

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

我們可以看出它中間又呼叫了兩個方法

  1. calculateCapacity(elementData, minCapacity):確定陣列容量
  2. ensureExplicitCapacity(object):進行相應的擴容
// 確定陣列容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {、
    // 如果陣列是預設的空陣列
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 返回連個容量的最大值,就是DEFAULT_CAPACITY = 10
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 否者,陣列不空,返回minCapacity
    return minCapacity;
}

我們可以看出該方法有兩個引數

  • elementData:存放元素的陣列
  • minCapacity:可以放下元素的最小的容量
// 進行相應的擴容
private void ensureExplicitCapacity(int minCapacity) {
    // 陣列修改次數加一
    modCount++;

    // 計算的最小容量是否大於陣列的長度
    if (minCapacity - elementData.length > 0)
        // 擴容
        grow(minCapacity);
}

可以看出,該方法主要是判斷其內部的陣列是否允許再新增元素,如果容量不夠則進行擴容從而保證元素的正常新增而不溢位。

那我們具體來分析一下grow(minCapacity)方法

// 真正擴容方法
private void grow(int minCapacity) {
    // 獲取陣列的長度
    int oldCapacity = elementData.length;
    // 計算新得長度,新長度為舊長度的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);
}

從這個方法,我們就可以知道如果ArrayList中如果陣列容量不足,則會擴容到原來的1.5倍,而具體的擴容操作這是要看Arrays.copyOf(elementData, newCapacity)這個方法的具體實現了。

public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}

再呼叫下面方法:

// 擴容方法的具體實現
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    // 建立指定長度的某種型別的陣列。
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    // 呼叫本地方法將舊陣列元素移動到新陣列中
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    // 返回新陣列
    return copy;
}

在這裡我們可以看到,它會先建立一個指定容量大小的陣列,該陣列就是擴容後的陣列,並且需要被返回出去。

然後這個本地方法System.arraycopy()作用就是將舊陣列元素移動到新陣列中,注意這個方法是native方法,是C++編寫的,這裡使用native方法是為了追求效率,讓擴容更快。

順便再提一下ArrayList原始碼中多個方法用到的判斷陣列下標合法的方法

rangeCheckForAdd(index): 檢查下標時候合理,如果合理不做處理,否則丟擲異常

private void rangeCheck(int index) {
	// 如果傳入的下標大於等於陣列中的元素個數,溢位
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

新增方法

有了上面擴容方法的分析,add方法就會更容易理解了!

ArrayList中向我們提供了四種新增元素的方法

  • 向末尾新增元素:public boolean add(E e)
  • 指定位置添新增元素:public void add(int index, E element)
  • 新增一個集合元素:public boolean addAll(Collection<? extends E> c)
  • 在指定位置新增集合元素:public boolean addAll(int index, Collection<? extends E> c)

add(E e)

public boolean add(E e) {
    // 確保陣列容量
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 在陣列末尾新增元素
    elementData[size++] = e;
    // 返回新增成功
    return true;
}

ensureCapacityInternal這個方法已經分析過了,它會確保我們新增元素的時候容量是充足的,然後就會直接新增元素到陣列末尾,最後再返回成功標識。

在這裡,我們也可以解釋ArrayList為什麼可以新增重複的值並且輸出的值與我們輸入的值順序一致的問題

add(int index, E element)

public void add(int index, E element) {
    // 1. 檢查下標
    rangeCheckForAdd(index);
	// 2. 保證容量
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 3. 開始移動元素,空出指定下標的位置出來
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 4. 在指定下標出賦值
    elementData[index] = element;
    // 5. 陣列元素值加 1
    size++;
}

addAll(Collection<? extends E> c)

// 新增一個集合到陣列中
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;
}

ArrayList中直接新增一個集合方法中我們可以看出集合元素會直接新增在末尾,和add方法基本類似。

addAll(int index, Collection<? extends E> c)

public boolean addAll(int index, Collection<? extends E> c) {
    // 1. 檢查下標
    rangeCheckForAdd(index);
	// 2. 將集合轉為陣列
    Object[] a = c.toArray();
    // 3. 獲取陣列長度
    int numNew = a.length;
    // 4. 保證容量
    ensureCapacityInternal(size + numNew);  // Increments modCount
	// 5. 計算需要移動元素的開始下標
    int numMoved = size - index;
    if (numMoved > 0)
        // 6. 開始移動元素
        System.arraycopy(elementData, index, elementData, index + numNew,
                         numMoved);
	// 7. 開始向目標陣列中新增元素
    System.arraycopy(a, 0, elementData, index, numNew);
    // 8. 設定元素個數
    size += numNew;
    // 9. 返回結果
    return numNew != 0;
}

在指定下標處新增一個集合的元素,關鍵點在於要計算出一個區間的下標出來,存放新增的集合資料,該實現程式碼在步驟5,6處可以看出。

設定方法

public E set(int index, E element) {
    // 檢查下標
    rangeCheck(index);
	// 獲取對應下標資料
    E oldValue = elementData(index);
    // 在對應下標處賦值
    elementData[index] = element;
    // 返回原始資料
    return oldValue;
}

獲取方法

public E get(int index) {
    // 檢查下標
    rangeCheck(index);
	// 返回對應下標值
    return elementData(index);
}

移除方法

對於移除方法,ArrayList中提供了挺多的,分析幾個常用的。

remove(int index)

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

remove(Object o)

// 根據元素移除對應的資料
public boolean remove(Object o) {
    if (o == null) {
        // 遍歷,移除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;
}

方法中呼叫fastRemove方法移除

// 移除第一個遇到的相等的值
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
}

從以上原始碼中我們可以知道,它只會移除第一個與對應的值相同的元素。

removeAll(Collection<?> c)

public boolean removeAll(Collection<?> c) {
    // 判斷集合時候為null
    Objects.requireNonNull(c);
    // 批次移除
    return batchRemove(c, false);
}

移除ArrayList中對應集合中的元素,共分為兩個步驟:

  1. 判斷入參是否為null
  2. 開始批次移除

判斷為null方法

public static <T> T requireNonNull(T obj) {
    if (obj == null)
        throw new NullPointerException();
    return obj;
}

這個非常簡單,就是簡單的判空,如空則丟擲空指標

批次移除方法

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++)
            // 如果要刪除的集合中,不存在ArrayList中的元素
            if (c.contains(elementData[r]) == complement)
                // 將集合中的元素放入elementData中,complement=true就是放入存在的元素,否者就是不存在的元素
                // w是元素個數
                elementData[w++] = elementData[r];
    } finally {
        // c.contains()會丟擲異常
        // 在c.contains()丟擲異常的時候將異常丟擲之前確定的元素進行處理
        if (r != size) {
            System.arraycopy(elementData, r,
                             elementData, w,
                             size - r);
            w += size - r;
        }
        if (w != size) {
            // 將 w 下標以後的元素置空,方便垃圾回收,w下標以前的元素就是我們需要的結果
            for (int i = w; i < size; i++)
                elementData[i] = null;
            // 記錄修改次數
            modCount += size - w;
            // 元素個數
            size = w;
            // 成功
            modified = true;
        }
    }
    return modified;
}

清除方法

public void clear() {
    modCount++;

    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        // 賦空
        elementData[i] = null;
	// 元素個數設為 0 
    size = 0;
}

elementData陣列被修飾transient問題

ArrayList是支援序列化的,那為什麼其中關鍵的儲存元素的陣列要被修飾成transient(序列化時忽略該陣列),矛盾了。

其實不然,我們點進原始碼可以發現,ArrayList中自己重寫了序列化和反序列化的方法,程式碼如下:

// 序列化
private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

// 反序列化
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt(); // ignored

    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        int capacity = calculateCapacity(elementData, size);
        SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
        ensureCapacityInternal(size);

        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}

那為什麼要自己實現一套序列化呢!

ArrayList底層是基於動態陣列實現的,陣列的長度是動態變化的,當陣列的長度擴容到很大的時候,其中的元素卻是寥寥幾個的話,那要是將這些沒有用的空元素也序列化到記憶體中就比較浪費記憶體。

所以就是考慮到這一點,ArrayList才會自己實現一套序列化標準,只序列化有用的元素,這樣可以節省空間。

至此,ArrayList的原始碼就全部分析完畢啦!

相關文章