Java ArrayList原始碼分析(含擴容機制等重點問題分析)

BWH_Steven發表於2021-02-06

寫在最前面

這個專案是從20年末就立好的 flag,經過幾年的學習,回過頭再去看很多知識點又有新的理解。所以趁著找實習的準備,結合以前的學習儲備,建立一個主要針對應屆生和初學者的 Java 開源知識專案,專注 Java 後端面試題 + 解析 + 重點知識詳解 + 精選文章的開源專案,希望它能伴隨你我一直進步!

說明:此專案內容參考了諸多博主(已註明出處),資料,N本書籍,以及結合自己理解,重新繪圖,重新組織語言等等所制。個人之力綿薄,或有不足之處,在所難免,但更新/完善會一直進行。大家的每一個 Star 都是對我的鼓勵 !希望大家能喜歡。

注:所有涉及圖片未使用網路圖床,文章等均開源提供給大家。

專案名: Java-Ideal-Interview

Github 地址: Java-Ideal-Interview - Github

Gitee 地址:Java-Ideal-Interview - Gitee(碼雲)

持續更新中,線上閱讀將會在後期提供,若認為 Gitee 或 Github 閱讀不便,可克隆到本地配合 Typora 等編輯器舒適閱讀

若 Github 克隆速度過慢,可選擇使用國內 Gitee 倉庫

一 ArrayList 原始碼分析(含擴容機制分析)

1. ArrayList 概述

1.1 List 是什麼?

List 在 Collection中充當著一個什麼樣的身份呢?——有序的 collection(也稱為序列)

實現這個介面的使用者以對列表中每個元素的插入位置進行精確地控制。使用者可以根據元素的整數索引(在列表中的位置)訪問元素,並搜尋列表中的元素。與 set 不同,列表通常允許重複的元素。

1.2 ArrayList 是什麼?

ArrayList 的底層就是一個陣列,依賴其擴容機制(後面會提到)它能夠實現容量的動態增長,所以 ArrayList 就是資料結構中順序表的一種具體實現。

其特點為:查詢快,增刪慢,執行緒不安全,效率高。

1.3 順序表的優缺點

優點:

  1. 邏輯與物理順序一致,順序表能夠按照下標直接快速的存取元素
  2. 無須為了表示表中元素之間的邏輯關係而增加額外的儲存空間

缺點:

  1. 線性表長度需要初始定義,常常難以確定儲存空間的容量,所以只能以降低效率的代價使用擴容機制

  2. 插入和刪除操作需要移動大量的元素,效率較低

1.4 時間複雜度證明

讀取

還記的這個公式嗎?

$$Loc(a_i) = Loc(a_1) + (i -1)*L$$

通過這個公式我們可以在任何時候計算出線性表中任意位置的地址,並且對於計算機所使用的時間都是相同的,即一個常數,這也就意味著,它的時間複雜度為 O(1)

插入和刪除

我們以插入為例子

  • 首先最好的情況是這樣的,元素在末尾的位置插入,這樣無論該元素進行什麼操作,均不會對其他元素產生什麼影響,所以它的時間複雜度為 O(1)

  • 那麼最壞的情況又是這樣的,元素正好插入到第一個位置上,這就意味著後面的所有元素全部需要移動一個位置,所以時間複雜度為 O(n)

  • 平均的情況呢,由於在每一個位置插入的概率都是相同的,而插入越靠前移動的元素越多,所以平均情況就與中間那個值的一定次數相等,為 (n - 1) / 2 ,平均時間複雜度還是 O(n)

總結

讀取資料的時候,它的時間複雜度為 O(1),插入和刪除資料的時候,它的時間複雜度為 O(n),所以線性表中的順序表更加適合處理一些元素個數比較穩定,查詢讀取多的問題

2. 核心原始碼分析

2.1 類宣告

