先看再點贊,給自己一點思考的時間,思考過后請毫不猶豫微信搜索【沉默王二】,關注這個靠才華茍且的程式員,
本文 GitHub github.com/itwanger 已收錄,里面還有技術大佬整理的面試題,以及二哥的系列文章,
ArrayList 和 LinkedList 是 List 介面的兩種不同實作,并且兩者都不是執行緒安全的,但初學者往往搞不清楚它們兩者之間的區別,不知道什么時候該用 ArrayList,什么時候該用 LinkedList,那這篇文章就來傳道受業解惑一下,

ArrayList 內部使用的動態陣列來存盤元素,LinkedList 內部使用的雙向鏈表來存盤元素,這也是 ArrayList 和 LinkedList 最本質的區別,
注:本文使用的 JDK 原始碼版本為 14,小伙伴如果發現文章中的原始碼和自己本地的不同時,不要擔心,不是我原始碼貼錯了,也不是你本地的原始碼錯了,只是版本不同而已,
由于 ArrayList 和 LinkedList 內部使用的存盤方式不同,導致它們的各種方法具有不同的時間復雜度,先來通過維基百科理解一下時間復雜度這個概念,
在計算機科學中,演算法的時間復雜度(Time complexity)是一個函式,它定性描述該演算法的運行時間,這是一個代表演算法輸入值的字串的長度的函式,時間復雜度常用大 O 符號表述,不包括這個函式的低階項和首項系數,使用這種方式時,時間復雜度可被稱為是漸近的,亦即考察輸入值大小趨近無窮時的情況,例如,如果一個演算法對于任何大小為 n (必須比 大)的輸入,它至多需要 的時間運行完畢,那么它的漸近時間復雜度是 ,
對于 ArrayList 來說:
1)get(int index) 方法的時間復雜度為 ,因為是直接從底層陣列根據下標獲取的,和陣列長度無關,
public E get(int index) {
Objects.checkIndex(index, size);
return elementData(index);
}
這也是 ArrayList 的最大優點,
2)add(E e) 方法會默認將元素添加到陣列末尾,但需要考慮到陣列擴容的情況,如果不需要擴容,時間復雜度為 ,
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;
}
如果需要擴容的話,并且不是第一次(oldCapacity > 0)擴容的時候,內部執行的 Arrays.copyOf() 方法是耗時的關鍵,需要把原有陣列中的元素復制到擴容后的新陣列當中,
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
3)add(int index, E element) 方法將新的元素插入到指定的位置,考慮到需要復制底層陣列(根據之前的判斷,擴容的話,陣列可能要復制一次),根據最壞的打算(不管需要不需要擴容,System.arraycopy() 肯定要執行),所以時間復雜度為 ,
public void add(int index, E element) {
rangeCheckForAdd(index);
modCount++;
final int s;
Object[] elementData;
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
elementData[index] = element;
size = s + 1;
}
來執行以下代碼,把沉默王八插入到下標為 2 的位置上,
ArrayList<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("沉默王四");
list.add("沉默王五");
list.add("沉默王六");
list.add("沉默王七");
list.add(2, "沉默王八");
System.arraycopy() 執行完成后,下標為 2 的元素為沉默王四,這一點需要注意,也就是說,在陣列中插入元素的時候,會把插入位置以后的元素依次往后復制,所以下標為 2 和下標為 3 的元素都為沉默王四,

之后再通過 elementData[index] = element 將下標為 2 的元素賦值為沉默王八;隨后執行 size = s + 1,陣列的長度變為 7,

