這是我參與更文挑戰的第1天,活動詳情查看:更文挑戰。 本文正在參加「Java主題月 - Java 開發實戰」,詳情查看 活動連接。java
常常在面試時,被問到集合的概念,集合 List、Map、Set 等底層設計以及其使用場景與注意細節。但大部分人的回答都是千篇一概,跟網上的答案如出一轍,這是致命滴。其實,你們都錯了,尤爲是網上,更是誤導你們,詳細緣由,且聽我來分析。面試
在廣大的網友心中,List 是一個緩存數據的容器,是 JDK 爲開發者提供的一種集合類型。面試時,被問到最多見的就是 ArrayList 和 LinkedList 的區別。編程
相信大部分網友都能回答上:ArrayList 是基於數組實現,LinkedList 是基於鏈表實現。而在使用場景時,我發現大部分網友的答案都是:在新增、刪除操做時,LinkedList 的效率要高於 ArrayList,而在查詢、遍歷操做的時候,ArrayList 的效率要高於 LinkedList。這個答案是否準確呢?今天就帶你們驗證一哈。數組
首先,你們都知道 ArrayList、LinkedList 都繼承了 AbstractList 抽象類,而 AbstractList 實現了 List 接口。ArrayList 使用數組實現,而 LinkedList 使用了雙向鏈表實現。接下來,咱們就詳細地分析下 ArrayList 和 LinkedList 的性能。緩存
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
複製代碼
在源碼中,咱們知道 ArrayList 除了實現克隆和序列化,還實現了 RandomAccess 接口。你們可能會對這個接口比較陌生,經過代碼咱們能夠發現,這個接口實際上是一個空接口,沒有實現邏輯,那麼 ArrayList 爲何要實現它呢?原來 RandomAccess 接口是一個標誌接口,它標誌着只要實現該接口,就能實現快速隨機訪問。markdown
至於 ArrayList、LinkedList 的各類操做方法這裏再也不說了,你們能夠看 這一篇。併發
接下來,咱們看看一些測試數據,以測試 50000 次爲例:dom
頭部:ArrayList.Time 大於 LinkedList.Time
中間:ArrayList.Time 小於 LinkedList.Time
末尾:ArrayList.Time 小於 LinkedList.Time
複製代碼
經過這測試,咱們能夠看到 LinkedList 新增元素的未必要快於 ArrayList。oop
因爲 ArrayList 是數組實現的,而數組是一塊連續的內存空間,在新增元素到數組頭部的時候,須要對頭部之後的數據進行重排,因此效率很低。而 LinkedList 是基於鏈表實現,在新增元素的時候,首先會經過循環查找到新增元素的位置,若是要新增的位置處於前半段,就從前日後找;若其位置處於後半段,就從後往前找。故 LinkedList 新增元素到頭部是很是高效的。post
在中間位置插入時,ArrayList 一樣有部分數據須要重排,效率也不是很高,而 LinkedList 將元素新增到中間,耗時最久的,由於靠近中間位置,在新增元素以前的循環查找是遍歷元素最多的操做。
而在尾部操做時,發如今沒有擴容的前提下,ArrayList 的效率要高於 LinkedList。這是由於 ArrayList 在新增元素到尾部的時候,不須要複製、重排,效率很是高。而 LinkedList 雖然也不用循環查找元素,但 LinkedList 中多了 new 對象以及變換指針指向對象的邏輯,因此要耗時多於 ArrayList 的操做。
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
複製代碼
頭部:ArrayList.Time 大於 LinkedList.Time
中間:ArrayList.Time 小於 LinkedList.Time
末尾:ArrayList.Time 小於 LinkedList.Time
複製代碼
你們會發現 ArrayList 和 LinkedList 刪除操做的測試結果和新增的結果很接近,這是同樣的道理,我就不贅述了。
for循環:ArrayList.Time 小於 LinkedList.Time
迭代器:ArrayList.Time 幾乎等於 LinkedList.Time
複製代碼
咱們能夠看到,LinkedList 的 for 循環遍歷比不上 ArrayList 的 for 循環。這是由於 LinkedList 基於鏈表實現的,在使用 for 循環的時候,每一次 for 循環都會去遍歷大半個 List,因此嚴重影響了遍歷的效率。而 ArrayList 是基於數組實現的,而且實現了 RandomAccess 接口標誌,意味着 ArrayList 能夠實現快速隨機訪問,因此 for 循環很是快。LinkedList 的迭代遍歷和 ArrayList 的迭代性能差很少,也不會太差,因此在遍歷 LinkedList 時,咱們要使用迭代循環遍歷。
在一次 ArrayList 刪除操做的過程當中,有下面兩種寫法:
public static void removeA(ArrayList<String> l) {
for (String s : l){
if (s.equals("aaa")) {
l.remove(s);
}
}
}
複製代碼
public static void removeB(ArrayList<String> l) {
Iterator<String> it = l.iterator();
while (it.hasNext()) {
String str = it.next();
if (str.equals("aaa")) {
it.remove();
}
}
}
複製代碼
第一種寫法錯誤,第二種是正確的,緣由是上面的兩種寫法都有用到 list 內部迭代器Iterator,即遍歷時,ArrayList 內部建立了一個內部迭代器 iterator,在使用 next 方法來取下一個元素時,會使用 ArrayList 裏保存的一個用來記錄 list 修改次數的變量 modCount,與 iterator 保存了一個叫 expectedModCount 的表示指望的修改次數進行比較,若是不相等則會拋出一個叫 ConcurrentModificationException 的異常。且在 for 循環中調用 list 中的 remove 方法,會走到一個 fastRemove 方法,該方法不是 iterator 中的方法,而是 ArrayList 中的方法,在該方法只作了 modCount++,而沒有同步到 expectedModCount。因此不一致就拋出了 ConcurrentModificationException 異常了。
下面是 ArrayList 本身的remove 方法:
public boolean remove(Object o) {
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++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
複製代碼
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
}
複製代碼
若是有看過阿里 Java 編程規範就知道,在集合中進行 remove 操做時,不要在 foreach 循環裏進行元素的 remove/add 操做。remove 元素使用 Iterator 方式,若是併發操做,須要對 Iterator 對象加鎖。