1. 程式人生 > >Java ArrayList底層實現原理原始碼詳細分析Jdk8

Java ArrayList底層實現原理原始碼詳細分析Jdk8

簡介

  • ArrayList是基於陣列實現的,是一個動態陣列,其容量能自動增長,類似於C語言中的動態申請記憶體,動態增長記憶體。
  • ArrayList不是執行緒安全的,只能用在單執行緒環境下,多執行緒環境下可以考慮用Collections.synchronizedList(List l)函式返回一個執行緒安全的ArrayList類,也可以使用concurrent併發包下的CopyOnWriteArrayList類。
  • ArrayList實現了Serializable介面,因此它支援序列化,能夠通過序列化傳輸,實現了RandomAccess介面,支援快速隨機訪問,實際上就是通過下標序號進行快速訪問,實現了Cloneable介面,能被克隆。

儲存結構

// 當前資料物件存放地方,當前物件不參與序列化
// 這個關鍵字最主要的作用就是當序列化時,被transient修飾的內容將不會被序列化
transient Object[] elementData;
  • Object型別陣列。

    資料域

    // 序列化ID
    private static final long serialVersionUID = 8683452581122892189L;
    // 預設初始容量
    private static final int DEFAULT_CAPACITY = 10;
    // 一個空陣列,方便使用,主要用於帶參建構函式初始化和讀取序列化物件等。
    private static final Object[] EMPTY_ELEMENTDATA = {};
    /**
     * 和官方文件寫的一樣,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 和EMPTY_ELEMENTDATA 的區別
     * 僅僅是為了區別使用者帶參為0的構造和預設構造的惰性初始模式物件。
     * 當用戶帶參為0的構造,第一次add時,陣列容量grow到1。
     * 當用戶使用預設構造時,第一次add時,容量直接grow到DEFAULT_CAPACITY(10)。
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
 
    // 當前資料物件存放地方,當前物件不參與序列化
    // 這個關鍵字最主要的作用就是當序列化時,被transient修飾的內容將不會被序列化
    transient Object[] elementData; // non-private to simplify nested class access
    // 當前陣列中元素的個數
    private int size;
    // 陣列最大可分配容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    // 集合陣列修改次數的標識(由AbstractList繼承下來)(fail-fast機制)
    protected transient int modCount = 0;
  • ArrayList的無參建構函式。初始化的時候並沒有真正的建立10個空間,這是惰性初始模式物件。
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA 和EMPTY_ELEMENTDATA 的區別僅僅是為了區別使用者帶參為0的構造和預設構造的惰性初始模式物件。
  • modCount用來記錄ArrayList結構發生變化的次數。用於Fail-Fast機制

建構函式

    public ArrayList() {
        // 只有這個地方會引用DEFAULTCAPACITY_EMPTY_ELEMENTDATA
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            // 使用 EMPTY_ELEMENTDATA,在其他的多個地方可能會引用EMPTY_ELEMENTDATA
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
   
    public ArrayList(Collection<? extends E> c) {
        // 把傳入集合傳化成[]陣列並淺拷貝給elementData 
        elementData = c.toArray();
        // 轉化後的陣列長度賦給當前ArrayList的size,並判斷是否為0
        if ((size = elementData.length) != 0) {
            //c.toArray可能不會返回 Object[],可以檢視 java 官方編號為 6260652 的 bug
            if (elementData.getClass() != Object[].class)
                // 若 c.toArray() 返回的陣列型別不是 Object[],則利用 Arrays.copyOf(); 來構造一個大小為 size 的 Object[] 陣列
                // 此時elementData是指向傳入集合的記憶體,還需要建立新的記憶體區域深拷貝給elementData 
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 傳入陣列size為零替換空陣列
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA 和EMPTY_ELEMENTDATA 的區別僅僅是為了區別使用者帶參為0的構造和預設構造的惰性初始模式物件。
  • 注意深拷貝和淺拷貝。
  • 帶參為0的構造會惰性初始化,不為0的構造則不會惰性初始化。

    add()原始碼解析

public boolean add(E e) {
        // 確保陣列已使用長度(size)加1之後足夠存下 下一個資料
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 陣列的下一個index存放傳入元素。
        elementData[size++] = e;
        // 始終返回true。
        return true;
}
private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 這裡就是DEFAULTCAPACITY_EMPTY_ELEMENTDATA 和
        // EMPTY_ELEMENTDATA 最主要的區別。
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // 預設構造第一次add返回10。
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        // 帶參為0構造第一次add返回 1 (0 + 1)。
        return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
        // 自增修改計數
        modCount++;

        // overflow-conscious code
        // 當前陣列容量小於需要的最小容量
        if (minCapacity - elementData.length > 0)
            // 準備擴容陣列
            grow(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
            newCapacity = minCapacity;
            // 判斷是需要的容量是否超過最大的陣列容量。
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        // 在Arrays.copyOf()中會將原陣列整個賦值到擴容的陣列中。
        elementData = Arrays.copyOf(elementData, newCapacity);
}
  • 擴容操作需要呼叫Arrays.copyOf()把原陣列整個複製到新陣列中,這個操作代價很高,因此最好在建立ArrayList物件時就指定大概的容量大小,減少擴容操作的次數。

add(int index, E element)原始碼分析

// 這是一個本地方法,由C語言實現。
public static native void arraycopy(Object src,  // 源陣列
                                    int  srcPos, // 源陣列要複製的起始位置
                                    Object dest, // 目標陣列(將原陣列複製到目標陣列)
                                    int destPos, // 目標陣列起始位置(從目標陣列的哪個下標開始複製操作)
                                    int length   // 複製源陣列的長度
                                    );

public void add(int index, E element) {
        // 判斷索引是否越界
        rangeCheckForAdd(index);
        // 確保陣列已使用長度(size)加1之後足夠存下 下一個資料
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 執行到這裡代表陣列容量滿足。
        // 陣列從傳入形參index處開始複製,複製size-index個元素(即包括index在內後面的元素全部複製),
        // 從陣列的index + 1處開始貼上。
        // 這時,index 和 index + 1處元素數值相同。
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        // 把index處的元素替換成新的元素。
        elementData[index] = element;
        // 陣列內元素長度加一。
        size++;
}
  • 需要呼叫System.arraycopy()將包括index在內後面的元素都複製到index + 1位置上,該操作的時間複雜度為O(N),可以看出ArrayList陣列頭增加元素的代價是非常高的。

remove(int index)原始碼分析

public E remove(int index) {
        // 檢查index 
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 和 add(int index, E element)原理想通。
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 引用計數為0,會自動進行垃圾回收。
        elementData[--size] = null; // clear to let GC do its work
        // 返回舊元素
        return oldValue;
    }
  • 需要呼叫System.arraycopy()將包括index + 1在內後面的元素都複製到index位置上,該操作的時間複雜度為O(N),可以看出ArrayList陣列頭增加元素的代價是非常高的。

Fail-Fast機制

fail-fast 機制,即快速失敗機制,是java集合(Collection)中的一種錯誤檢測機制。當在迭代集合的過程中該集合在結構上發生改變的時候,就有可能會發生fail-fast,即丟擲ConcurrentModificationException異常。fail-fast機制並不保證在不同步的修改下一定會丟擲異常,它只是盡最大努力去丟擲,所以這種機制一般僅用於檢測bug。

  • 結構發生變化是指新增或者刪除至少一個元素的所有操作,或者是調整內部陣列大小,僅僅只是設定元素的值不算結構發生變化。
  • 在進行序列化或者迭代操作時,需要比較操作前後modCount是否改變,如果改變了需要跑出ConcurrentModificationException
private class Itr implements Iterator<E> {
        int cursor;
        int lastRet = -1;
        // 期待的修改值等於當前修改次數(modCount)
        int expectedModCount = modCount;
 
        public boolean hasNext() {
            return cursor != size;
        }
 
        public E next() {
            // 檢查 expectedModCount是否等於modCount,不相同則丟擲ConcurrentModificationException
            checkForComodification();
            /** 省略此處程式碼 */
        }
 
        public void remove() {
            if (this.lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
            /** 省略此處程式碼 */
        }
 
        final void checkForComodification() {
            if (ArrayList.this.modCount == this.expectedModCount)
                return;
            throw new ConcurrentModificationException();
        }
    }