先來看一下類的宣告,有一個繼承(抽象類)和四個介面關係

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{ 
    // 原始碼具體內容... 
}
  • RandomAccess 是一個標誌介面(Marker)只要 List 集合實現這個介面,就能支援快速隨機訪問(通過元素序號快速獲取元素物件 —— get(int index)

  • Cloneable :實現它就可以進行克隆(clone()

  • java.io.Serializable :實現它意味著支援序列化,滿足了序列化傳輸的條件

2.2 類成員

下面接著看一些成員屬性

// 序列化自動生成的一個碼,用來在正反序列化中驗證版本一致性。
private static final long serialVersionUID = 8683452581122892189L;

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

/**
 * 指定 ArrayList 容量為0(空例項)時,返回此空陣列
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * 與 EMPTY_ELEMENTDATA 的區別是,它是預設返回的,而前者是使用者指定容量為 0 才返回
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * 具體存放元素的陣列
 * 儲存新增到 ArrayList 中的元素資料(第一次新增元素時,會擴容到 DEFAULT_CAPACITY = 10 ) 
 */
transient Object[] elementData; // non-private to simplify nested class access

/**
 * ArrayList 實際所含元素個數(大小)
 */
private int size;

2.4 構造方法

/**
 * 帶參建構函式,引數為使用者指定的初始容量
 */
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // 引數大於0,建立 initialCapacity 大小的陣列
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        // 引數為0,建立空陣列(成員中有定義)
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        // 其他情況,直接拋異常
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

/**
 * 預設無參建構函式,初始值為 0
 * 也說明 DEFAULT_CAPACITY = 10 這個容量
 * 不是在建構函式初始化的時候設定的(而是在新增第一個元素的時候)
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/**
 * 構造一個包含指定 collection 的元素的列表
 * 這些元素是按照該 collection 的迭代器返回它們的順序排列的。
 */
public ArrayList(Collection<? extends E> c) {
    // 將給定的集合轉成陣列
    elementData = c.toArray();
    // 如果陣列長度不為 0
    if ((size = elementData.length) != 0) {
        // elementData 如果不是 Object 型別的資料,返回的就不是 Object 型別的陣列
        if (elementData.getClass() != Object[].class)
            // 將不是 Object 型別的 elementData 陣列,賦值給一個新的 Object 型別的陣列
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 陣列長度為 0 ,用空陣列代替
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

2.5 最小化例項容量方法

/**
 * 最小化例項容量方法,可以根據實際元素個數,將陣列容量優化,防止浪費
 */
public void trimToSize() {
    modCount++;
    // 陣列容量大於實際元素個數(例如10個元素,卻有15個容量)
    if (size < elementData.length) {
        // 根據元素實際個數,重新最小化例項容量
        elementData = (size == 0)
            ? EMPTY_ELEMENTDATA
            : Arrays.copyOf(elementData, size);
    }
}

2.5 擴容方法

這裡只是按照順序介紹,後面還會專門針對擴容進行一個分析

/**
 * 增加ArrayList例項的容量,如果有必要,確保它至少可以儲存由最小容量引數指定的元素數量。
 */
public void ensureCapacity(int minCapacity) {
    //如果元素陣列不為預設的空,則 minExpand 的值為0,反之值為10
    int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
        // any size if not default element table
        ? 0
        // larger than default for default empty table. It's already
        // supposed to be at default size.
        : DEFAULT_CAPACITY;
    // 如果最小容量大於已有的最大容量
    if (minCapacity > minExpand) {
        ensureExplicitCapacity(minCapacity);
    }
}

/**
 * 計算最小擴容量(被呼叫)
 */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
     // 如果元素陣列為預設的空
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 獲取“預設的容量”和“傳入引數 minCapacity ”兩者之間的最大值
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

/**
 * 得到最小擴容量
 */
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}


/**
 * 判斷是否需要擴容
 */
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    // 如果最小容量比陣列的長度還大
    if (minCapacity - elementData.length > 0)
        // 就呼叫grow方法進行擴容
        grow(minCapacity);
}

/**
 * 要分配的最大陣列大小
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * ArrayList 擴容的核心方法
 */
private void grow(int minCapacity) {
    // 將當前元素陣列長度定義為 oldCapacity 舊容量
    int oldCapacity = elementData.length;
    // 新容量更新為舊容量的1.5倍
    // oldCapacity >> 1 為按位右移一位,相當於 oldCapacity 除以2的1次冪
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 然後檢查新容量是否大於最小需要容量,若還小,就把最小需要容量當作陣列的新容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 再檢查新容量是否超出了ArrayList 所定義的最大容量
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        // 若超出,則呼叫hugeCapacity()
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}
	
/**
 * 比較minCapacity和 MAX_ARRAY_SIZE
 */
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
}

2.6 常規方法

/**
 * 返回元素數量
 */
public int size() {
    return size;
}

/**
 * 此列表元素數量為 0 則返回 true
 */
public boolean isEmpty() {
    return size == 0;
}

/**
 * 此列表含有指定元素,則返回true
 */
public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

/**
 * 返回此列表中元素首次出現位置的索引
 * 若不包含此元素,則返回 -1
 */
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        // 本質就是迴圈 equals 比對
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

/**
 * 返回此列表中指定元素的最後一次出現的索引
 * 如果此列表不包含元素,則返回 -1
 */
public int lastIndexOf(Object o) {
    if (o == null) {
        for (int i = size-1; i >= 0; i--)
            if (elementData[i]==null)
                return i;
    } else {
        // 逆向迴圈 equals 比對
        for (int i = size-1; i >= 0; i--)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}


/**
 * 返回 ArrayList 例項的淺拷貝
 */
public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        // 實現陣列的複製,引數為被複制者的引數
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

/**
 * 返回一個包含此列表中所有元素的陣列(理解為將集合轉為陣列即可)
 */
public Object[] toArray() {
    return Arrays.copyOf(elementData, size);
}

/**
 * 將list轉化為你所需要型別的陣列,然後返回
 */
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    // 複製用法,下面專題會講解此內容
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

// Positional Access Operations

@SuppressWarnings("unchecked")
E elementData(int index) {
    return (E) elementData[index];
}

/**
 * 返回此列表中指定位置的元素。
 */
public E get(int index) {
    // index 範圍檢查
    rangeCheck(index);
    return elementData(index);
}

/**
 * 用指定的元素替換此列表中指定位置的元素。
 */
public E set(int index, E element) {
    // index 範圍檢查
    rangeCheck(index);
	// 根據 index 找到想替換的舊元素
    E oldValue = elementData(index);
    // 替換元素
    elementData[index] = element;
    return oldValue;
}

/**
 * 將指定的元素追加到此列表的末尾。
 */
public boolean add(E e) {
    // 確認 list 容量,嘗試容量加 1,看看有無必要擴容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 賦值
    elementData[size++] = e;
    return true;
}

/**
 * 在此列表中的指定位置插入指定的元素
 * 再將從index開始之後的所有成員後移一個位置;將element插入index位置;最後size加1。
 */
public void add(int index, E element) {
    // 呼叫 rangeCheckForAdd 對 index 進行範圍檢查
    rangeCheckForAdd(index);
	// 保證容量足夠
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 自己複製自己,然後達到 index 之後全部元素向後挪一位的效果
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 然後將 index 賦值為指定的元素
    elementData[index] = element;
    size++;
}

/**
 * 移除該列表中指定位置的元素。 將任何後續元素移動到左側(從其索引中減去一個元素)。
 */
public E remove(int index) {
    // 呼叫 rangeCheckForAdd 對 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;
}

/**
 * 從集合中移除第一次出現的指定元素
 */
public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        // 也很簡單,就是一個迴圈 equals 判斷,然後移除
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

/*
 * 跳過範圍檢查的刪除方式,與remove(Object o)相同
 */
private void fastRemove(int index) {
    modCount++;
    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
}

/**
 * 從列表中刪除所有元素。
 */
public void clear() {
	modCount++;
    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        // 元素全部設為 null
        elementData[i] = null;
    // 長度設為 0
    size = 0;
}

/**
 * 按指定集合的Iterator返回的順序
 * 將指定集合中的所有元素追加到此列表的末尾。
 */
public boolean addAll(Collection<? extends E> c) {
    // 轉為陣列
    Object[] a = c.toArray();
    // 拿到待新增指定陣列的長度
    int numNew = a.length;
    // 確認 list 容量,嘗試容量加上 numNew,看看有無必要擴容
    ensureCapacityInternal(size + numNew);  // Increments modCount
    // 利用 arraycopy 指定陣列a的元素追加到當前陣列 elementData 後
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

/**
 * 按指定集合的Iterator返回的順序
 * 將指定集合中的所有元素新增到此列表中,從指定位置開始
 * 
 */
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)
        // 實現元素指定位置的插入,本質還是 arraycopy 自身
        System.arraycopy(elementData, index, elementData, index + numNew,
                         numMoved);

    System.arraycopy(a, 0, elementData, index, numNew);
    size += numNew;
    return numNew != 0;
}

/**
 * 刪除指定索引範圍內的元素(fromIndex - toIndex)
 * 將任何後續元素移動到左側(減少其索引)。
 */
protected void removeRange(int fromIndex, int toIndex) {
    modCount++;
    int numMoved = size - toIndex;
    System.arraycopy(elementData, toIndex, elementData, fromIndex,
                     numMoved);

    // clear to let GC do its work
    int newSize = size - (toIndex-fromIndex);
    for (int i = newSize; i < size; i++) {
        elementData[i] = null;
    }
    size = newSize;
}

/**
 * 檢查給定的索引是否在範圍內。
 */
private void rangeCheck(int index) {
    // 下標越界就直接拋異常
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

/**
 * 另一個版本,針對add 和 addAll使用
 */
private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

/**
 * 與上面套娃使用
 */
private String outOfBoundsMsg(int index) {
    return "Index: "+index+", Size: "+size;
}

/**
 * 從此列表中刪除指定集合中包含的所有元素。
 */
public boolean removeAll(Collection<?> c) {
    Objects.requireNonNull(c);
    return batchRemove(c, false);
}

/**
 * 僅保留此列表中包含在指定集合中的元素。即刪掉沒有的部分
 */
public boolean retainAll(Collection<?> c) {
    Objects.requireNonNull(c);
    return batchRemove(c, true);
}

/**
 * 刪除的具體邏輯,下面會有專題講解
 */
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++)
            // 通過迴圈判斷陣列中有沒有指定陣列中的每一個值,complement 是引數傳遞的
            if (c.contains(elementData[r]) == complement)
                // 就將原陣列的r位置的資料覆蓋掉w位置的資料
                // r位置的資料不變,並其w自增,r自增
                // 否則,r自增,w不自增
                // 本質:把需要移除的資料都替換掉,不需要移除的資料前移
                elementData[w++] = elementData[r];
    } finally {
        // Preserve behavioral compatibility with AbstractCollection,
        // even if c.contains() throws.
        if (r != size) {
            System.arraycopy(elementData, r,
                             elementData, w,
                             size - r);
            w += size - r;
        }
        if (w != size) {
            // clear to let GC do its work
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            size = w;
            modified = true;
        }
    }
    return modified;
}

