ArrayList原始碼剖析與程式碼實測

Yuasin發表於2020-09-09

ArrayList原始碼剖析與程式碼實測(基於OpenJdk14)

  • 寫本篇部落格的目的在於讓自己能夠更加了解Java的容器與實現,能夠掌握原始碼的一些實現與思想,選擇從ArrayList入手是因為ArrayList相對來說是實現較為簡單的容器,底層實現依賴與陣列,將ArrayList整理清楚便於之後理解實現更復雜的容器和執行緒安全容器
  • 不同JDK的原始碼實現會有區別,本篇部落格基於OpenJdk14進行原始碼分析
  • 本篇部落格除了剖析原始碼以外還將討論Java中的fail-fast機制

繼承關係

image-20200908180018052
  • ArrayList實現List介面,而繼承的AbstractList類也實現了List介面,為什麼要實現兩次List介面呢?詳見:https://stackoverflow.com/questions/2165204/why-does-linkedhashsete-extend-hashsete-and-implement-sete
  • List介面定義了方法,但不進行實現(JDK1.8後介面可以實現default方法,List類中就有體現),我們要實現自己特定的列表時,不需要通過實現List介面去重寫所有方法,AbstractList抽象類替我們實現了很多通用的方法,我們只要繼承AbstractList並根據需求修改部分即可

從建構函式開始

  • 使用一個容器當然要從容器的構造開始,ArrayList過載了三種建構函式

  • 日常中最常使用的是無引數建構函式,使用另一個ArrayList來構造新的ArrayList在諸如回溯演算法中也很常見。

    public ArrayList()
    public ArrayList(int initialCapacity)
    public ArrayList(Collection<? extends E> c) 
    
  • 無參建構函式中將elementData 賦值為DEFAULTCAPACITY_EMPTY_ELEMENTDATA(即空陣列),其中elementData就是ArrayList存放元素的真實位置。也可以在初始化時將容器容量確定為傳入的int引數。

//類中定義的變數
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access,如果是私有變數,在內部類中獲取會比較麻煩

//無參構造
public ArrayList() {
  this.elementData = DEFAULTCAPACITY_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);
  }
}
  • 如果使用已有容器來構造ArrayList,則新的容器必須實現Collection介面,且其中的泛型 ? 需要是ArrayList泛型引數E的子類(或相同)。由於每個容器的toArray()方法實現可能不同,返回值不一定為Object[],即elementData的型別會發生變化(例子見ClassTypeTest.java)。所以需要進行型別判斷,若elementData.getClass() != Object[].class則使用Arrays工具類中的copyOf方法將elementData的型別改回。
public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // defend against c.toArray (incorrectly) not returning Object[]
            // (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
//ClassTypeTest.java
public class ClassTypeTest {
    public class Person{ }
    public class Student extends Person{ }
    public static void main(String[] args) {
        Person[] p = new Student[5];
        System.out.println(p.getClass());
    }
}
//output:
//class [LClassTypeTest$Student;

從add方法深入 / 陣列的擴容

  • 容器的本質無非是替我們保管一些我們需要儲存的資料(基本資料型別、物件),我們可以往容器里加入,也可以從容器裡獲取,也可以刪除容器內元素。使用容器而不是陣列是因為陣列對於我們使用來說過於不便利

    • 無法動態改變陣列大小
    • 陣列元素刪除和插入需要移動整個陣列
  • ArrayList容器底層是基於陣列實現,但是我們使用的時候卻不需要關心陣列越界的問題,是因為ArrayList實現了陣列的動態擴容,從add方法出發檢視ArrayList是怎麼實現的

ArrayList原始碼

  • 可以看到add方法的呼叫鏈如上,ArrayList提供了兩個add方法,可以直接往列表尾部新增,或者是在指定位置新增。elementData陣列擴容操作開始於 add方法,當grow()返回擴容後的陣列,add方法在這個陣列上進行新增(插入)操作。在add方法中看到的modCount變數涉及 Java 的 fail-fast 機制,將在本文後面進行講解
//size是ArrayList實際新增的元素的數量,elementData.length為ArrayList能最多容納多少元素的容量
//通過程式碼可以看出,當size==elementData.length時,容器無法再放入元素,所以此時需要一個新的、更大的elementData陣列
private int size;

