搞懂Java ArrayList原碼

commonBean發表於2020-10-18

【轉載】搞懂Java ArrayList原碼
在這裡插入圖片描述

概述

ArrayList的基本特點

  • ArrayList 底層是一個動態擴容的陣列結構
  • 允許存放(不止一個) null 元素
  • 允許存放重複資料,儲存順序按照元素的新增順序
  • ArrayList 並不是一個執行緒安全的集合。如果集合的增刪操作需要保證執行緒的安全性,可以考慮使用 CopyOnWriteArrayList 或者使用 collections.synchronizedList(List l)函式返回一個執行緒安全的ArrayList類.

繼承關係

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList 繼承自 AbstractList,實現了List, RandomAccess, Cloneable, java.io.Serializable 介面(4介面1父類)。

構造方法

  • 無參構造方法
    容量為0,呼叫add方法後擴為10
  • 指定初始容量的構造方法
    如果事先知道資料的大小,就可以構造指定容量的list,後面不需要擴容,會節省很多開銷(陣列拷貝),避免浪費記憶體空間(每次擴容為原先的1.5倍)
  • 使用另個一個集合 Collection 的構造方法
    這裡注意程式碼elementData.getClass() != Object[].class的判斷,ArrayList的elementData是Object[]型別,而傳入的collection物件可能不是(如Lists.asList(“a”,“b”)的toArray()結果是String[]),在賦值時,就會做判斷,向上升級.
    transient Object[] elementData; // non-private to simplify nested class access
    ...

    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

...
                
    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }                

新增元素,擴容機制

擴容

//擴容檢查
private void ensureCapacityInternal(int minCapacity) {
    //如果是無參構造方法構造的的集合,第一次新增元素的時候會滿足這個條件 minCapacity 將會被賦值為 10
   if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
       minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
   }
    // 將 size + 1 或 10 傳入 ensureExplicitCapacity 進行擴容判斷
   ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
  //運算元加 1 用於保證併發訪問 
   modCount++;
   // 如果 當前陣列的長度比新增元素後所需的長度要小則進行擴容 
   if (minCapacity - elementData.length > 0)
       grow(minCapacity);
}

上面的原始碼主要做了擴容前的判斷操作.

/**
 * 集合的最大長度 Integer.MAX_VALUE - 8 是為了減少出錯的機率 Integer 最大值已經很大了
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * 增加容量,以確保它至少能容納最小容量引數指定的元素個數。
 * @param 滿足條件的最小容量
 */
private void grow(int minCapacity) {
  //獲取當前 elementData 的大小,也就是 List 中當前的容量
   int oldCapacity = elementData.length;
   //oldCapacity >> 1 等價於 oldCapacity / 2  所以新容量為當前容量的 1.5 倍
   int newCapacity = oldCapacity + (oldCapacity >> 1);
   //如果擴大1.5倍後仍舊比 minCapacity 小那麼直接等於 minCapacity
   if (newCapacity - minCapacity < 0)
       newCapacity = minCapacity;
    //如果新陣列大小比  MAX_ARRAY_SIZE 就需要進一步比較 minCapacity 和 MAX_ARRAY_SIZE 的大小
   if (newCapacity - MAX_ARRAY_SIZE > 0)
       newCapacity = hugeCapacity(minCapacity);
   // minCapacity通常接近 size 大小
   //使用 Arrays.copyOf 構建一個長度為 newCapacity 新陣列 並將 elementData 指向新陣列
   elementData = Arrays.copyOf(elementData, newCapacity);
}

/**
 * 比較 minCapacity 與 Integer.MAX_VALUE - 8 的大小如果大則放棄-8的設定,設定為 Integer.MAX_VALUE 
 */
private static int hugeCapacity(int minCapacity) {
   if (minCapacity < 0) // overflow
       throw new OutOfMemoryError();
   return (minCapacity > MAX_ARRAY_SIZE) ?
       Integer.MAX_VALUE :
       MAX_ARRAY_SIZE;
}

擴容過程:

  • 每次擴為原先大小的1.5倍.如果放不下,就以實際需要的空間大小為準.
  • 將原來元素拷貝到一個擴容後陣列大小的長度新陣列中。所以 ArrayList 的擴容其實是相對來說比較消耗效能的。

末尾新增

先呼叫 ensureCapacityInternal 來判斷是否需要進行陣列擴容,然後將元素新增到陣列末尾:elementData[size++] = e;size加一

指定位置新增

先擴容檢查,然後呼叫System#arraycopy方法拷貝資料,再新增新元素.size加一

批量新增

刪除元素

根據下標刪除

主要是呼叫System#arraycopy方法進行復制,然後將最後一位賦值為null(以便垃圾回收),size減一

刪除指定元素

/**
* 刪除指定元素,如果它存在則反會 true,如果不存在返回 false。
* 更準確地說是刪除集合中第一齣現 o 元素位置的元素 ,
* 也就是說只會刪除一個,並且如果有重複的話,只會刪除第一個次出現的位置。
*/
public boolean remove(Object o) {
    // 如果元素為空則只需判斷 == 也就是記憶體地址
   if (o == null) {
       for (int index = 0; index < size; index++)
           if (elementData[index] == null) {
                //得到第一個等於 null 的元素角標並移除該元素 返回 ture
               fastRemove(index);
               return true;
           }
   } else {
        // 如果元素不為空則需要用 equals 判斷。
       for (int index = 0; index < size; index++)
           if (o.equals(elementData[index])) {
                //得到第一個等於 o 的元素角標並移除該元素 返回 ture
               fastRemove(index);
               return true;
           }
   }
   return false;
}

