類圖結構
ArrayList
是對List
列表數據結構的一種具體實現,先放一張源碼類圖結構,有個直觀的印象,該圖是Java
集合Collection
類圖的一個子集:
存儲結構
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.5倍,將原數組對象複製到新數組,
elementData
對象引用指向新數組 - 第一次擴容時,數組分配長度取決於構造
ArrayList
時的參數,還記得那兩個空數組對象麼? - 如果初始化
ArrayList
時,空數組對象是“系統默認”的,那麼,數組擴容第一次得到的內存空間就是10個對象長度 - 如果初始化
ArrayList
時,空數組對象是“人爲指定”的,那麼,數組擴容第一次得到的內存空間就是1個對象長度 - 數組擴容的最大值是
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;
}
刪除邏輯非常簡單:
- 檢查刪除對象索引是否有效,索引就對應數組下標
- 拿到將要刪除的對象
- 將數組刪除位置之後的所有對象前移一位
- 返回刪除對象
需要注意的是,ArrayList
只有數組的擴容機制,沒有“減容機制”!刪除元素的時候不會動態減少數組空間。
面試的時候,不止一次的有面試者告訴我:ArrayList
數組空間是動態分配的,新增對象時,空間不夠就增加,刪除對象時,空間多了就減少。這完全是錯誤的理解!
再強調一次:ArrayList
底層數組只會在新增元素且數組空間不足時擴容,數組空間沒有動態變小的途徑!!!
查找對象
從ArrayList
裏面查找對象非常的快,因爲數組具有時間複雜度爲O(1)
的隨機查找能力。
查找源碼如下:
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
邏輯也簡單,沒啥可分析的:
- 檢查查找索引是否有效
- 從數組中獲取對象並返回
線程安全性
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.5
,Vector
的擴容因子是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
就是專門爲這種業務場景設計的。
它的特點是:
- 讀操作支持併發,寫操作保證同步
- 寫操作進行時,不會阻塞讀操作
- 它能保證數據不出錯,但是並非嚴格意義上的線程安全
看看新增和查找的源碼,從中可以看出它的實現原理:
// 數組存儲結構
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
- 在新數組中進行寫操作
- 寫操作完成後,將原數組引用指向新數組即可
- 併發寫操作時加鎖,保證同步
從寫操作邏輯中,可以看出CopyOnWriteArrayList
爲什麼會對讀操作有很好的併發支持。
讀操作包括新增和刪除,每一次寫操作,都需要複製一次數組,對內存空間有一定程度的浪費。
而且,因爲讀寫之間沒有同步機制,所以寫操作成功後,不一定能及時反饋給讀操作,可能就會出現兩種現象:
- 對象新增後不能及時讀到
- 對象刪除後還能讀到
這就是上面說的,從嚴格意義上講,CopyOnWriteArrayList
並不是線程安全的,但是宏觀上,它又能保證數據的正確性,很有特點的一個類!