一個單執行緒環境下的fail-fast的例子

     public static void main(String[] args) {
           List<String> list = new ArrayList<>();
           for (int i = 0 ; i < 10 ; i++ ) {
                list.add(i + "");
           }
           Iterator<String> iterator = list.iterator();
           int i = 0 ;
           while(iterator.hasNext()) {
                if (i == 3) {
                     list.remove(3);
                }
                System.out.println(iterator.next());
                i ++;
           }
     }

序列化

ArrayList 實現了 java.io.Serializable 介面,但是自己定義了序列化和反序列化。因為ArrayList基於陣列實現,並且具有動態擴容特性,因此儲存元素的陣列不一定都會被使用,那麼就沒有必要全部進行序列化。因此 elementData 陣列使用 transient 修飾,可以防止被自動序列化。

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        // 將當前類的非靜態(non-static)和非瞬態(non-transient)欄位寫入流
        // 在這裡也會將size欄位寫入。
        s.defaultWriteObject();

        // Write out size as capacity for behavioural compatibility with clone()
        // 序列化陣列包含元素數量,為了向後相容
        // 兩次將size寫入流
        s.writeInt(size);

        // Write out all elements in the proper order.
        // 按照順序寫入,只寫入到陣列包含元素的結尾,並不會把陣列的所有容量區域全部寫入
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }
        // 判斷是否觸發Fast-Fail
        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
        // 將流中的的非靜態(non-static)和非瞬態(non-transient)欄位讀取到當前類
        // 包含 size
        s.defaultReadObject();

        // Read in capacity
        // 讀入元素個數,沒什麼用,只是因為寫出的時候寫了size屬性,讀的時候也要按順序來讀
        s.readInt(); // ignored

        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            // 根據size計算容量。
            int capacity = calculateCapacity(elementData, size);
            // SharedSecrets 一個“共享機密”儲存庫,它是一種機制,
            // 用於呼叫另一個包中的實現專用方法,而不使用反射。TODO
            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中為什麼size要序列化兩次?