// writeObject readObject 序列化相關的省略
   
/**
 * 列表迭代器:List集合特有的迭代器
 */
public ListIterator<E> listIterator(int index) {
    if (index < 0 || index > size)
        throw new IndexOutOfBoundsException("Index: "+index);
    return new ListItr(index);
}


public ListIterator<E> listIterator() {
    return new ListItr(0);
}

// foreach 遍歷等同於 iterator
public Iterator<E> iterator() {
    return new Itr();
}


private class Itr implements Iterator<E> {
    // 下一個要訪問的元素下標
    int cursor; 
    // 上一個要訪問的元素下標
    int lastRet = -1; 
    // 代表對 ArrayList 修改次數的期望值,初始值為 modCount
    int expectedModCount = modCount;

    Itr() {}

    // 下標如果
    public boolean hasNext() {
        return cursor != size;
    }

    /**
     * 剛開始cursor = 0,lastRet = -1
     * 整個過程結束 cursor 和 lastRet 都會自增 1
     */
    @SuppressWarnings("unchecked")
    public E next() {
        // 跳轉本質是判斷 modCount 是否等於 expectedModCount
        checkForComodification();
        int i = cursor;
       // 判斷 cursor 是否超過集合大小和陣列長度
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        // 將 cursor 賦值給 lastRet,然後把此下標處的元素返回
        return (E) elementData[lastRet = i];
    }

