Java——ArrayList原始碼解析

Zhaoxi_Zhang發表於2018-12-11

以下針對JDK 1.8版本中的ArrayList進行分析。

概述

    ArrayList基於List介面實現的大小可變的陣列。其實現了所有可選的List操作,並且元素允許為任意型別,包括null元素。除了實現List介面,此類還提供了操作內部用於儲存列表陣列大小的方法(這個類除了沒有實現同步外,功能基本與Vector一致)。

    每個ArrayList例項都有一個容量。容量是用於儲存列表中元素的陣列的大小。它始終至少與列表大小一樣大。隨著元素新增到ArrayList,其容量會自動增加。除了新增元素具有恆定的攤銷時間成本這一事實之外,增長策略並沒有詳細指出。

    我們在新增大容量資料的時候可以使用ensureCapacity方法來主動擴容,這可以減少自動擴容的次數。

    值得注意的是,這些實現都不是同步的。因此,當多個執行緒併發訪問一個ArrayList例項並且至少有一個執行緒對這個例項進行結構性調整的時候,必須在外部額外實現同步(對於結構性調整,主要指增加或刪除一個或多個元素,更精確的說,就是對這個列表的大小進行了調整,對於更改元素的數值並非是結構性調整)。這通常通過在自然封裝列表的某個物件上進行同步來完成。如果不存在此類物件,則應使用Collections.synchronizedList方法“包裝”該列表。這最好在建立時完成,以防止意外地不同步訪問列表:List list = Collections.synchronizedList(new ArrayList(...));

    注意,迭代器的快速失敗行為無法得到保證,因為一般來說,不可能對是否出現不同步併發修改做出任何硬性保證。快速失敗迭代器會盡最大努力丟擲ConcurrentModificationException。因此,為提高這類迭代器的正確性而編寫一個依賴於此異常的程式是錯誤的做法:迭代器的快速失敗行為應該僅用於檢測 bug。

原始碼分析

主要欄位

/**
 * 預設初始容量
 */
private static final int DEFAULT_CAPACITY = 10;

/**
 * 用於ArrayList空例項的共享空陣列例項
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * 用於預設大小空例項的共享空陣列例項。我們將this(DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
 * 和EMPTY_ELEMENTDATA區別開來,以便在新增第一個元素時知道陣列大小要擴容為多少多少。
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * 儲存 ArrayList 元素的陣列緩衝區。ArrayList 的容量是此陣列緩衝區的長度。
 * 當第一個元素新增進空陣列時候 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 將會被擴容至 DEFAULT_CAPACITY
 */
transient Object[] elementData; // non-private to simplify nested class access

/**
 * ArrayList的陣列大小(ArrayList包含的元素個數
 */
private int size;
複製程式碼

    注意此處的elementData欄位是用的transient修飾的以及對於空例項有DEFAULTCAPACITY_EMPTY_ELEMENTDATAEMPTY_ELEMENTDATA兩個共享空陣列例項,下面會對提到的這些注意點進行分析。

建構函式

/**
 * 根據指定的容量初始化空的列表,注意當容量為 0 時,使用的是 EMPTY_ELEMENTDATA
 */
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);
    }
}

/**
 * 初始化容量為 10 的空列表
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/**
 * Constructs a list containing the elements of the specified
 * collection, in the order they are returned by the collection's
 * iterator.
 */
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;
    }
}
複製程式碼

    注意到對於無參構造器使用的是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,而對於帶參構造器,當 initialCapacity 為0時,使用的是EMPTY_ELEMENTDATA;另外,在無參構造器中的註釋——“初始化容量為10的空列表”,我們不禁有以下疑惑:

  • EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA都是空的物件陣列,為什麼在構造器中要對其進行區分
  • 無參構造器中,只是把空的物件陣列賦值給了elementData,為什麼註釋稱宣告瞭長度為10的空陣列

對於以上問題,將在儲存和擴容部分進行講解。

儲存和擴容

/**
 * 增加 ArrayList 例項的容量,確保 ArrayList 例項能儲存 minCapacity 個元素
 */
public void ensureCapacity(int minCapacity) {
    int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
        // any size if not default element table
        ? 0
        // larger than default for default empty table. It's already
        // supposed to be at default size.
        : DEFAULT_CAPACITY;

    if (minCapacity > minExpand) {
        ensureExplicitCapacity(minCapacity);
    }
}
複製程式碼

    當ArrayList例項是個空列表並且 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA時,minExpand 設定為DEFAULT_CAPACITY(10),此時如果 minCapacity 小於 minExpand,那麼不馬上進行擴容操作,在進行add操作時候,會初始化一個容量為 10 的空列表,這樣不僅符合無參構造器中的註釋,並且保證了 ArrayList 例項能夠儲存 minCapacity 個元素。

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}
複製程式碼

    在add操作內部,會呼叫這個私有方法來確保有足夠的容量來放置元素。注意,這個函式一開始就對 elementData 進行判斷是否為 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,如果是的話,證明是無參構造器初始化的例項,在下一步會初始化一個容量為 10 的空列表,符合無參構造器中的註釋,其實就是一個延遲初始化的技巧。

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // 擴容操作
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