在程式碼中s.defaultWriteObject();中size應該也被序列化了,為什麼下邊還要再單獨序列化一次呢?
這樣寫是出於相容性考慮。
舊版本的JDK中,ArrayList的實現有所不同,會對length欄位進行序列化。
而新版的JDK中,對優化了ArrayList的實現,不再序列化length欄位。
這個時候,如果去掉s.writeInt(size),那麼新版本JDK序列化的物件,在舊版本中就無法正確讀取,
因為缺少了length欄位。
因此這種寫法看起來多此一舉,實際上卻保證了相容性。

小結

  • ArrayList基於陣列方式實現,無容量的限制(會擴容)
  • 新增元素時可能要擴容(所以最好預判一下),刪除元素時不會減少容量(若希望減少容量可以使用trimToSize()),刪除元素時,將刪除掉的位置元素置為null,下次gc就會回收這些元素所佔的記憶體空間。
  • 執行緒不安全
  • add(int index, E element):新增元素到陣列中指定位置的時候,需要將該位置及其後邊所有的元素都整塊向後複製一位
  • get(int index):獲取指定位置上的元素時,可以通過索引直接獲取(O(1))
  • remove(Object o)需要遍歷陣列
  • remove(int index)不需要遍歷陣列,只需判斷index是否符合條件即可,效率比remove(Object o)高
  • contains(E)需要遍歷陣列