    public void remove() {
        // 先判斷 lastRet 的值是否小於 0
        if (lastRet < 0)
            throw new IllegalStateException();
        // 跳轉本質是判斷 modCount 是否等於 expectedModCount
        checkForComodification();

        try {
            // 直接呼叫 ArrayList 的 remove 方法刪除下標為 lastRet 的元素
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
    
    // forEachRemaining 略

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


3. 重點內容分析

3.1 擴容機制再分析

3.1.1 ArrayList 是如何被初始化的

ArrayList 提供了 1 個無參構造和 2 個帶參構造來初始化 ArrayList ,我們在建立 ArrayList 時,經常使用無參構造的方式,其本質就是初始化了一個空陣列,直到向陣列內真的新增元素的時候才會真的去分配容量。例如:向陣列中新增第一個元素,陣列容量擴充為 10

補充:JDK7 無參構造 初始化 ArrayList 物件時,直接建立了長度是 10 的 Object[] 陣列elementData

3.1.2 擴容機制流程分析(無參構造為例)

3.1.2.1 add()

一般來說,都是通過 add 方法觸發擴容機制,我們拿最簡單的尾部追加的 add() 方法舉例

/**
 * 將指定的元素追加到此列表的末尾。
 */
public boolean add(E e) {
    // 確認 list 容量,嘗試容量加 1,看看有無必要擴容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 賦值
    elementData[size++] = e;
    return true;
}

核心要點就這一句 ensureCapacityInternal(size + 1);

3.1.2.2 ensureCapacityInternal()

追蹤進去

/**
 * 得到最小擴容量
 */
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

方法內呼叫了 ensureExplicitCapacity() 方法,引數是 calculateCapacity(elementData, minCapacity)

先來分析一下這個引數的結果是什麼,聚焦到 calculateCapacity() 方法中去

3.1.2.3 calculateCapacity()
/**
 * 計算最小擴容量(被呼叫)
 */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
     // 如果元素陣列為預設的空
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 獲取“預設的容量”和“傳入引數 minCapacity ”兩者之間的最大值
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

也很簡單,就是為了計算出一個最小擴容量,當元素為初次初始化時,陣列還沒進過擴容,是一個空陣列,所以會走 if 這個判斷,而且當時傳入的 size + 1 也就是 minCapacity 的值為 0 + 1 = 1 ,經過一個取大值的操作,與預設的 DEFAULT_CAPACITY 進行比對,自然返回的就是 10。

如果陣列已經不是為空了,就直接返回一個 minCapacity (size + 1)就可以了

3.1.2.4 ensureExplicitCapacity

ensureCapacityInternal 方法內呼叫了 ensureExplicitCapacity(引數已經計算出來了) 方法

繼續去看它

/**
 * 判斷是否需要擴容
 */
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    // 如果最小容量比陣列的長度還大
    if (minCapacity - elementData.length > 0)
        // 就呼叫grow方法進行擴容
        grow(minCapacity);
}

此方法的核心就是 if 判斷這個陣列需不需要擴容,可以分為三種情況

  • add 第 1 個元素時:此時陣列還只是一個被初始化過的空陣列,minCapacity 經過 calculateCapacity 計算會返回 DEFAULT_CAPACITY 的預設值 10,而 elementData.length 也自然是 0,所以 minCapacity - elementData.length > 0 是成立的,直接進入 grow(minCapacity); 開始擴容。

  • add 第 2 到 10 個元素的時候(以 2 舉例):此時 minCapacity = size + 1 = 1 + 1 = 2 ,而 elementData.length 已經在新增第 1 個元素後等於 10 了。所以 minCapacity - elementData.length > 0 就不成立了,所以不會進入 grow(minCapacity); ,也不會擴容

    • 新增第 3 ... 10 個元素的時候,都是一樣的。
  • add 第 11 個元素的時候,minCapacity 變成了 11,比 10 還要大,所以又一次進去擴容了

3.1.2.5 grow()

這裡是真正去執行擴容邏輯的程式碼

/**
 * 要分配的最大陣列大小
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * ArrayList 擴容的核心方法
 */
private void grow(int minCapacity) {
    // 將當前元素陣列長度定義為 oldCapacity 舊容量
    int oldCapacity = elementData.length;
    // 新容量更新為舊容量的1.5倍
    // oldCapacity >> 1 為按位右移一位,相當於 oldCapacity 除以2的1次冪
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 然後檢查新容量是否大於最小需要容量,若還小,就把最小需要容量當作陣列的新容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 再檢查新容量是否超出了ArrayList 所定義的最大容量
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        // 若超出,則呼叫hugeCapacity()
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

擴容的核心就是這句:int

newCapacity = oldCapacity + (oldCapacity >> 1);

本質就是擴容 1.5 倍,而且其中使用了移位運算,這裡從計算的角度上來看,相當於 oldCapacity 除以 2 的 1 次冪(偶數除以 2 剛好除盡,奇數丟掉小數部分)。使用按位右移,效率會高很多

>> 按位右移運算子:最高位為 0,左邊補齊 0,最高位是 1,左邊補齊 1

  • 快速計算:把 >> 左邊的資料 除以 2 的移動次冪:例如 -24 >> 2 即:-24 / 2 ^ 2 = -6

—— 此專案 【001-Java基礎知識】 章節中有具體介紹

擴容後,需要對這個新容量的範圍進行一個判斷,不能小於最小需要容量,也不能大於定義的最大容量,分情況細細看一下(以 1 和 11 舉例,是因為這兩種都是剛好需要擴容的)

  • add 第 1 個元素的時候,陣列還為空,所以無論是 oldCapacity 還是 newCapacity 都是 0,經過第一次判斷後,newCapacity = minCapacity 執行了,此時 newCapacity 為 10,第二個判斷不會進入,它不可能大於陣列的最大容量。

  • add 第 11 個元素的時候,oldCapacity 為 10,newCapacity = 10 + 10/2 = 15,大於 minCapacity = 11,第一個判斷不會進入,同時它肯定也沒有大於陣列最大 size,不會進入 。陣列容量此時就擴為 15,add 方法中會返回一個 true,size 也增加成 11。

  • 後面都是同樣的道理 ...

3.1.2.6 hugeCapacity()

這個方法就是在 newCapacity 大於 MAX_ARRAY_SIZE 的時候,開始判斷 minCapacity 和 MAX_ARRAY_SIZE 誰大,然後賦予不同的值。

/**
 * 比較minCapacity和 MAX_ARRAY_SIZE
 */
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
}

3.2 System.arraycopy() 和 Arrays.copyOf() 複製方法

在前面的方法中,大量的用到了這兩個方法,基本但凡涉及到元素移動的都會用到。

3.2.1 System.arraycopy()

拿 add 方法中的舉例

/**
 * 在此列表中的指定位置插入指定的元素
 * 再將從index開始之後的所有成員後移一個位置;將element插入index位置;最後size加1。
 */
public void add(int index, E element) {
    // 呼叫 rangeCheckForAdd 對 index 進行範圍檢查
    rangeCheckForAdd(index);
	// 保證容量足夠
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 自己複製自己,然後達到 index 之後全部元素向後挪一位的效果
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 然後將 index 賦值為指定的元素
    elementData[index] = element;
    size++;
}

arraycopy 是 System類 中的一個方法

/**
 * 陣列複製
 * 	src - 源陣列。
 * 	srcPos - 源陣列中的起始位置。
 * 	dest - 目標陣列。 
 * 	destPos - 目的地資料中的起始位置。 
 * 	length - 要複製的陣列元素的數量。
 */
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)

舉例:

public static void main(String[] args) {
    int[] arr = new int[10];
    arr[0] = 11;
    arr[1] = 22;
    arr[2] = 33;
    arr[3] = 44;
    arr[4] = 55;

    System.out.println("前:" + Arrays.toString(arr));
    // 指定下標後向後挪動一位
    System.arraycopy(arr, 1, arr, 2, 4);
    // 指定下標處替換元素
    arr[1] = 666;
    System.out.println("後:" + Arrays.toString(arr));
}

執行結果:

前:[11, 22, 33, 44, 55, 0, 0, 0, 0, 0]
後:[11, 666, 22, 33, 44, 55, 0, 0, 0, 0]

這樣就實現了 add 中的一個指定下標插入操作(不考慮擴容)

3.2.2 Arrays.copyOf()

所以,可以簡單的認為,這個方法的目的只要是為了給原陣列擴容。

public static void main(String[] args) {

    int[] arr1 = {1, 2, 3, 4, 5};
    int[] arr2 = Arrays.copyOf(arr1, 5);
    int[] arr3 = Arrays.copyOf(arr1, 10);

    System.out.println(Arrays.toString(arr1));
    System.out.println(Arrays.toString(arr2));
    System.out.println(Arrays.toString(arr3));
}

執行結果:

[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5, 0, 0, 0, 0, 0]

3.3 removeAll() 和 retainAll() 中的 batchRemove() 方法

在 removeAll() 和 retainAll() 方法中,都呼叫了 batchRemove()方法,區別只是傳參不同,就能實現兩種不同的正反刪除效果

/**
 * 從此列表中刪除指定集合中包含的所有元素。
 */
public boolean removeAll(Collection<?> c) {
    Objects.requireNonNull(c);
    return batchRemove(c, false);
}

/**
 * 僅保留此列表中包含在指定集合中的元素。即刪掉沒有的部分
 */
public boolean retainAll(Collection<?> c) {
    Objects.requireNonNull(c);
    return batchRemove(c, true);
}

來重點看一下這個方法的原始碼

/**
 * 刪除的具體邏輯,下面會有專題講解
 */
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) {
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            size = w;
            modified = true;
        }
    }
    return modified;
}