4)remove(int index) 方法將指定位置上的元素洗掉,考慮到需要復制底層陣列,所以時間復雜度為 ,
public E remove(int index) {
Objects.checkIndex(index, size);
final Object[] es = elementData;
@SuppressWarnings("unchecked") E oldValue = (E) es[index];
fastRemove(es, index);
return oldValue;
}
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
if ((newSize = size - 1) > i)
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;
}
對于 LinkedList 來說:
1)get(int index) 方法的時間復雜度為 ,因為需要回圈遍歷整個鏈表,
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
LinkedList.Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
LinkedList.Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
LinkedList.Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
下標小于鏈表長度的一半時,從前往后遍歷;否則從后往前遍歷,這樣從理論上說,就節省了一半的時間,
如果下標為 0 或者 list.size() - 1 的話,時間復雜度為 ,這種情況下,可以使用 getFirst() 和 getLast() 方法,
public E getFirst() {
final LinkedList.Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E getLast() {
final LinkedList.Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
first 和 last 在鏈表中是直接存盤的,所以時間復雜度為 ,
2)add(E e) 方法默認將元素添加到鏈表末尾,所以時間復雜度為 ,
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final LinkedList.Node<E> l = last;
final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
3)add(int index, E element) 方法將新的元素插入到指定的位置,需要先通過遍歷查找這個元素,然后再進行插入,所以時間復雜度為 ,
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
如果下標為 0 或者 list.size() - 1 的話,時間復雜度為 ,這種情況下,可以使用 addFirst() 和 addLast() 方法,
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
final LinkedList.Node<E> f = first;
final LinkedList.Node<E> newNode = new LinkedList.Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
linkFirst() 只需要對 first 進行更新即可,
public void addLast(E e) {
linkLast(e);
}
void linkLast(E e) {
final LinkedList.Node<E> l = last;
final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
linkLast() 只需要對 last 進行更新即可,
需要注意的是,有些文章里面說,LinkedList 插入元素的時間復雜度近似 ,其實是有問題的,因為 add(int index, E element) 方法在插入元素的時候會呼叫 node(index) 查找元素,該方法之前我們之間已經確認過了,時間復雜度為 ,即便隨后呼叫 linkBefore() 方法進行插入的時間復雜度為 ,總體上的時間復雜度仍然為 才對,
void linkBefore(E e, LinkedList.Node<E> succ) {
// assert succ != null;
final LinkedList.Node<E> pred = succ.prev;
final LinkedList.Node<E> newNode = new LinkedList.Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
4)remove(int index) 方法將指定位置上的元素洗掉,考慮到需要呼叫 node(index) 方法查找元素,所以時間復雜度為 ,
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
E unlink(LinkedList.Node<E> x) {
// assert x != null;
final E element = x.item;
final LinkedList.Node<E> next = x.next;
final LinkedList.Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
通過時間復雜度的比較,以及原始碼的分析,我相信小伙伴們在選擇的時候就有了主意,對吧?
需要注意的是,如果串列很大很大,ArrayList 和 LinkedList 在記憶體的使用上也有所不同,LinkedList 的每個元素都有更多開銷,因為要存盤上一個和下一個元素的地址,ArrayList 沒有這樣的開銷,
但是,ArrayList 占用的記憶體在宣告的時候就已經確定了(默認大小為 10),不管實際上是否添加了元素,因為復雜物件的陣列會通過 null 來填充,LinkedList 在宣告的時候不需要指定大小,元素增加或者洗掉時大小隨之改變,
另外,ArrayList 只能用作串列;LinkedList 可以用作串列或者佇列,因為它還實作了 Deque 介面,
我在寫這篇文章的時候,遇到了一些問題,所以請教了一些大廠的技術大佬,結果有個朋友說,“如果真的不知道該用 ArrayList 還是 LinkedList,就選擇 ArrayList 吧!”
我當時以為他在和我開玩笑呢,結果通過時間復雜度的分析,好像他說得有道理啊,查詢的時候,ArrayList 比 LinkedList 快,這是毋庸置疑的;插入和洗掉的時候,之前有很多資料說 LinkedList 更快,時間復雜度為 ,但其實不是的,因為要遍歷串列,對吧?
反而 ArrayList 更輕量級,不需要在每個元素上維護上一個和下一個元素的地址,

我這樣的結論可能和大多數文章得出的結論不符,那么我想,選擇權交給小伙伴們,你們在使用的程序中認真地思考一下,并且我希望你們把自己的思考在留言區放出來,
我是沉默王二,一枚有顏值卻靠才華茍且的程式員,關注即可提升學習效率,別忘了三連啊,點贊、收藏、留言,我不挑,奧利給,
注:如果文章有任何問題,歡迎毫不留情地指正,
如果你覺得文章對你有些幫助歡迎微信搜索「沉默王二」第一時間閱讀,回復「小白」更有我肝了 4 萬+字的 Java 小白手冊 2.0 版,本文 GitHub github.com/itwanger 已收錄,歡迎 star,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/137680.html
標籤:Java
上一篇:從今天開始我想學習C語言C++語言、大家監督,我會匯報自己的學習果,及時跟大家分享
下一篇:查詢/查詢顯示報錯:
