[JDK8] ArrayList源碼解析

類圖結構

ArrayList是對List列表數據結構的一種具體實現,先放一張源碼類圖結構,有個直觀的印象,該圖是Java集合Collection類圖的一個子集:
ArrayList源碼結構

存儲結構

transient Object[] elementData;

ArrayList實例本身只是一個普通的Java對象,它的內部封裝了一個數組,添加到ArrayList裏面的對象元素都是存儲在這個數組當中。

數組存儲結構的最大特點就是內存空間具有連續性,隨機訪問數組任何位置,時間複雜度都是O(1)

因此,單從數組存儲結構,就能看出ArrayList的特點:

1、優秀的對象查找速度,時間複雜度永遠是O(1)
2、增刪對象的時候,涉及數組其它對象的前後移動,因此效率較低
3、在列表尾部的增刪效率高於在頭部的增刪效率,因爲尾部增刪需要移動的其它對象較少

數組還有一個特點,就是一旦創建,數組長度就是固定的。當數組存儲空間用完,還要繼續向列表添加元素的時候,就需要開闢新的存儲空間,這就是ArrayList的數組擴容機制,後文再講。

ArrayList初始化

ArrayList有三個構造方法:

// 對象存儲數組
transient Object[] elementData;

// 兩個空集合標識,一個表示“人爲指定”,一個表示“系統默認”
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 無參構造函數
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

// 構造的時候,指定ArrayList的初始容量
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);
    }
}

// 構造的時候,添加一些初始化元素
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;
    }
}

從源碼可以看出,構造ArrayList的時候,最關心的是就數組elementData存儲空間的初始化。

通過以下方式構造ArrayList時,數組暫不分配存儲空間:

ArrayList list = new ArrayList();
ArrayList list = new ArrayList(0);
ArrayList list = new ArrayList(list);// list是空的

ArrayList構造完成以後,數組變量elementData會指向一個預定義的空數組對象,要麼是EMPTY_ELEMENTDATA,要麼是DEFAULTCAPACITY_EMPTY_ELEMENTDATA

爲什麼要預定義兩個空數組對象呢?

這是在爲後面新增元素時數組擴容作準備,數組第一次擴容時,需要知道指向空對象的原因是“人爲指定”還是“系統默認”。暫時先記住這一點,後面講擴容時再具體分析。

通過以下方式構造ArrayList時,數組立即分配存儲空間:

ArrayList list = new ArrayList(128);
ArrayList list = new ArrayList(list);// list裏面有對象元素

小結:構建ArrayList對象時,爲了優化性能,非必要的情況下,不會分配數組存儲空間,如果明確知道後續操作需要多大的數組空間,指定一個合適的初始容量也是極好的。

新增對象與數組擴容

新增元素的方法有四個,實現上大同小異,順着其中任何一個方法追蹤下去,很快就可以看到新增邏輯和數組擴容機制,數組擴容只在新增的時候纔會有。

add(E e)方法爲例,查看完整方法調用鏈如下:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 計算數組存儲空間的最小長度
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
// 數組擴容的核心邏輯
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

這段代碼邏輯包含兩個方面內容:添加元素、數組擴容。

添加元素的邏輯是這樣的:

  1. 判斷當前的數組空間夠不夠用
  2. 如果夠用,將元素添加到數組當中
  3. 如果不夠用,先觸發數組擴容機制,再將元素添加到數組當中

數組擴容的邏輯是這樣的:

  1. 數組空間不足時纔會觸發擴容機制,創建新的內存數組,長度是原來的1.5倍,將原數組對象複製到新數組,elementData對象引用指向新數組
  2. 第一次擴容時,數組分配長度取決於構造ArrayList時的參數,還記得那兩個空數組對象麼?
  3. 如果初始化ArrayList時,空數組對象是“系統默認”的,那麼,數組擴容第一次得到的內存空間就是10個對象長度
  4. 如果初始化ArrayList時,空數組對象是“人爲指定”的,那麼,數組擴容第一次得到的內存空間就是1個對象長度
  5. 數組擴容的最大值是Integer.MAX_VALUE,再繼續擴容就會拋出OutOfMemoryError異常

擴容機制的好處是可以保證對象元素存儲空間的動態增加,避開了數組固定長度的限制,但這也是降低列表性能的操作。

因此,在實際應用場景下,如何降低擴容次數也是ArrayList一個可以考慮的優化方向。

刪除對象

刪除方法有多個,實現也是大同小異,最常用的刪除操作是:

public E remove(int 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;
}

