「必知必會」最細緻的 ArrayList 原理分析

梓川耶發表於2021-08-05

  從今天開始也正式開 JDK 原理分析的坑了,其實寫原始碼分析的目的不再是像以前一樣搞懂原理,更重要的是看看他們編碼風格更進一步體會到他們的設計思想。看原始碼前先自己實現一個再比對也許會有不一樣的收穫!

1. 結構

  首先我們需要對 ArrayList 有一個大致的瞭解就從結構來看看吧.

1. 繼承

  該類繼承自 AbstractList 這個比較好說

2. 實現

這個類實現的介面比較多,具體如下:

  1. 首先這個類是一個 List 自然有 List 介面
  2. 然後由於這個類需要進行隨機訪問,所謂隨機訪問就是用下標任一訪問,所以實現了RandomAccess
  3. 然後就是兩個集合框架肯定會實現的兩個介面 Cloneable, Serializable 前面這個好說序列化一會我們具體再說說

3. 主要欄位

    // 預設大小為10
    private static final int DEFAULT_CAPACITY = 10;
    // 空陣列  
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // 預設的空陣列  這個是在傳入無參的是建構函式會呼叫的待會再 add 方法中會看到
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    // 用來存放 ArrayList 中的元素 注意他的修飾符是一個 transient 也就是不會自動序列化
    transient Object[] elementData; 
    // 大小
    private int size;

4. 主要方法

下面的方法後面標有數字的就是表示過載方法

  1. ctor-3
  2. get
  3. set
  4. add-2
  5. remove-2
  6. clear
  7. addAll
  8. write/readObject
  9. fast-fail 機制
  10. subList
  11. iterator
  12. forEach
  13. sort
  14. removeIf

2. 構造方法分析

1. 無參的構造方法

   裡面只有一個操作就是把 elementData 設定為 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 這個空陣列。

// 無參的建構函式,傳入一個空陣列  這時候會建立一個大小為10的陣列,具體操作在 add 中
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

2. 傳入陣列大小的構造

   這個就是 new 一個陣列,如果陣列大小為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);
        }
    }

3. 傳入 Collection 介面

   在這個方法裡面主要就是把這個 Collection 轉成一個陣列,然後把這個陣列 copy 一下,如果這個介面的 size 為0 和上面那個方法一樣傳入 EMPTY_ELEMENTDATA

public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            // 上面的註釋的意思是說 jdk 有一個 bug 具體來說就是一個 Object 型別的陣列不一定能夠存放 Object型別的物件,有可能拋異常
            // 主要是因為 Object 型別的陣列可能指向的是他的子類的陣列,存 Object 型別的東西會報錯
            if (elementData.getClass() != Object[].class)
                // 這個操作是首先new 了新的陣列,然後再呼叫 System.arraycopy 拷貝值。也就是產生新的陣列
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 傳入的是空的就直接使用空陣列初始化
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
 

   但是注意一點這裡有一個 jdk 的 bug 也就是一個 Object 型別的陣列不一定能夠存放 Object型別的物件,有可能拋異常,主要是因為 Object 型別的陣列可能指向的是他的子類的陣列,存 Object 型別的東西會報錯。 為了測試這個 bug 寫了幾行程式碼測試一下。這個測試是通不過的,就是存在上面的原因。

   一個典型的例子就是 我們建立一個 string 型別的 list 然後呼叫 toArray 方法發現返回的是一個 string[] 這時候自然就不能隨便存放元素了。

class A{
}

class B extends A {
}

public class JDKBug {

    @Test
    public void test1() {
        B[] arrB = new B[10];
        A[] arrA = arrB;
        arrA[0]=new A();
    }
}

3. 修改方法分析

1. Set 方法

   這個方法也很簡單 ,首先進行範圍判斷,然後就是直接更新下標即可。

    // 也沒啥好說的就是,設定新值返回老值
    public E set(int index, E element) {
        rangeCheck(index);

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

2. Add(E e) 方法

  這個方法首先呼叫了 ensureCapacityInternal() 這個方法裡面就判斷了當前的 elementData 是否等於 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 如果是的話,就把陣列的大小設定為 10 然後進行擴容操作,這裡剛好解釋了為什麼採用無參構造的List 的大小是 10 ,這裡擴容操作呼叫的方法是 ensureExplicitCapacity 裡面就幹了一件事如果使用者指定的大小 大於當前長度就擴容,擴容的方法採用了 Arrays.copy 方法,這個方法實現原理是 new 出一個新的陣列,然後呼叫 System.arraycopy 拷貝陣列,最後返回新的陣列。

    public boolean add(E e) {
        // 當呼叫了無參構造,設定大小為10
        ensureCapacityInternal(size + 1);  // Increments modCount        
        elementData[size++] = e;
        return true;
    }

    private void ensureCapacityInternal(int minCapacity) {
        // 如果當前陣列是預設空陣列就設定為 10和 size+1中的最小值
        // 這也就是說為什麼說無參構造 new 的陣列大小是 10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

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

        // 若使用者指定的最小容量 > 最小擴充容量,則以使用者指定的為準,否則還是 10
        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;
        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);
    }

3. Add(int index, E e) 方法

   這個方法比較簡單和上面基本一樣,然後只是最後放元素的時候的操作不一樣,他是採用了 System.arraycopy 從自己向自己拷貝,目的就在於覆蓋元素。 注意一個規律這裡面只要涉及下標的操作的很多不是自己手寫 for 迴圈而是採用類似的拷貝覆蓋的方法。算是一個小技巧。