public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }
private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }
  • 當擴容發生時,要求容器需要至少能多放置 minCapacity 個元素(即容量比原來至少大minCapacity
private static final int DEFAULT_CAPACITY = 10;

private Object[] grow() {
  return grow(size + 1);
}
private Object[] grow(int minCapacity) {
        int oldCapacity = elementData.length;
        if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            int newCapacity = ArraysSupport.newLength(oldCapacity,
                    minCapacity - oldCapacity, /* minimum growth */
                    oldCapacity >> 1           /* preferred growth */);
            return elementData = Arrays.copyOf(elementData, newCapacity);
        } else { 
        		// 當oldCapacity==0 || elementData==DEFAULTCAPACITY_EMPTY_ELEMENTDATA 時進入該分支
          	// 即容器使用無參建構函式 或 new ArrayList(0)等情況時進入
          	// elementData陣列大小被擴容為 10
            return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
        }
    }
  • 通常情況下prefGrowth=oldCapacity/2,由此處可看出大部分情況下擴容後的陣列大小為原陣列的1.5倍
    • 擴容後的陣列大小為原來的1.5倍,可能存在越界情況,此處使用 newLength - MAX_ARRAY_LENGTH <= 0 進行判斷,不能使用 newLength <= MAX_ARRAY_LENGTH 進行判斷,如果 newLength 超過 2147483647 ,會溢位為負值,此時newLength依舊小於MAX_ARRAY_LENGTH。而用newLength - MAX_ARRAY_LENGTH <= 0 則是相當於將newLength這個數字在“int環”上向左移動了MAX_ARRAY_LENGTH位,若這個數字此時為負數(即落在綠色區域),則直接返回當前newLength,否則進入hugeLength方法。
    • Integer
    • 在hugeLength中,當老容量已經達到 2147483647 時,需求的最小新容量加一則溢位,此時丟擲異常
public static final int MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8;

public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
  // assert oldLength >= 0
  // assert minGrowth > 0

  int newLength = Math.max(minGrowth, prefGrowth) + oldLength;
  //!!! 判斷陣列大小是否超過int值允許的大小 
  if (newLength - MAX_ARRAY_LENGTH <= 0) {
    return newLength;
  }
  return hugeLength(oldLength, minGrowth);
}

private static int hugeLength(int oldLength, int minGrowth) {
  int minLength = oldLength + minGrowth;
  if (minLength < 0) { // overflow
    throw new OutOfMemoryError("Required array length too large");
  }
  if (minLength <= MAX_ARRAY_LENGTH) {
    return MAX_ARRAY_LENGTH;
  }
  return Integer.MAX_VALUE;
}
  • 除了add方法,還有public boolean addAll(Collection<? extends E> c)方法以及它的過載public boolean addAll(int index, Collection<? extends E> c)方法

其他的刪查改方法

  • 因為是基於陣列的容器,其他一些刪查改的方法都比較簡單,基本上就是在陣列上操作,此處就不一一展開
//刪除元素:
public E remove(int index) 
public boolean remove(Object o)
public boolean removeAll(Collection<?> c)
boolean removeIf(Predicate<? super E> filter, int i, final int end)
public void clear()

//修改元素:
public E set(int index, E element)
public void replaceAll(UnaryOperator<E> operator)

//查詢/獲得元素:
public E get(int index)
public int indexOf(Object o) 
public List<E> subList(int fromIndex, int toIndex)

modCount與fail-fast機制

根據官方文件的描述,ArrayList是一個非執行緒安全的容器,兩個執行緒可以同時對一個ArrayList進行讀、寫操作。通常來說對封裝了ArrayList的類進行了同步操作後就能確保執行緒安全。

Note that this implementation is not synchronized. If multiple threads access an ArrayList instance concurrently, and at least one of the threads modifies the list structurally, it must be synchronized externally. (A structural modification is any operation that adds or deletes one or more elements,or explicitly resizes the backing array; merely setting the value of an element is not a structural modification.)

當然,ArrayList實現中也通過fail-fast確保了不正確的多執行緒操作會盡快的丟擲錯誤,防止Bug隱藏在程式中直到未來的某一天被發現。

  • fail-fast機制的實現依賴變數 modCount,該變數在ArrayList執行結構性的修改(structural modification)時會 +1,如add、remove、clear等改變容器size的方法,而在set方法中不自增變數(但令人迷惑的是replaceAll和sort方法卻會修改modCount的值,總結來說不應該依賴modCount實現的fail-fast機制)
