ArrayList淺析

妖怪來了發表於2018-04-12

0x00 前言

大家好,我是 ArrayList, 應該是大家都耳熟能詳的容器之一了。學習一下內中原理,還是很有必要的。至於為什麼叫淺析呢,因為本文不會分析 Arrays 的相關方法。為什麼不分析 Arrays 的相關方法,就是淺析了呢?那就接著往下看~(本文分析原始碼基於jdk1.8。本文基於第一人稱描述,我 == ArrayList。)

0x01 一句話介紹

我實際上就是一個可以自動擴容的陣列,並可以進行增刪改查等操作。

0x02 概述

我是list介面的一個可擴容實現。通過 Java 的泛型機制,我可以容納任何型別的物件。我和 Vector 非常像,但我是執行緒不安全的,而他是執行緒安全的,其他地方的差異很小。

我所有方法中,時間複雜度為O(1)的有:

  • size
  • get
  • isEmpty
  • set
  • iterator
  • listIterator 而add方法是O(n)的。

我的每個例項,都有一個capacity變數。那麼這個變數是幹嘛的呢?這個變數用於衡量我內在的陣列用來裝變數的長度,他總是大於等於 size 。當新增一個元素到我這裡,他會自動的增長,以滿足需要。

如果一個應用他需要往我這裡放置很多的元素,那最好一開始就設定我的 ensureCapacity 變數,這樣我一開始就申請很多的空間,而不用我一次次擴容浪費時間了。

請注意,我不是執行緒安全的!如果多個執行緒同時修改我,一個要設定同步,否則會出現資料錯誤的情況,這個鍋,我是不背的。簡單的方式就是用Collections#synchronizedList來包裝一下我,我就變成一個同步的容器了。

我同樣擁有fail-fast機制,如果你迭代我,同時又修改我,我就會丟擲ConcurrentModificationException異常,讓你承受。多執行緒同時修改迭代,也會出現這個問題。

0x03 解釋幾個變數

private static final int DEFAULT_CAPACITY = 10

預設的陣列大小。

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}

如果構造我時,採用的是預設的 capacity ,就使用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 來當做預設的空陣列。

private static final Object[] EMPTY_ELEMENTDATA = {}

capacity == 0 ,則使用 EMPTY_ELEMENTDATA 來當做預設的空陣列。此時,產生了一個疑問,為什麼要弄兩個這樣的變數呢?後面我們在擴容的時候可以看到,他們是用來區分到底是被設定了 capacity 是 0 ,還是使用了預設的 capacity。那區分這個幹嘛呢?那就往下看看擴容是怎麼擴的。

transient Object[] elementData

先解釋一下 transient 這個,這個主要是讓此變數不進行序列化。更多的可以谷歌一下,看看詳解,此處就略過了。這個就是我的核心部件,我所有的元素都放在他裡面!實質就是一個 Object 陣列!

private int size

想要知道我裡面到底有多少個元素?喏,size 就是我所擁有的元素數量。

0x04 方法分析