解釋一下剛開始的那些欄位

  • size :原陣列長度

  • elementData: 原陣列

  • modCount : 從父類繼承過來的變數,作用是記錄著集合的修改次數。

來看第一個關鍵程式碼

for (; r < size; r++)
	if (c.contains(elementData[r]) == complement)
		elementData[w++] = elementData[r];

我們以 removeAll() 為例,意圖從此列表中刪除指定集合中包含的所有元素。即,有的就刪,沒有的就不刪。

所以 complement 經過引數傳遞過來自然是 false,所以引數指定陣列中不含有原陣列指定位置下標的資料的時候,就將 elementData[r] 位置的資料覆蓋掉 elementData[w++] 位置的資料,r 根據迴圈++自增,w 根據變數 w++ 自增,若 if 表示式不成立則,r 自增,w 不自增。

舉例:原陣列:[1, 2, 3, 4, 5, 6, 7, 8, 9] ,指定引數陣列: [a, b, c, 3, 5, 8, f](例子參考自)重新排版

迴圈次數 r w 布林值 賦值語句 替換後的陣列值 說明
1 0 0 true elementData[0]=elementData[0] [1, 2, 3, 4, 5, 6, 7, 8, 9] 1 替換 1,r++ ,w++
2 1 1 true elementData[1]=elementData[1] [1, 2, 3, 4, 5, 6, 7, 8, 9] 2 替換 2,r++ ,w++
3 2 2 false [1, 2, 3, 4, 5, 6, 7, 8, 9]
4 3 2 true elementData[2]=elementData[3] [1, 2, 4, 4, 5, 6, 7, 8, 9] 4 替換 3,r++ ,w++
5 4 3 false [1, 2, 4, 4, 5, 6, 7, 8, 9]
6 5 3 true elementData[3]=elementData[5] [1, 2, 4, 6, 5, 6, 7, 8, 9] 6 替換 4,r++ ,w++
7 6 4 true elementData[4]=elementData[6] [1, 2, 4, 6, 7, 6, 7, 8, 9] 7 替換 5,r++ ,w++
8 7 5 false [1, 2, 4, 6, 7, 6, 7, 8, 9]
9 8 5 true elementData[5]=elementData[8] [1, 2, 4, 6, 7, 9, 7, 8, 9] 9 替換 6,r++ ,w++
9 6