//java.util.AbstractList.java
protected transient int modCount = 0;
  • equals方法就使用到了fail-fast,將modCount賦值給一個expectedModCount變數,在對兩個容器內的元素一一進行完比較判斷後得出兩個物件是否相等的判斷,但在返回判斷之前要問一個問題,在對比判斷的過程中當前這個ArrayList(this)有沒有被其他人(執行緒)動過?所以加了一個checkForComodification方法進行判斷,如果modCount與原先不同則代表該ArrayList經過改動,則equals的判斷結果並不可信,丟擲throw new ConcurrentModificationException()異常
//java.util.ArrayList.java
public boolean equals(Object o) {
        if (o == this) {
            return true;
        }

        if (!(o instanceof List)) {
            return false;
        }

        final int expectedModCount = modCount;
        // ArrayList can be subclassed and given arbitrary behavior, but we can
        // still deal with the common case where o is ArrayList precisely
        boolean equal = (o.getClass() == ArrayList.class)
            ? equalsArrayList((ArrayList<?>) o)
            : equalsRange((List<?>) o, 0, size);

        checkForComodification(expectedModCount);
        return equal;
}

private void checkForComodification(final int expectedModCount) {
  if (modCount != expectedModCount) {
    throw new ConcurrentModificationException();
  }
}

我使用程式碼模擬了在使用迭代器的情況下throw new ConcurrentModificationException()的丟擲

public class failFastTest_02 {
    
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        List<Integer> list = new ArrayList<>();
        int changeIndex = 5;
        for(int i=0;i<10;i++){
            list.add(i);
        }

        Iterator iterator = list.iterator();

        //反射獲取expectedModCount
        Field field = iterator.getClass().getDeclaredField("expectedModCount");
        field.setAccessible(true);

        //反射獲取modCount
        Class<?> l = list.getClass();
        l = l.getSuperclass();
        Field fieldList = l.getDeclaredField("modCount");
        fieldList.setAccessible(true);

        while(iterator.hasNext()){
            if(changeIndex==0){
                list.add(-42);
            }
            System.out.println("Value of expectedModCount:" + field.get(iterator));
            System.out.println("Value of modCount:" + fieldList.get(list));
            System.out.println("iterator get element in list  "+ iterator.next());
            changeIndex--;
        }
    }
}

getClass()方法來獲取類的定義資訊,通過定義資訊再呼叫getFields()方法來獲取類的所有公共屬性,或者呼叫getDeclaredFields()方法來獲取類的所有屬性,包括公共,保護,私有,預設的方法。但是這裡有一點要注意的是這個方法只能獲取當前類裡面顯示定義的屬性,不能獲取到父類或者父類的父類及更高層次的屬性的。使用Class.getSuperClass()獲取父類後再獲取父類的屬性。

  • 可以看到,在迭代器初始化後,迭代器中的expectedModCount不會因為ArrayList方法對列表的修改而改變,在這之後對於該列表(ArrayList)的結構性修改都會導致異常的丟擲,這確保了迭代器不會出錯(迭代器使用 cursor維護狀態,當外界的結構變化時 size改變,不使用fail-fast public boolean hasNext() {return cursor != size;}可能會產生錯誤結果),如果想在使用迭代器時修改列表,應該使用迭代器自帶的方法。上述程式碼報錯如下。
image-20200909223232857
  • 插一句題外話, cursor顧名思義跟游標一樣,讀取一個元素後要將游標向後移動一格,刪除一個元素則是將游標前的一個元素刪除,此時游標隨之退後一格。當然,ArrayList迭代器不能一直退格(remove),必須要先能讀取一個元素然後才能將其刪除

總結

  • ArrayList底層基於陣列實現,元素存放在elementData陣列中,使用無參建構函式時,加入第一個元素後elementData陣列大小為10。
  • new ArrayList<>().size()為列表儲存真實元素個數,不為列表容量
  • 正常情況下每次擴容後,容量為原先的1.5倍
  • ArrayList中還有內部類Itr、ListItr、SubList、ArrayListSpliterator,其中Itr、ListItr為迭代器,SubList是一個很神奇的實現,方便某些ArrayList方法的使用,對於SubList的非結構性修改會對映到ArrayList上。關於這幾個內部類的內容,或許之後還會在該部落格內繼續更新

參考

fail-fast相關:https://www.cnblogs.com/goody9807/p/6432904.html

https://baijiahao.baidu.com/s?id=1638201147057831295&wfr=spider&for=pc

內部類訪問外部類私有變數:https://blog.csdn.net/qq_33330687/article/details/77915345

相關文章