/**
 * 允許分配的最大陣列大小
 *一些 VM 會在陣列頭部儲存頭資料,試圖嘗試建立一個比 Integer.MAX_VALUE - 8 大的陣列可能會產生 OOM 異常。
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * 增加 ArrayList 例項的容量,確保 ArrayList 例項能儲存 minCapacity 個元素
 */
private void grow(int minCapacity) {
    // overflow-conscious code
    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);
    // minCapacity is usually close to size, so this is a win:
    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;
}

/**
 * 將指定的元素追加到此列表的末尾
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

/**
 * 在列表中將指定元素插入到指定位置,將其後元素都向右移動一個位置
 */
public void add(int index, E element) {
    //檢查 index 是否越界
    rangeCheckForAdd(index);

    //確保有足夠的容量能夠新增元素
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    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;
}

/**
 * 將指定集合中的全部元素插入到列表指定的位置後面
 */
public boolean addAll(int index, Collection<? extends E> c) {
    rangeCheckForAdd(index);

    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount

    int numMoved = size - index;
    if (numMoved > 0)
        System.arraycopy(elementData, index, elementData, index + numNew,
                         numMoved);

    System.arraycopy(a, 0, elementData, index, numNew);
    size += numNew;
    return numNew != 0;
}
複製程式碼

    因此對於無參構造器的註釋的疑問,到達這裡就可以解答了,它確確實實初始化了一個大小為 10 的空列表,只是不是一開始就初始化,而是使用了延遲初始化的方式,在add的時候才進行初始化。     對於另一個問題,無參構造器使用DEFAULTCAPACITY_EMPTY_ELEMENTDAT,對於new ArrayList(0);使用的是EMPTY_ELEMENTDATA,前者是不知道需要的容量大小,後者預估元素較少。因此ArrayList對此做了區別,通過引用判斷來區別使用者行為,使用不同的擴容演算法(擴容速度:無參:10->15->22...,有參且引數為0 :0->1->2->3->4->6->9...)。另外,在 JDK 1.7 中,沒有通過兩個空陣列來對使用者行為進行區分,因此容量為 0 的話,會建立很多空陣列new Object[0],因此上述方式也對這種情況進行了優化。

//JDK 1.7
public ArrayList(int initialCapacity) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity];
}


public ArrayList() {
    this(10);
}
複製程式碼

刪除

/**
 * 從列表中刪除指定位置的元素,並將其後位置的元素向左移動
 */
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);
    //置null,讓 GC 可以工作
    elementData[--size] = null;
    return oldValue;
}

/**
 * 刪除列表中首次出現的指定的元素,若列表不存在相應元素,則不做改變
 */
public boolean remove(Object o) {
    //若指定元素為 null,因其為空,沒有 equals 方法,因此這兩個地方做一個區分
    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 remove method that skips bounds checking and does not
 * return the value removed.
 */
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
}

/**
 * 刪除列表中所有的元素
 */
public void clear() {
    modCount++;

    // 置 null 以便讓 GC 回收
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

/**
 * 從列表中刪除 fromIndex <= pos < toIndex 的元素
 */
protected void removeRange(int fromIndex, int toIndex) {
    modCount++;
    int numMoved = size - toIndex;
    System.arraycopy(elementData, toIndex, elementData, fromIndex,
                     numMoved);

    // clear to let GC do its work
    int newSize = size - (toIndex-fromIndex);
    for (int i = newSize; i < size; i++) {
        elementData[i] = null;
    }
    size = newSize;
}
複製程式碼

ArrayList的序列化

主要欄位 部分我們可以看到,elementData 是通過transient修飾的(transient具體用法可參看Java物件序列化一文),通過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();
    }
}

/**
 * 從序列中恢復 ArrayList 中例項的狀態
 */
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
        ensureCapacityInternal(size);

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

通過一個例子驗證其序列化和反序列化過程:

package test;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class ListDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        List<String>list = new ArrayList<>();
        list.add("hello");
        list.add("world");

        //write Obj to File
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("file"));
        oos.writeObject(list);
        oos.close();

        //read Obj from File
        File file = new File("file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        List<String>newList = (List<String>) ois.readObject();
        ois.close();
        System.out.println(newList);
    }
}
/*
[hello, world]
 */
複製程式碼

可以得出結論:ArrayList支援進行序列化操作,此時不禁會思考既然要將 ArrayList 的欄位序列化(即將 elementData 序列化),那為什麼又要用 transient 修飾 elementData 呢?實際上,ArrayList 通過動態陣列的技術,當陣列放滿後,自動擴容,但是擴容後的容量往往都是大於或者等於 ArrayList 所存元素的個數。如果直接序列化 elementData 陣列,那麼就會序列化一大部分沒有元素的陣列,導致浪費空間,為了保證在序列化的時候不會將這麼大部分沒有元素的陣列進行序列化,因此設定為 transient。

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

從原始碼中,可以觀察到迴圈時是使用i < size而不是i<elementData.length,說明序列化時,只需實際儲存的那些元素,而不是整個陣列。

問題

  • List<Integer>list = new ArrayList<>(10); out.println(list.get(1));是否會丟擲異常
  • ArrayList 支援序列化,為什麼 elementData 要設定為transient
  • 對於空例項陣列,為什麼要區分DEFAULTCAPACITY_EMPTY_ELEMENTDATAEMPTY_ELEMENTDATA

相關文章