自己走一遍上面的邏輯,就能深刻的感受得到

這步的作用:把需要移除的資料都替換掉,不需要移除的資料前移。(這步的處理尤為重要!)

接下來進入 finally 中,這一段是最終肯定會執行的

if (r != size) {
    System.arraycopy(elementData, r,elementData, w,size - r);
    w += size - r;
}
if (w != size) {
    for (int i = w; i < size; i++)
        elementData[i] = null;
    modCount += size - w;
    size = w;
    modified = true;
}

首先判斷 r 是否等於 size,如果上面的迴圈正常執行結束,r 和 size 應該是相同的,所以肯定不會走上面,第一個 if 判斷的目的就是為了解決某種異常情況下(異常,併發修改)導致的 for 迴圈未結束,此時 r != size 所以通過 arraycopy 將新增的元素追加到w索引後面。

而第二個 if ,主要是為了把 w 之後沒處理過的給刪掉,這樣就可以達到目的了。

例如上面表格的例子,最後 w = 6,也就是 [1, 2, 4, 6, 7, 9, 7, 8, 9] 中從下標為 6 的元素 7 開始刪除,將 7,8,9 賦值為 null 後面會被 GC 清理掉。最後得到的結果 [1, 2, 4, 6, 7, 9] 就是清除過的了 。

