ArrayList原始碼閱讀筆記

20170405發表於2020-08-18

簡介

ArrayList是基於陣列實現的一種列表。

ArrayList繼承體系如下:

圖一:ArrayList繼承體系

在這裡插入圖片描述

  • ArrayList實現了List, RandomAccess, Cloneable, java.io.Serializable等介面。

  • ArrayList實現了List,提供了基礎的新增、刪除、遍歷等操作。

  • ArrayList實現了RandomAccess,提供了隨機訪問的能力。

  • ArrayList實現了Cloneable,可以被克隆。

  • ArrayList實現了Serializable,可以被序列化。


原始碼分析

屬性

首先看看ArrayList的屬性。

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

    /**
     * 空陣列,如果傳入的容量為0時使用
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     *空陣列,傳傳入容量時使用,新增第一個元素的時候會重新初始為預設容量大小
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     *儲存資料元素的陣列
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * ArrayList的大小(包含資料元素的個數)
     */
    private int size;

構造方法

  • 無參構造方法
    /**
     * 建立一個初始容量為10的空列表
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
   
  • 指定初始容量的構造方法
  /**
  * 建立一個指定容量的list
  */
    public ArrayList(int initialCapacity) {
       // 如果傳入的初始容量大於0,就新建一個陣列儲存元素
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            // 如果傳入的初始容量等於0,使用空陣列EMPTY_ELEMENTDATA
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
  • 從其它集合構造
   /**
   * 把傳入集合的元素初始化到ArrayList中
   */
    public ArrayList(Collection<? extends E> c) {
       //把集合轉為陣列
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            //  檢查c.toArray()返回的是不是Object[]型別,如果不是,重新拷貝成Object[].class型別
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 如果是空集合,則初始化為空陣列EMPTY_ELEMENTDATA
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

add(E e)

新增元素到末尾,平均時間複雜度為O(1)。
在這裡插入圖片描述

    /**
    * 在末尾新增元素
    */
    public boolean add(E e) {
         // 檢查是否需要擴容
        ensureCapacityInternal(size + 1);  
        // 把元素插入到末尾
        elementData[size++] = e;
        return true;
    }

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

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 如果是空陣列DEFAULTCAPACITY_EMPTY_ELEMENTDATA,就初始化為預設大小10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

    private void ensureExplicitCapacity(int minCapacity) {
       // 修改次數 +1,用於 fail-fast 處理
        modCount++;

        // 如果 minCapacity 大於 elementData 的長度,則進行擴容處理
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    } 
    
    /**
    *  擴容
    */
     private void grow(int minCapacity) {
        // 有整形溢位風險的程式碼
        int oldCapacity = elementData.length;
        //新容量=舊容量+(舊容量右移1位(除以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);
    }

這裡我們稍微看一下Arrays.copyOf的原始碼:

    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) {
        @SuppressWarnings("unchecked")
        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方法:

    public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

關於陣列拷貝,谷歌了一下,說法不一,有說是深拷貝的,有說是淺拷貝的。暫時先放下,未來有機會再研究。


add(int index, E element)

add(int index, E element)在特定位置插入元素,時間複雜度為O(n)。

    public void add(int index, E element) {
        // 檢查是否越界
        rangeCheckForAdd(index);
        //檢查是否需要擴容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //// 將 elementData 中位置為 index 位置及其後面的元素都向後移動一個下標(底層是 native 方法,使用 cpp 直接操作記憶體。)
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        // 將元素插入到index的位置                 
        elementData[index] = element;
        //// 大小增1
        size++;
    }

addAll

addAll用於批量新增。

    public boolean addAll(Collection<? extends E> c) {
       // 集合轉化成陣列
        Object[] a = c.toArray();
        
        int numNew = a.length;
        //檢查是否需要擴容
        ensureCapacityInternal(size + numNew);  // Increments modCount
        //將集合內的元素複製到 elementData 中,覆蓋 [size, size+numNew) 的元素
        System.arraycopy(a, 0, elementData, size, numNew);
        //陣列大小增加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)
           // 將 elementData 中位置為 index 及其以後的元素都向後移動 numNew 個位置
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);
        // 將集合內的元素複製到 elementData 中,覆蓋 [index, index+numNew) 的元素                      
        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

get(int index)

獲取指定位置元素,時間複雜度為O(1)。

    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

remove(int index)

刪除指定索引位置的元素,時間複雜度為O(n)。

    public E remove(int index) {
       //檢查下標是否越界
        rangeCheck(index);

        modCount++;
        //獲取指定索引處元素
        E oldValue = elementData(index);
        //// 如果index不是最後一位,則將index之後的元素往前挪一位
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //將最後一個元素刪除,幫助GC                     
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

remove(Object o)

刪除指定元素,時間複雜度為O(n²)。

    public boolean remove(Object o) {
       //元素為null
        if (o == null) {
           //遍歷陣列
            for (int index = 0; index < size; index++)
               //快速刪除所有為null的元素
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            //元素不為null,遍歷陣列
            for (int index = 0; index < size; index++)
               //找到對應元素,快讀刪除
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

    //和remove(int index)基本相同,少了檢查越界的方法,無返回值
    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        // 如果index不是最後一位,則將index之後的元素往前挪一位
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 將最後一個元素刪除,幫助GC                     
        elementData[--size] = null; // clear to let GC do its work
    }

removeAll

用於批量刪除元素。

   //批量刪除 ArrayList 和集合 c 都存在的元素
    public boolean removeAll(Collection<?> c) {
       //非空校驗
        Objects.requireNonNull(c);
       //批量刪除 
        return batchRemove(c, false);
    }

    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++)
                 // 把需要保留的元素前置
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            if (w != size) {
                // 跟 fastRemove(int index) 裡面的操作類似,防止記憶體洩漏
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }

set

用於更改特定下標的值,時間複雜度為O(1)。

    public E set(int index, E element) {
        
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

序列化

注意觀察,ArrayList 有兩個屬性被 transient 關鍵字 修飾,transient 關鍵字的作用:讓某些被修飾的成員屬性變數不被序列化

transient Object[] elementData;
protected transient int modCount = 0;

為什麼最重要的元素陣列要被transient 修飾呢?

因為ArrayList 並沒有用Java序列化機制的預設處理來序列化 elementData 陣列,而是通過 readObject、writeObject 方法自定義序列化和反序列化策略。

之所以要用自定義的序列化和反序列化策略,是因為效率的問題。如果用預設處理來序列化的話,如果 elementData 的長度有100,但是實際只用了50,其實剩餘的50是可以不用序列化的,這樣可以提高序列化和反序列化的效率,節省空間。

   /**
   *  自定義序列化
   */
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // fail-fast,後續判斷是否有併發處理
        int expectedModCount = modCount;
         // 序列化沒有標記為 static、transient 的欄位,包括 size 等。
        s.defaultWriteObject();

        
        s.writeInt(size);

        // 序列化陣列的前size個元素
        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;

        //// 反序列化沒有標記為 static、transient 的欄位,包括 size 等
        s.defaultReadObject();

       
        s.readInt(); // ignored

        if (size > 0) {
             // 陣列擴容
            int capacity = calculateCapacity(elementData, size);
            SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
            ensureCapacityInternal(size);

            Object[] a = elementData;
            // 反序列化元素並填充到陣列中
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }



執行緒安全

在上面的原始碼裡我們看到很多方法裡有快速失敗的機制,例如判斷擴容的方法:

private void ensureExplicitCapacity(int minCapacity) {
    // 修改次數 +1,用於 fail-fast 處理
    modCount++;

這種快讀失敗的機制一定程度上避免了執行緒安全問題。

"快速失敗”即fail-fast,它是java集合的一種錯誤檢測機制。當多錢程對集合進行結構上的改變或者集合在迭代元素時直接呼叫自身方法改變集合結構而沒有通知迭代器時,有可能會觸發fast-fail機制並丟擲異常。

當然,fail-fast機制只是可能觸發,實際上,ArrayList的執行緒安全還是沒有保證的。一般,保證ArrayList的執行緒安全可以通過這些方案:

  • 使用 Vector 代替 ArrayList。(不推薦,Vector是一個歷史遺留類)
  • 使用 Collections.synchronizedList 包裝 ArrayList,然後操作包裝後的 list 即可。
  • 使用 CopyOnWriteArrayList 代替 ArrayList。
  • 在使用 ArrayList 時,應用程式通過同步機制去控制 ArrayList 的讀寫(不推薦)。




紙上得來終覺淺,絕知此事要躬行。



參考:

【1】:【死磕 Java 集合】— ArrayList原始碼分析
【2】:java 集合之 ArrayList 原始碼解讀
【3】:ArrayList 原始碼分析

相關文章