建構函式分析
// 擁有設定 capacity 引數的建構函式
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {  // 如果設定的 initialCapacity 初始值大於0,那我的陣列就初始相應的長度
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) { 
        // 如果設定了 0 ,那就用 EMPTY_ELEMENTDATA 來初始化我的陣列。
        this.elementData = EMPTY_ELEMENTDATA;
    } else { // 小於 0 ,丟擲異常
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

// 無參構造
public ArrayList() {
    // 直接用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 來初始化我的陣列。
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 還有其他建構函式,此處略去不講。
複製程式碼
size()

此方法用於檢視我有多少個元素,來看看實現。

public int size() {
    // 非常簡單,就是返回 size 變數。
    return size;
}
複製程式碼
get(int index)

get 方法用來返回指定索引處的元素。

public E get(int index) {
    // 檢查索引是否越界
    rangeCheck(index);
    // 直接根據索引返回元素
    return elementData(index);
}

private void rangeCheck(int index) {
    // 可以看到,如果索引大於等於 size,將會丟擲異常。所以在使用時一定要注意不能傳入錯誤的索引,導致程式異常。
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
複製程式碼
set(int index, E element)

這個方法相當於修改操作。

public E set(int index, E element) {
    // 檢查邊界
    rangeCheck(index);
    // 先拿到老的元素
    E oldValue = elementData(index);
    // 對應位置附上新元素
    elementData[index] = element;
    // 返回老元素。所以 set 方法的返回是對應的舊元素。
    return oldValue;
}
複製程式碼
add(E e)

這個方法相當於增加操作。

public boolean add(E e) {
    // 確保陣列空間充足
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 將元素放到原陣列長度後面一位。
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    // 這裡使用了 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 。
    // 這個變數是在我初始化時,使用了預設的 capacity 的時候,用來初始化陣列的。
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 在這種情況下, 將入參和 預設 capacity 進行比較,取其較大大者。
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 根據上面的操作,決定了使用什麼長度來擴容。下面來進行擴容。
    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    // 如果會執行這個操作,就代表會改變陣列,則將改變標誌位+1.
    modCount++;

    // 原文註釋這裡說可能會有記憶體溢位
    if (minCapacity - elementData.length > 0) // 如果大於陣列長度,則進行擴容。
        grow(minCapacity);
}

// 我內部的陣列最大可以這麼大,為什麼減了個8呢?因為有些VM底層實現,保留了一些內部欄位,致使留給使用者的長度就變小了。
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private void grow(int minCapacity) {
    // 要注意是否會溢位。
    // 先儲存陣列的原長度。
    int oldCapacity = elementData.length;
    // 新長度是原長度的1.5倍。(右移相當於除以2,所以加起來是1.5倍)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0) // 如果新長度小於入參長度,則新長度賦值為入參長度。
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0) // 如果新長度大於陣列最大長度,則呼叫 hugeCapacity 方法獲取長度。
        newCapacity = hugeCapacity(minCapacity);
    // 呼叫了 Arrays 的方法,對陣列進行了一個複製。
    // 底層就不解釋了,可以簡單理解為把原 elementData 陣列中的值,一個個都搬移到了新的長度為 newCapacity 的陣列中,然後讓 elementData 指向新陣列。
    //(由於沒有解析 Arrays.copy 方法,所以本文只能算淺析,後面相關操作,也不會進行解析。要解析的話,一篇文章可能就放不下了。以後專開文章介紹這些工具類。)
    elementData = Arrays.copyOf(elementData, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // 如果 傳遞進來的引數小於0,則直接拋異常,此時陣列已經放不下了。
        // 什麼時候會小於0呢?可以看到引數是通過 index + 1 傳遞進來的,當index 已經到達了 Integer.MAX_VALUE,則會出現此情況。
        throw new OutOfMemoryError();
    // 發現引數比設定的最大長度還要大,那行吧,那就返回最大值,不然就直接返回最大的陣列長度。
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}
複製程式碼
add(int index, E element)

這個方法實際上是一個插入操作。

public void add(int index, E element) {
    // 範圍檢查
    rangeCheckForAdd(index);

    // 容量檢查
    ensureCapacityInternal(size + 1);  // modCount增加了
    // 直接呼叫了 System.arraycopy 方法。
    // 這裡也不去分析他底層的原始碼,去分析也不太容易,他實際上是一個 native 方法,要看只能去看 jni 層的程式碼了。
    // 解釋一下現在引數所表示的意思:就是將陣列根據傳入的 index 分成兩部分,然後把後面一部分往後面整體移動一個位置,index位置留出空位。
    // 第一個參數列示源陣列
    // 第二個參數列示從哪個位置開始複製
    // 第三個參數列示複製到哪個陣列中
    // 第四個參數列示複製從哪裡開始
    // 第五個參數列示複製多少位
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 將 index 位置進行填充
    elementData[index] = element;
    // 長度自增
    size++;
}
舉個?解釋一下 System.arraycopy 的操作。
有一個陣列如下:
[0,1,2,3,4,5,6,null,null,null]
現在如果要在第二位插入一個數字7.
第一步:找到第2個位置,是2;
第二步:把第二位開始的欄位整體往後遷移一位,變成:[0,1,2,2,3,4,5,6,null,null]
此時陣列已經移動完畢,再進行賦值即可。
複製程式碼

通過原始碼的分析,可以看到插入實際上是先對陣列進行復制移動,耗費巨大。所以應當避免此操作。

remove(int index)

此即刪除操作。

public E remove(int index) {
    // 邊界檢查
    rangeCheck(index);
    // 修改計數
    modCount++;
    // 找到舊值
    E oldValue = elementData(index);
    // 計算需要移動多少個元素。
    int numMoved = size - index - 1;
    if (numMoved > 0)
        // 和 上面的 add 操作呼叫,如出一轍。
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // 利於記憶體回收
    // 返回舊值
    return oldValue;
}
複製程式碼

可以發現,刪除操作也比較費勁,除非是最後一個元素。

remove(Object o)

刪除指定元素,可以想象,肯定是一個個的遍歷然後對比,再執行刪除操作。

public boolean remove(Object o) {
    // null 和 非 null 分開處理,私以為,直接使用 Objects.equals 方法不就好了麼。
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            // 可以看到他呼叫的是 equals 方法,然而沒有呼叫 == 來判斷是否是同一個物件。
            // 所以此處要注意,如果重寫了 equals 方法,則同一個物件未必相等。這裡可能就識別不到。
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}
// 這個方法和 remove 的上一個方法裡面的內容一模一樣,不知道上個 remove 方法不用。
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
}
複製程式碼
clear()

清空此 list。

public void clear() {
    modCount++;

    // 挨個賦值 null
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}
複製程式碼
contains(Object o) && indexOf(Object o)

為什麼這兩個方法放在一起呢,因為他們之間是好基友的關係。

public boolean contains(Object o) {
    // 可以看到,直接呼叫的 indexOf 方法。
    return indexOf(o) >= 0;
}

public int indexOf(Object o) {
    // 還是將 null 和 非 null 進行了區分處理。是不是似曾相識的程式碼!remove 不也這麼做的麼!
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}
複製程式碼

所以可以看到, contains 和 indexOf 實際上都是對陣列進行一個遍歷操作,所以使用一定要謹慎。而 Set 方法的 contains 方法是直接用的 hash 計算的,複雜度是 O(1).所以儘量用 Set 進行類似操作。

subList(int fromIndex, int toIndex)

從字面上看,他就是返回一個我的孩子 list。

public List<E> subList(int fromIndex, int toIndex) {
    // 邊界檢查
    subListRangeCheck(fromIndex, toIndex, size);
    // 返回一個 SubList 類!竟然不是 ArrayList
    return new SubList(this, 0, fromIndex, toIndex);
}

看看內部類 SubList 的宣告:
private class SubList extends AbstractList<E> implements RandomAccess
和 ArrayList 竟然不一樣。
那就看看建構函式吧:

SubList(AbstractList<E> parent,
        int offset, int fromIndex, int toIndex) {
    // parent 當然就是我咯,ArrayList
    this.parent = parent;
    // 相對我的偏移量,就是孩子是從哪開始擷取的
    this.parentOffset = fromIndex;
    // 如果兒子也要生兒子,就是產生SubList,則要計算相對爺爺的偏移量,此值就是為了來計算這個。
    // 如果要生重孫子,那就要計算相對於祖爺爺的偏移量。子子孫孫無窮盡也。
    this.offset = offset + fromIndex;
    // 計算孩子有多長。
    this.size = toIndex - fromIndex;
    // 繼承父親的更改計數。
    this.modCount = ArrayList.this.modCount;
}
看一下 SubList 方法的 get 方法。
public E get(int index) {
    // 檢查邊界
    rangeCheck(index);
    // 檢查是否已經被修改
    checkForComodification();
    // 吃驚嗎?竟然直接修改的是父親的陣列。
    return ArrayList.this.elementData(offset + index);
}
private void checkForComodification() {
    // 和父親的修改計數進行一個對比,如果父親變了,則丟擲異常。
    if (ArrayList.this.modCount != this.modCount)
        throw new ConcurrentModificationException();
}
......
複製程式碼

可以看到,SubList 實際上並不是拿到了一個和原陣列完全分離的陣列,對於 SubList 的修改,全都會作用於父親這裡。這就好比兒子惹得禍,父親都要來背。所以使用此方法一定要注意。同理,生兒子也要慎重!哈哈。

其他方法就先略過了

0x05 喝口水,來個總結

ArrayList 相對來說簡單些,但是其中也不乏 contains subList 這種需要注意的方法。知己知彼,方能百戰不殆!

文中如有錯誤,請大家不吝賜教!感激不盡,與君共勉。