刪除邏輯非常簡單:

  1. 檢查刪除對象索引是否有效,索引就對應數組下標
  2. 拿到將要刪除的對象
  3. 將數組刪除位置之後的所有對象前移一位
  4. 返回刪除對象

需要注意的是,ArrayList只有數組的擴容機制,沒有“減容機制”!刪除元素的時候不會動態減少數組空間。

面試的時候,不止一次的有面試者告訴我:ArrayList數組空間是動態分配的,新增對象時,空間不夠就增加,刪除對象時,空間多了就減少。這完全是錯誤的理解!

再強調一次:ArrayList底層數組只會在新增元素且數組空間不足時擴容,數組空間沒有動態變小的途徑!!!

查找對象

ArrayList裏面查找對象非常的快,因爲數組具有時間複雜度爲O(1)的隨機查找能力。

查找源碼如下:

public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

邏輯也簡單,沒啥可分析的:

  1. 檢查查找索引是否有效
  2. 從數組中獲取對象並返回

線程安全性

ArrayList是線程不安全的,因爲源碼裏面沒有涉及到任何的鎖操作,也沒有任何的數據同步保障。

所以,多線程場景下使用ArrayList存在線程安全問題。

如何解決這個問題呢?提供三個方案。

Vector

Vector實現邏輯與ArrayList很像,最大的區別在於Vector會在方法上使用synchronzied關鍵字保證線程安全:

public synchronized boolean add(E e) {...}
public synchronized E remove(int index) {...}
public synchronized E get(int index) {...}

可以看到,新增、刪除、查找,這三類方法都加上了synchronized關鍵字。

Vector保證了線程安全,但犧牲了增刪查的效率,尤其是查找效率大打折扣,這是非常致命的一點。

再提一個它們的區別,默認情況下,ArrayList的擴容因子是1.5Vector的擴容因子是2。也就是它們各自的數組擴容速度,相同數據量下,Vector擴容次數不會高於ArrayList

因此,小數據量的場景下,即使Vector有同步操作,它的新增速度通常也會優於ArrayList,大數據量的場景下,Vector通常又會比ArrayList浪費更多的數組存儲空間。

總有人跟我說,不要使用Vector,因爲它的同步性能低下。

我不否認這一點,但是我想說的是,Vector並非一無是處,它也有優於ArrayList的場景,合理的選擇利用它們,揚長避短,纔是編程取捨之道。

SynchronizedList

SynchronizedList是集合工具類Collections裏面的一個靜態內部類,通常,用法如下:

ArrayList<String> list = new ArrayList<>();
List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("string");
String str = synchronizedList.get(0);

SynchronizedList是保證線程安全的方法也是利用的synchronized同步機制:

public void add(int index, E element) {
    synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
    synchronized (mutex) {return list.remove(index);}
}
public E get(int index) {
    synchronized (mutex) {return list.get(index);}
}

這種方案是運用了代理模式,對List實現類進行了代理,在增刪查操作之前添加同步操作,效率也不高。

CopyOnWriteArrayList

使用List集合的業務場景,通常情況下是讀多寫少,CopyOnWriteArrayList就是專門爲這種業務場景設計的。

它的特點是:

  1. 讀操作支持併發,寫操作保證同步
  2. 寫操作進行時,不會阻塞讀操作
  3. 它能保證數據不出錯,但是並非嚴格意義上的線程安全

看看新增和查找的源碼,從中可以看出它的實現原理:

// 數組存儲結構
private transient volatile Object[] array;

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
public E get(int index) {
    return get(getArray(), index);
}

它的寫操作邏輯是這樣的:

  1. 底層還是數組存儲結構
  2. 進行寫操作前,將原數組複製一份,新數組空間長度加1
  3. 在新數組中進行寫操作
  4. 寫操作完成後,將原數組引用指向新數組即可
  5. 併發寫操作時加鎖,保證同步

從寫操作邏輯中,可以看出CopyOnWriteArrayList爲什麼會對讀操作有很好的併發支持。

讀操作包括新增和刪除,每一次寫操作,都需要複製一次數組,對內存空間有一定程度的浪費。

而且,因爲讀寫之間沒有同步機制,所以寫操作成功後,不一定能及時反饋給讀操作,可能就會出現兩種現象:

  1. 對象新增後不能及時讀到
  2. 對象刪除後還能讀到

這就是上面說的,從嚴格意義上講,CopyOnWriteArrayList並不是線程安全的,但是宏觀上,它又能保證數據的正確性,很有特點的一個類!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章