Java 常用List集合使用場景分析
過年前的最后一篇,本章通過介紹ArrayList,LinkedList,Vector,CopyOnWriteArrayList 底層實現原理和四個集合的區別。讓你清楚明白,為什么工作中會常用ArrayList和CopyOnWriteArrayList?了解底層實現原理,我們可以學習到很多代碼設計的思路,開闊自己的思維。本章通俗易懂,還在等什么,快來學習吧!
知識圖解:
技術:ArrayList,LinkedList,Vector,CopyOnWriteArrayList
說明:本章基於jdk1.8,github上有ArrayList,LinkedList的簡單源碼代碼
源碼:https://github.com/ITDragonBlog/daydayup/tree/master/Java/collection-stu
知識預覽
ArrayList : 基於數組實現的非線程安全的集合。查詢元素快,插入,刪除中間元素慢。
LinkedList : 基於鏈表實現的非線程安全的集合。查詢元素慢,插入,刪除中間元素快。
Vector : 基於數組實現的線程安全的集合。線程同步(方法被synchronized修飾),性能比ArrayList差。
CopyOnWriteArrayList : 基於數組實現的線程安全的寫時復制集合。線程安全(ReentrantLock加鎖),性能比Vector高,適合讀多寫少的場景。
ArrayList 和 LinkedList 讀寫快慢的本質
ArrayList : 查詢數據快,是因為數組可以通過下標直接找到元素。 寫數據慢有兩個原因:一是數組復制過程需要時間,二是擴容需要實例化新數組也需要時間。
LinkedList : 查詢數據慢,是因為鏈表需要遍歷每個元素直到找到為止。 寫數據快有一個原因:除了實例化對象需要時間外,只需要修改指針即可完成添加和刪除元素。
本章會通過源碼分析,驗證上面的說法。
注:這里的塊和慢是相對的。並不是LinkedList的插入和刪除就一定比ArrayList快。明白其快慢的本質:ArrayList快在定位,慢在數組復制。LinkedList慢在定位,快在指針修改。
ArrayList
ArrayList 是基於動態數組實現的非線程安全的集合。當底層數組滿的情況下還在繼續添加的元素時,ArrayList則會執行擴容機制擴大其數組長度。ArrayList查詢速度非常快,使得它在實際開發中被廣泛使用。美中不足的是插入和刪除元素較慢,同時它並不是線程安全的。
我們可以從源碼中找到答案
// 查詢元素
public E get(int index) {
rangeCheck(index); // 檢查是否越界
return elementData(index);
}
// 順序添加元素
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 擴容機制
elementData[size++] = e;
return true;
}
// 從數組中間添加元素
public void add(int index, E element) {
rangeCheckForAdd(index); // 數組下標越界檢查
ensureCapacityInternal(size + 1); // 擴容機制
System.arraycopy(elementData, index, elementData, index + 1, size - index); // 復制數組
elementData[index] = element; // 替換元素
size++;
}
// 從數組中刪除元素
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
}
從源碼中可以得知,
ArrayList在執行查詢操作時:
第一步:先判斷下標是否越界。
第二步:然后在直接通過下標從數組中返回元素。
ArrayList在執行順序添加操作時:
第一步:通過擴容機制判斷原數組是否還有空間,若沒有則重新實例化一個空間更大的新數組,把舊數組的數據拷貝到新數組中。
第二步:在新數組的最后一位元素添加值。
ArrayList在執行中間插入操作時:
第一步:先判斷下標是否越界。
第二步:擴容。
第三步:若插入的下標為i,則通過復制數組的方式將i后面的所有元素,往后移一位。
第四步:新數據替換下標為i的舊元素。
刪除也是一樣:只是數組往前移了一位,最后一個元素設置為null,等待JVM垃圾回收。
從上面的源碼分析,我們可以得到一個結論和一個疑問。
結論是:ArrayList快在下標定位,慢在數組復制。
疑問是:能否將每次擴容的長度設置大點,減少擴容的次數,從而提高效率?其實每次擴容的長度大小是很有講究的。若擴容的長度太大,會造成大量的閑置空間;若擴容的長度太小,會造成頻發的擴容(數組復制),效率更低。
LinkedList
LinkedList 是基於雙向鏈表實現的非線程安全的集合,它是一個鏈表結構,不能像數組一樣隨機訪問,必須是每個元素依次遍歷直到找到元素為止。其結構的特殊性導致它查詢數據慢。
我們可以從源碼中找到答案
// 查詢元素
public E get(int index) {
checkElementIndex(index); // 檢查是否越界
return node(index).item;
}
Node<E> node(int index) {
if (index < (size >> 1)) { // 類似二分法
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
// 插入元素
public void add(int index, E element) {
checkPositionIndex(index); // 檢查是否越界
if (index == size) // 在鏈表末尾添加
linkLast(element);
else // 在鏈表中間添加
linkBefore(element, node(index));
}
void linkBefore(E e, Node<E> succ) {
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
從源碼中可以得知,
LinkedList在執行查詢操作時:
第一步:先判斷元素是靠近頭部,還是靠近尾部。
第二步:若靠近頭部,則從頭部開始依次查詢判斷。和ArrayList的elementData(index)
相比當然是慢了很多。
LinkedList在插入元素的思路:
第一步:判斷插入元素的位置是鏈表的尾部,還是中間。
第二步:若在鏈表尾部添加元素,直接將尾節點的下一個指針指向新增節點。
第三步:若在鏈表中間添加元素,先判斷插入的位置是否為首節點,是則將首節點的上一個指針指向新增節點。否則先獲取當前節點的上一個節點(簡稱A),並將A節點的下一個指針指向新增節點,然后新增節點的下一個指針指向當前節點。
Vector
Vector 的數據結構和使用方法與ArrayList差不多。最大的不同就是Vector是線程安全的。從下面的源碼可以看出,幾乎所有的對數據操作的方法都被synchronized關鍵字修飾。synchronized是線程同步的,當一個線程已經獲得Vector對象的鎖時,其他線程必須等待直到該鎖被釋放。從這里就可以得知Vector的性能要比ArrayList低。
若想要一個高性能,又是線程安全的ArrayList,可以使用Collections.synchronizedList(list);
方法或者使用CopyOnWriteArrayList集合
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized boolean removeElement(Object obj) {
modCount++;
int i = indexOf(obj);
if (i >= 0) {
removeElementAt(i);
return true;
}
return false;
}
CopyOnWriteArrayList
在這里我們先簡單了解一下CopyOnWrite容器。它是一個寫時復制的容器。當我們往一個容器添加元素的時候,不是直接往當前容器添加,而是先將當前容器進行copy一份,復制出一個新的容器,然后對新容器里面操作元素,最后將原容器的引用指向新的容器。所以CopyOnWrite容器是一種讀寫分離的思想,讀和寫不同的容器。
應用場景:適合高並發的讀操作(讀多寫少)。若寫的操作非常多,會頻繁復制容器,從而影響性能。
CopyOnWriteArrayList 寫時復制的集合,在執行寫操作(如:add,set,remove等)時,都會將原數組拷貝一份,然后在新數組上做修改操作。最后集合的引用指向新數組。
CopyOnWriteArrayList 和Vector都是線程安全的,不同的是:前者使用ReentrantLock類,后者使用synchronized關鍵字。ReentrantLock提供了更多的鎖投票機制,在鎖競爭的情況下能表現更佳的性能。就是它讓JVM能更快的調度線程,才有更多的時間去執行線程。這就是為什么CopyOnWriteArrayList的性能在大並發量的情況下優於Vector的原因。
private E get(Object[] a, int index) {
return (E) a[index];
}
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();
}
}
private boolean remove(Object o, Object[] snapshot, int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
......
Object[] newElements = new Object[len - 1];
System.arraycopy(current, 0, newElements, 0, index);
System.arraycopy(current, index + 1, newElements, index, len - index - 1);
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
總結
看到這里,如果面試官問你ArrayList和LinkedList有什么區別時
如果你回答:ArrayList查詢快,寫數據慢;LinkedList查詢慢,寫數據快。面試官只能算你勉強合格。
如果你回答:ArrayList查詢快是因為底層是由數組實現,通過下標定位數據快。寫數據慢是因為復制數組耗時。LinkedList底層是雙向鏈表,查詢數據依次遍歷慢。寫數據只需修改指針引用。
如果你繼續回答:ArrayList和LinkedList都不是線程安全的,小並發量的情況下可以使用Vector,若並發量很多,且讀多寫少可以考慮使用CopyOnWriteArrayList。
因為CopyOnWriteArrayList底層使用ReentrantLock鎖,比使用synchronized關鍵字的Vector能更好的處理鎖競爭的問題。
面試官會認為你是一個基礎扎實,內功深厚的人才!!!
到這里Java 常用List集合使用場景分析就結束了。過年前的最后一篇博客,有點浮躁,可能身在職場,心在老家!最后祝大家新年快樂!!!狗年吉祥!!!大富大貴!!!可能都沒人看博客了 ⊙﹏⊙‖∣ 哈哈哈哈(ಡωಡ)hiahiahia