3.4 併發修改異常問題探索

public static void main(String[] args) {
    // 建立集合物件
    List list = new ArrayList();

    // 儲存元素
    list.add("I");
    list.add("love");
    list.add("you");

    Iterator it = list.iterator();
    while (it.hasNext()) {
        String s = (String) it.next();
        if ("love".equals(s)) {
            list.add("❤");
        }
        System.out.println(s);
    }
}

//執行結果(節選)
Exception in thread "main" java.util.ConcurrentModificationException

使用增強for或者迭代器遍歷集合的時候,如果對集合進行 list的 remove 和 add 操作,會出現 ConcurrentModificationException 併發修改異常的問題。

3.4.1 原因解釋:

當我們對集合進行遍歷的時候,我們會獲取當前集合的迭代物件

//List為例,獲取集合的迭代物件
Iterator it = list.iterator();

這個迭代物件中,封裝了迭代器的方法與集合本身的一些方法,當我們在迭代中使用集合本身的add / remove方法的時候,就產生了ConcurrentModificationException異常,通俗的說就是,在判斷 equals 成功後,執行了 list 的 add / remove 方法, 操作集合中元素或者刪除增加了,但是迭代器不清楚,所以就報錯,如果迭代器中含有這一種方法(假設),我們是用迭代器新增元素就不會有問題了。

