引言
ArrayList
集合類在面試、開發中飽受關注,用起來也是真香。本篇文章有針對性的歸納整理ArrayList的常見問題,如有遺漏,歡迎留言或評論。
面試開始
小夥子,說下ArrayList的底層數據結構吧?
ArrayList的底層數據結構就是一個數組,數組元素的類型爲Object類型,對ArrayList的所有操作底層都是基於該數組的。
程序清單1: ArrayList的底層數組
transient Object[] elementData;
由源碼可以看出,底層是個Object類型的數組,並且是用關鍵字transient
修飾的,表示數組不可被序列化。
那結合剛剛說的底層數組實現說下ArrayList有哪些優缺點?
- ArrayList的優點
- ArrayList底層以數組實現,是一種隨機訪問模式,再加上它實現了RandomAccess接口,因此查找也就是get的時候非常快。
- ArrayList在順序添加一個元素的時候非常方便,只是往數組裏面添加了一個元素而已。
- 根據下標遍歷、訪問元素,效率高。
- 可以自動擴容,默認爲每次擴容爲原來的1.5倍。
- ArrayList的缺點
- 插入和刪除元素的效率較低。
- 根據元素下標查找元素需要遍歷整個元素數組,效率不高。
- 線程不安全。
好,剛剛你有提到擴容,可以說下ArrayList的擴容機制嗎?
主要從構造函數和add()方法擴容兩個層面進行分析。
程序清單2: ArrayList的構造函數源碼
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
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時,默認初始化爲一個空的數組。
程序清單3: ArrayList的擴容源碼
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
private Object[] grow() {
return grow(size + 1);
}
/* 擴容代碼 */
private Object[] grow(int minCapacity) {
/* 集合擴容完成後,需要將舊集合中的元素全部複製到新集合中 */
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
/* 新的容量 */
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
/* 擴容爲1.5倍 */
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
/* 返回默認大小(10)和擴容後的大小的最大值 */
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
由上述源碼可發現,當添加元素時每次都會校驗數組大小。當初始化一個空的集合時,第一次add元素時集合的大小會被初始化爲10。然後隨着集合元素不斷增加,當第11個元素插入時,這個時候集合需要擴容,擴容後的容量就是10+10>>1=15。擴容完成後,需要將員集合的元素複製到新的集合中。
注意:根據以上源碼分析可以知道,ArrayList每次擴容都會在堆內存裏開闢一個新的集合空間,將舊的集合中的所有元素都拷貝到新的集合中,舊的集合等待JVM回收。實際上這種不斷複製需要內存和時間開銷,所以最好在ArrayList初始化的時候就指定容量,並儘量保證之後不會擴容。
ArrayList是線程安全的麼?爲什麼?
ArrayList
是線程不安全的。體現在add()方法和迭代器裏,具體的這裏將結合源碼說明。
來呀,先給國師上源碼:
程序清單4: ArrayList的add()源碼
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
注意看modCount++
(該成員變量記錄着ArrayList的修改次數)這裏,我們知道自增、自減操作都是非原子操作,併發條件下必然有安全性問題。
add(e, elementData, size)
這個方法的作用就是將當前的新元素加到列表後面,ArrayList底層數組的大小是否滿足,如果size 長度等於底層數組的長度,那麼就要對這個數組進行擴容。注意看,這個方法是沒有任何的線程安全性保障的,假設現在ArrayList只剩一個元素可以添加了,此時線程1判斷無需擴容進行正常添加操作;當添加尚未完成時,線程2也進入到這裏,進行判斷,也是返回還剩一個元素可以添加(但實際上這個位置已經被線程1佔用了),此時線程2操作會報數組越界異常:ArrayIndexOutOfBoundsException。
程序清單5: ArrayList的迭代器源碼
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
SubList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = root.modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (root.modCount != expectedModCount)
throw new ConcurrentModificationException();
}
注意看checkForComodification()方法的實現,會對ArrayList的modCount
參數(該成員變量記錄着ArrayList的修改次數)進行判斷,如果實際修改次數和預期修改次數expectedModCount
不一致(併發條件下會出現),則會拋出併發修改異常ConcurrentModificationException。
怎樣讓ArrayList成爲線程安全的呢?
有兩種方案:
- 使用
synchronized
關鍵字 - 創建ArrayList對象的時候採用Collections類中的靜態方法
synchronizedList(new ArrayList<>())
ArrayList、Vector和LinkedList的區別?
數據結構 | 線程安全性 | 增刪改查的效率 | |
---|---|---|---|
ArrayList | 數組 | 線程不安全 | 查詢快,增刪慢(在末尾增刪除外) |
LinkedList | 雙向鏈表 | 線程不安全 | 查詢慢,增刪快 |
Vector | 數組 | 線程安全 | 查詢快,增刪慢(在末尾增刪除外) |
面試結束
好的,小夥子表現不錯,對ArrayList的掌握較好,下輪面試再見!悄悄給你透露一下哦,下一輪面試官喜歡考察
HashSet
,回去好好備戰!
好嘞,謝謝面試官的提醒,下一回合我也會再接再厲。