ArrayList 與 LinkedList
前言
ArrayList 與 LinkedList 估計在java面試彙總肯定會問到,一般需要掌握的點有.
比如還應該瞭解的有:
- 都是List,都java集合Collection介面的實現類.
- ArrayList基於陣列實現,長度固定,但可以擴容.
- ArrayList查詢快,增刪慢,因為需要移動資料
- LinkedList增刪快,不用移動資料
- LinkedList 提供了更多的方法來操作其中的資料,但是這並不意味著要優先使用LinkedList ,因為這兩種容器底層實現的資料結構截然不同。
- 二者執行緒多不安全.為什麼?
所以本文將試圖去原始碼上找答案.
ArrayList
ArrayList是可變長度陣列-當元素個數超過陣列的長度時,會產生一個新的陣列,將原陣列的資料複製到新陣列,再將新的元素新增到新陣列中。
方法及原始碼
- add
//底冊Object型別的陣列 transient Object[] elementData; // non-private to simplify nested class access /** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return <tt>true</tt> (as specified by {@link Collection#add}) */ public boolean add(E e) { ensureCapacityInternal(size + 1); // 擴容 elementData[size++] = e; //擴容後直接index賦值 return true; }
先看是否越界,越界就擴容,
擴容原始碼如下
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); //通過拷貝放入新陣列進行擴容 }
這裡Arrays.copyOf 底層使用System.arraycopy拷貝到新陣列(len+1)裡.
- get
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
get直接通過index獲取值,so快.
- 修改
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
沒啥好講的,直接覆蓋.
- 刪除
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, //拷貝new陣列
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
刪除後把index+1前移.然後末尾置為null,這裡註釋交給GC不太懂. 有一次拷貝和移動陣列.
執行緒不安全
- 為什麼會執行緒不安全
- 因為size++屬於非原子操作,這裡在多執行緒下可能會被其他線拿到,又沒有做擴容處理 可能會值覆蓋或者越界即ArrayIndexOutOfBoundsException.
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
- 如果兩個執行緒擦操作同一個下標資料,可能會覆蓋. 原因是a執行緒已經擴容了,b執行緒沒有get到size的變化,直接坐了覆蓋操作.
- 如何保證執行緒安全
首先理解執行緒安全,就是保證多個執行緒下資料
- Collections.synchronizedList
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);} //同步
}
但是原始碼註釋有寫到,遍歷集合的時候需要手動同步
* It is imperative that the user manually synchronize on the returned
* list when iterating over it:
* <pre>
* List list = Collections.synchronizedList(new ArrayList());
* ...
* synchronized (list) {
* Iterator i = list.iterator(); // Must be in synchronized block
* while (i.hasNext())
* foo(i.next());
* }
* </pre>
所以為了防止忘記,建議第二種.
- 使用CopyOnWriteArrayList
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();
}
}
LinkedList
LinkedList 底層的資料結構是基於雙向迴圈連結串列的,且頭結點中不存放資料,如下:
- add
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* Links e as last element.
*/
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++;
}
- remove
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
/**
* Unlinks non-null node x.
*/
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next; // 將前一節點的next引用賦值為x的下一節點
x.prev = null; //解除了x節點對前節點的引用
}
if (next == null) {
last = prev;
} else {
next.prev = prev; // 將x的下一節點的previous賦值為x的上一節點
x.next = null; //解除了x節點對後節點的引用
}
x.item = null; // 將被移除的節點的內容設為null
size--;
modCount++;
return element;
}
刪除過程基本上就是:
- 前節點指向後節點
- 當前pre為空
- 後節點的pre指向前節點
- 當前節點next為空
- 當前節點為空 交給gc處理
- size--
與ArrayList比較而言,LinkedList的刪除動作不需要“移動”很多資料,從而效率更高。
- get
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
// 獲取index處的節點。
// 若index < 雙向連結串列長度的1/2,則從前先後查詢;
// 否則,從後向前查詢。
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;
}
}
index與長度size的一半比較,要麼從頭開始,要麼從尾部開始. 但是都需要挨個查詢,所以這裡體驗出LinkedList比ArrayList查詢效率慢了
多執行緒下
LinkedList原始碼中發現,同樣會出現非原子性操作 size++/size--問題,所以是執行緒不安全的.
- Collections.synchronizedList 通ArrayList同步方法
- ConcurrentLinkedQueue 這個原始碼下詳解
總結
儲存結構
ArrayList是基於陣列實現的;按照順序將元素儲存(從下表為0開始),刪除元素時,刪除操作完成後,需要使部分元素移位,預設的初始容量都是10.
LinkedList是基於雙向連結串列實現的(含有頭結點)。
執行緒安全性
ArrayList 與 LinkedList 因為都不是原子操作,都不能保證執行緒安全.
Collections.synchronizedList,但是迭代list需要自己手動加鎖
ps:
Vector實現執行緒安全的,即它大部分的方法都包含關鍵字synchronized,但是Vector的效率沒有ArraykList和LinkedList高。
效率
ArrayList 從指定的位置檢索一個物件,或在集合的末尾插入、刪除一個元素的時間是一樣的,時間複雜度都是O(1)
LinkedList中,在插入、刪除任何位置的元素所花費的時間都是一樣的,時間複雜度都為O(1),但是他在檢索一個元素的時間複雜度為O(n).
ps: 事實上lindlinst並不會節省空間,只是相比增刪節省時間而已.