ArrayList底層原理淺析

IT冰棍發表於2020-12-05

一. ArrayList和LinkedList異同

1. 相同點:

     ArrayList 和LinkedList 都是List 介面下的實現類,相同於List介面,這兩個實現類也同樣是有序且不唯一(可重複)的集合類。

2. 不同點:

      ArrayList 的底層仍然是使用陣列(Object[] elementData)實現的,通過對陣列操作的封裝,簡化了程式設計師程式設計中對集合的使用過程。ArrayList是List介面的主要實現類,也就是使用得最多的,因為其效率高,但是執行緒不安全。
   LinkedList 不同於ArrayList,其底層使用了雙向連結串列儲存資料,實現集合功能。由於其是雙向連結串列實現,對於集合插入、刪除需求高的情況,建議使用LinkedList。

二. ArrayList的底層實現原理淺析

1. 部分成員變數及常量的作用

private static final int DEFAULT_CAPACITY = 10;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;
// 常量MAX_ARRAY_SIZE表示整型的最大值-8,文末會用到並解釋作用
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

      這是在ArrayList 原始碼中的部分成員變數及常量,先對這幾個解釋以便後續理解。
     Object[] elementData:前面提到ArrayList 其實就是以陣列儲存資料的,elementData就是該儲存資料的陣列,後續對陣列的資料操作就是對該陣列的操作。

     private int size:size指的是該elementData的容量大小。

      final int DEFAULT_CAPACITY = 10:當使用ArrayList 的無帶參構造方法初始化時,即無指定陣列容量,會有一個預設的陣列容量,就是DEFAULT_CAPACITY ,即陣列容量預設是10。

   final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}:該常量是一個空陣列,當ArrayList 初始化時,若沒有指定容量大小,即呼叫無帶參建構函式,在底層會先將elementData初始化為空陣列。

2. ArrayList的構造方法

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 的無參建構函式時,會先將elementData 初始化為空陣列,即賦值為常量DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
          值得注意的是,當ArrayList 初始化時,若沒有指定容量大小,即呼叫無參構造方法,在底層會先將elementData初始化為空陣列,而不是直接將elementData初始化為預設容量(10)。當開始對elementData進行儲存資料的操作時,才會將其容量擴充為預設容量(10)。為什麼要提這一點呢,因為該情況是JDK1.8對ArrayList進行優化後才出現的,最初在JDK1.7及之前版本,對ArrayList 的初始化後,若無指定容量則直接將elementData初始化為預設容量(10)。
          相比於JDK1.7,JDK1.8的做法不會在初始化ArrayList 後就建立空間,而是等到真正需要將資料存入陣列時才擴充容量,對記憶體佔用有所優化。

  • 呼叫帶參構造方法時,根據傳入的int型別變數initialCapacity,initialCapacity即為指定的容量大小,將陣列初始化為指定容量大小的陣列。
    若傳參initialCapacity為0,則進行相同於無參構造方法的操作,即elementData為空陣列,當開始對elementData進行儲存資料的操作時,將其容量擴充為預設容量(10)。
    若initialCapacity < 0,則丟擲異常。

  • 除此之外ArrayList還有一個帶參構造方法傳入的引數是Collection<? extends E> c。該構造方法按照它們由集合的迭代器返回的順序構造一個包含指定集合的元素的列表。
    原始碼如下:

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

3. ArrayList的add方法及容量擴充

      我們知道ArrayList 的底層陣列elementData的容量要麼是預設值(10),要麼是指定容量。但是當我們對其進行新增資料超過了elementData的容量大小會怎麼樣呢?這時候就需要進行容量擴充的處理,前面說到的elementData初始化為空陣列,等到對資料進行儲存時才進行的容量擴充是也是類似的處理方式,只不過根據具體情況的不同會有些許步驟有所差別。接下來看看其處理過程。

public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

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

      容量擴充處理過程:此時先假設elementData容量大小n=10。當elementData的10個容量都已經儲存了資料後,又有新的資料需要add()進來,此時呼叫ensureCapacityInternal(size + 1)方法,因為是要add()一個資料進來,所以傳參是原本的陣列大小+1,。在該方法中有一個if 語句

private void ensureCapacityInternal(int minCapacity) {
   if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
       minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
   }
   ensureExplicitCapacity(minCapacity);
}
// 別忘了前面提到DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是空陣列
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

      這個if 語句存在的意義就是當elementData如果是初始化後的空陣列時,就需要進行容量擴充,其擴充的容量就是預設容量10。這對應著上文提到的JDK1.8對ArrayList優化後的特點:若沒有指定容量大小,即呼叫無參建構函式,在底層會先將elementData初始化為空陣列,當開始對elementData進行add操作時,才會將其容量擴充為預設容量(10)。 將變數minCapacity設定為10後,呼叫ensureExplicitCapacity(minCapacity)。
      若elementData是非空陣列,那麼直接呼叫ensureExplicitCapacity(minCapacity)函式

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

      這個方法也有一條if 語句,表示的意思是當minCapacity 大於陣列的長度時,則表明需要的陣列容量已經比當前的陣列容量大,因此需要進行容量擴充,而grow(minCapacity)方法就是對容量擴充的方法,將其呼叫。

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

      oldCapacity 表示當前陣列的容量大小,oldCapacity + (oldCapacity >> 1)表示將當前容量大小擴充為1.5倍大小,即若原本容量大小n=10的話,擴充完後的容量n=15。由此可看出ArrayList正常情況下的容量擴充都是擴充為原來的1.5倍。擴充完的容量大小賦值給變數newCapacity後,接著進行判斷,此時有兩條if 語句:
      先看第一條if 語句:

 if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

      第一個條if 語句表示當擴充後的容量比原來的容量還小時的情況。可是為什麼n擴充為原來1.5倍卻比原來小呢?這時候就回到之前說的若沒有指定容量大小,即呼叫無參建構函式,在底層會先將elementData初始化為空陣列,當開始對elementData進行儲存資料的操作時,才會將其容量擴充為預設容量(10)。 也就是說當elementData為空陣列時,長度為0,0的1.5倍仍然為0,所以要將容量直接擴充為minCapacity,而minCapacity的值在此情況下就是預設容量10。這條if 語句處理了呼叫無參建構函式,將elementData初始化為空陣列的容量擴充問題。
      接著看第二條if 語句:

if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
// 常量MAX_ARRAY_SIZE表示整型的最大值-8
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

      當擴充後的容量大於MAX_ARRAY_SIZE而又小於整型數的最大值時,那麼將容量設定為整型數的最大值Integer.MAX_VALUE。這個處理在 hugeCapacity(minCapacity)方法內進行。原始碼如下:

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // 溢位
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

      在這些方法處理完後完成了容量擴充,就又回到了add()方法,此時接著執行add()的新增資料的操作,因為已經完成了容量擴充,對於新資料有了多餘的容量存放。

      ArrayList 原始碼中還封裝了對陣列的各種操作的方法,建議可以去看看。本篇文章僅作為輔助理解ArrayList 部分原始碼。有錯歡迎評論指出。

相關文章