詳細解釋

  • 開始時,cursor 指向下標為 0 的元素,lastRet 指向下標為 -1 的元素,每次呼叫 next 方法,cursor 和 lastRet 會分別自增 1。

  • 當突然 ArrayList 的 remove 方法被呼叫(不是 Itr 的 remove),會導致被刪除元素後面的所有元素都會往前移動一位,且 modCount 這個修改次數會增加,繼續迴圈,去執行 next 方法,而 next 方法中首先判斷的就是 modCount 和 expectedModCount 是否相等,很明顯由於 ArrayList 的操作,導致 modCount 變化,兩者現在已經不等了,所以出現異常

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

針對這個問題,我們給出兩個解決方案

3.4.2 解決方案:

3.4.2.1 方式1:迭代器迭代元素,迭代器修改元素

我們假想如果Iterator迭代器中有新增或者刪除等功能就好了,但很遺憾並沒有,但是它的子介面 ListIterator 卻擁有 add 這個功能(ListIterator 擁有 add、set、remove 方法,Iterator 擁有 remove 方法,這裡只演示 add 方法,remove 方法就用原來的 Iterator .remove() )

ListIterator 的 add()和 Iterator 的 remove() 可以使用的原因都是因為,方法進行了新增刪除操作後,都會執行 expectedModCount = modCount 這樣的賦值操作,相當於告訴迭代器我進行了修改操作。

public static void main(String[] args) {
    // 建立集合物件
    List list = new ArrayList();
    
    // 儲存元素
    list.add("I");
    list.add("love");
    list.add("you");

    ListIterator lit = list.listIterator();
    while (lit.hasNext()) {
        String s = (String) lit.next();
        if ("love".equals(s)) {
            // add 、remove 都是可以的
            lit.add("❤");
        }
        System.out.print(s + " ");
    }
    
    System.out.println();

    for (Object l : list){
    	System.out.print(l + " ");
    }
}

//執行結果
I love you
I love ❤ you 
3.4.2.1 方式2:集合遍歷元素,集合修改元素(普通for)
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

public class Demo2 {
    public static void main(String[] args) {
        //建立集合物件
        List list = new ArrayList();

        //儲存元素
        list.add("I");
        list.add("love");
        list.add("you");

        for (int x = 0; x < list.size(); x++){
            String s = (String)list.get(x);
            if ("love".equals(s)){
                list.add("❤");
            }
            System.out.print(s + " ");
        }
    }
}

//執行結果
I love you ❤ 

兩者均可以解決併發修改異常的問題,但是通過執行結果也可以看出,方法一新增後,在本次遍歷中不會輸出新增的結果,而方法二卻可以。

補充:增強for迴圈實現將集合進行遍歷,也產生了併發修改異常,這是因為增強for在底層也是呼叫的集合本身的 remove

3.4.3 iterator.remove() 的弊端

  • Iterator 只有 remove() 方法,add 方法在 ListIterator 中有
  • remove 之前必須先呼叫 next,remove 開始就對 lastRet 做了不能小於 0 的校驗,而l astRet 初始化值為 -1
  • next 後只能呼叫一次 remove,因為 remove 會將 lastRet 重新初始化為 -1

相關文章