public void add(int index, E element) {
        rangeCheckForAdd(index);
        ensureCapacityInternal(size + 1);  // Increments modCount
        // 覆蓋
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

4. remove(int index)

  同理這裡面還是用了拷貝覆蓋的技巧。 但是有一點注意的就是不用的節點需要手動的觸發 gc ,這也是在 Efftive Java 中作者舉的一個例子。

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

5. remove(E e)

   這個方法操作很顯然會判斷 e 是不是 null 如果是 null 的話直接採用 == 比較,否則的話就直接呼叫 equals 方法然後執行拷貝覆蓋。

public boolean remove(Object o) {
        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++)
                // 呼叫 equals 方法
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

6. clear()

   這個方法就幹了一件事,把陣列中的引用全都設定為 null 以便 gc 。而不是僅僅把 size 設定為 0 。

// gc 所有節點
    public void clear() {
        modCount++;
        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;
        size = 0;
    }

7. addAll(Collection e)

   這個沒啥好說的就是,採用轉陣列然後 copy

    // 一個套路 只要涉及到 Collection介面的方法都是把這個介面轉成一個陣列然後對陣列操作
    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;
    }

4. 訪問方法分析

1. get

   直接訪問陣列下標。

    // 沒啥好說的直接去找陣列下標
    public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }
    

2. subList

   這個方法的實現比較有意思,他不是直接擷取一個新的 List 返回,而是在這個類的內部還有一個 subList 的內部類,然後這個類就記錄了 subList 的開始結束下標,然後返回的是這個 subList 物件。你可能會想返回的 subList 他不是 List 不會有問題嗎,這裡這個 subList 是繼承的 AbstractList 所以還是正確的。

public List<E> subList(int fromIndex, int toIndex) {
        subListRangeCheck(fromIndex, toIndex, size);
        return new SubList(this, 0, fromIndex, toIndex);
    }
    // subList 返回的是一個位置標記例項,就是在原來的陣列上放了一些標誌,沒有修改或者拷貝新的空間
private class SubList extends AbstractList<E> implements RandomAccess {
        private final AbstractList<E> parent;
        private final int parentOffset;
        private final int offset;
        int size;
        // other functions .....
     }

5. 其他功能方法

1. write/readObject

  前面在介紹資料域的時候我就有標註 elementData 是一個 transition 的變數也就是在自動序列化的時候會忽略這個欄位。

   然後我們又在原始碼中找到到了 write/readObject 方法,這兩個方法是用來序列化 elementData 中的每一個元素,也就是手動的對這個欄位進行序列化和反序列化。這不是多此一舉嗎?

   既然要將ArrayList的欄位序列化(即將elementData序列化),那為什麼又要用transient修飾elementData呢?

   回想ArrayList的自動擴容機制,elementData陣列相當於容器,當容器不足時就會再擴充容量,但是容器的容量往往都是大於或者等於ArrayList所存元素的個數。

   比如,現在實際有了8個元素,那麼elementData陣列的容量可能是8x1.5=12,如果直接序列化elementData陣列,那麼就會浪費4個元素的空間,特別是當元素個數非常多時,這種浪費是非常不合算的。

   所以ArrayList的設計者將elementData設計為transient,然後在writeObject方法中手動將其序列化,並且只序列化了實際儲存的那些元素,而不是整個陣列。

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

2. fast-fail

   所謂的 fast-fail 就是在我們進行 iterator 遍歷的時候不允許呼叫 Collection 介面的方法進行對容器修改,否則就會拋異常。這個實現的機制是在 iterator 中維護了兩個變數,分別是 modCountexpectedModCount 由於 Collection 介面的方法在每次修改操作的時候都會對 modCount++ 所以如果在 iterator 中檢測到他們不相等的時候就拋異常。

private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;
        
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
   }

3. forEach

   這個是一個函數語言程式設計的方法,看看他的引數 forEach(Consumer<? super E> action) 很有意思裡面接受是一個函式式的介面,我們裡面回撥了 Consumeraccept 所以我們只需要傳入一個函式介面就能對每一個元素處理。

    @Override
    public void forEach(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        final int expectedModCount = modCount;
        @SuppressWarnings("unchecked")
        final E[] elementData = (E[]) this.elementData;
        final int size = this.size;
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            //回撥
            action.accept(elementData[i]);
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

寫了一段測試程式碼,但是這個方法不常用,主要是 Collection 是可以自己生成 Stream 物件,然後呼叫上面的方法即可。這裡提一下。

public class ArrayListTest {

    @Test
    public void foreach() {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(2);
        list.add(1);
        list.add(4);
        list.add(6);
        list.forEach(System.out::print);  //列印每一次元素。
    }
}

4. sort

底層呼叫了 Arrays.sort 方法沒什麼好說的。

public void sort(Comparator<? super E> c) {
        final int expectedModCount = modCount;
        Arrays.sort((E[]) elementData, 0, size, c);
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }

5. removeIf

   這個和 forEach 差不多,就是回撥寫好了。

6. Vector

以上基本是把 ArrayList 的重要的方法和屬性介紹完了,我們已經比較清楚他底層的實現和資料結構了。然後提到 ArrayList 自然也少不了一個比較古老的容器 Vector 這個容器真的和 ArrayList 太像了。因為你會發現他們連繼承和實現的介面都是一樣的。但是也會有一些不同的地方,下面分條介紹一下。

  1. Vector 中基本所有的方法都是 synchronized 的方法,所以說他是執行緒安全的 ArrayList

  2. 構造方法不一樣,在屬性中沒有兩個比較特殊的常量,所以說他的構造方法直接初始化一個容量為 10 的陣列。然後他有四個構造方法。

  3. 遍歷的介面不一樣。他還是有 iterator 的但是他以前的遍歷的方法是 Enumeration 介面,通過 elements 獲取 Enumeration 然後使用 hasMoreElementsnextElement 獲取元素。

  4. 缺少一些函數語言程式設計的方法。

相關文章