ArrayList源碼剖析與代碼實測

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重載了三種構造函數html

  • 平常中最常使用的是無參數構造函數,使用另外一個ArrayList來構造新的ArrayList在諸如回溯算法中也很常見。java

    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.)ide

固然,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相關:http://www.javashuo.com/article/p-kutmancs-ne.html

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

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

相關文章
相關標籤/搜索