//移除元素的邏輯和 remve(Index)一樣 
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
}

說明:

  • 根據元素刪除只會刪除匹配的第一次出現的元素,後面的重複的元素不刪除
  • 元素為null和不為null判斷邏輯不一樣.不為null時使用equal判斷是不是同一個物件.
  • 刪除是使用System.arraycopy拷貝前移,然後最後一位置空,邏輯和 remve(Index)一樣

批量移除/保留

改查

修改某下標元素,查詢某下標元素,查詢元素的下標或者list是否包含某元素.都是與內部陣列有關的簡單操作.

遍歷

迭代器

public Iterator<E> iterator() {
        return new Itr();
}

問題: 為什麼迭代器刪除是安全的?


   int cursor; // 對照 hasNext 方法 cursor 應理解為下個呼叫 next 返回的元素 初始為 0
   int lastRet = -1; // 上一個返回的角標
   int expectedModCount = modCount;//初始化的時候將其賦值為當前集合中的運算元

   @SuppressWarnings("unchecked")
   public E next() {
        // 驗證期望的運算元與當前集合中的運算元是否相同 如果不同將會丟擲異常
       checkForComodification();
       // 如果迭代器的索引已經大於集合中元素的個數則丟擲異常,這裡不丟擲角標越界
       int i = cursor;
       if (i >= size)
           throw new NoSuchElementException();
           
       Object[] elementData = ArrayList.this.elementData;
       // 由於多執行緒的問題這裡再次判斷是否越界,如果有非同步執行緒修改了List(增刪)這裡就可能產生異常
       if (i >= elementData.length)
           throw new ConcurrentModificationException();
       // cursor 移動
       cursor = i + 1;
       //最終返回 集合中對應位置的元素,並將 lastRet 賦值為已經訪問的元素的下標
       return (E) elementData[lastRet = i];
   }
   
   final void checkForComodification() {
       if (modCount != expectedModCount)
           throw new ConcurrentModificationException();
   }

checkForComodification方法檢驗在迭代期間是否有其他執行緒對元素做了改動.

    // 實質呼叫了集合的 remove 方法移除元素
   public void remove() {
        // 比如操作者沒有呼叫 next 方法就呼叫了 remove 操作,lastRet 等於 -1的時候拋異常
       if (lastRet < 0)
           throw new IllegalStateException();
           
        //檢查運算元
       checkForComodification();
    
       try {
            //移除上次呼叫 next 訪問的元素
           ArrayList.this.remove(lastRet);
           // 集合中少了一個元素,所以 cursor 向前移動一個位置(呼叫 next 時候 cursor = lastRet + 1)
           cursor = lastRet;
           //刪除元素後賦值-1,確保先前 remove 時候的判斷
           lastRet = -1;
           //修改運算元期望值, modCount 在呼叫集合的 remove 的時候被修改過了。
           expectedModCount = modCount;
       } catch (IndexOutOfBoundsException ex) {
            // 集合的 remove 會有可能丟擲 rangeCheck 異常,catch 掉統一丟擲 ConcurrentModificationException 
           throw new ConcurrentModificationException();
       }
   }

注意:

  • 1.迭代器的remove方法,內部也是呼叫ArrayList#remove處理的.

  • 2.刪除完後,遊標cursor回退一個位置

  • 3.遊標的remove()操作,一次迴圈只能呼叫一次

  • 4.呼叫remove後modCod+1, expectedModCount 要設定為和modCod相等

  • 5.lastRet重置為-1

    根據第2點可知,迭代器的刪除方法,會在迭代器物件內維護一個遊標,該遊標是動態變化的.只要迭代器遍歷期間沒有執行緒改動list,在迴圈中進行操作都沒有問題.
    而對list使用for迴圈,並在迴圈中使用ArrayList#remove方法,就會有問題.

    第3點,可以根據程式碼看出:

            if (lastRet < 0)
                throw new IllegalStateException();

next方法會將當前的遊標位置賦值給lastRet:

        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

ListIterator 迭代器

ListIterator 繼承了Itr,因此不但可以向前遍,還可以向後遍歷.
在這裡插入圖片描述

安全性 modCount與expectedModCount

ArrayList 並不是一個執行緒安全的集合。如果集合的增刪操作需要保證執行緒的安全性,可以考慮使用 CopyOnWriteArrayList 或者使用 collections.synchronizedList(List l)函式返回一個執行緒安全的ArrayList類.(CopyOnWriteArrayList使用的是可重入鎖,collections.synchronizedList(List l)使用synchronized關鍵字)

雖然不是執行緒安全的集合.但是在對集合進行操作時.由於有modCount與expectedModCount對比校驗,能夠很快地判斷是否有其他執行緒對資料進行了修改.
各類增加和刪除元素的方法都會導致modCount加1.而在writeObject,forEach,removeIf,replaceAll,sort等方法,Iterator,SubList,ArrayListSeperator等內部類中都會進行校驗.

相關文章