前言
上篇文章分析了 ArrayList 的源碼,面試過程中經常會被面試官來問 LinkedList 和 ArrayList 的區別,這篇文章從源碼的角度來看下 LinkedList 以后,再和上篇文章做個對比,相信你會有一個自己的判斷的。
LinkedList 簡介
老規矩,先來看下官方 Api 對 LinkedList 的介紹:
從圖中可以看出,LinkedList 和 ArrayList 都是直接或者間接繼承于 AbstractList 的,但是和 ArrayList 不同的是 LinkedList 是直接繼承于 AbstractSequentialList 的。
先來看下這個 AbstractSequentialList :
Api 中也描述了 AbstractSequentialList 提供了一個基本的List接口實現,為實現序列訪問的數據結構存儲提供了所需要的最小化接口實現,而對于支持隨機訪問數據的List比如數組,應該優先使用 AbstractList。
和 AbstractList 實現隨機訪問相反,AbstractSequentialList 采用的迭代器實現的 get、set、add 和 remove 等黨閥
為了實現這個列表。僅僅需要拓展這個類,并且提供ListIterator和size方法。
對于不可修改的List,編程人員只需要實現Iterator的hasNext、next和hasPrevious、previous和index方法
對于可修改的List還需要額外實現Iterator的的set的方法
對于大小可變的List,還需要額外的實現Iterator的remove和add方法
LinkedList 實現的所有接口有:
- 實現了 Serializable 是序列化接口,因此它支持序列化,能夠通過序列化傳輸。
- 實現了 Cloneable 接口,能被克隆。
- 實現了Iterable<E> 接口,可以被迭代器遍歷
- 實現了 Collection<E> ,擁有集合操作的方法
- 實現 Deque<E>/Queue<E> 可以當作隊列/雙端隊列使用
- 實現了 List<E> 接口,擁有增刪改查等方法
先看下LinkedList 的特點,對 LinkedList 有一個大體上的認識:
- LinkedList 底層數據結構是雙向鏈表,但是頭節點不存放數據,只有后置節點的引用;
- 集合中的元素允許為 null,可以看到源碼中在查找和刪除時,都劃分為該元素為null和不為null兩種情況來處理。
- 允許內部元素重復
- 不存在擴容問題,所以是沒有擴容的方法
- 元素在內部是有序存放的,依次在鏈表上添加節點
- 實現了棧和隊列的操作方法,因此也可以作為棧、隊列和雙端隊列來使用
- 由于是鏈表實現,并且沒有實現RandomAccess ,雖然在查找的時候,會先判斷是在前半部分或者后半部分,然后依次從前或者從后查找,但是查找效率還是很低,不過增刪效率高,但是查找和修改大部分情況下不如 ArrayList。
- 線程不安全,可以用個 Collections.SynchronizedList(new LinkedList()) 返回一個線程安全的 LinkedList
下面從源碼的角度進行分析:
LinkedList 源碼分析
一些屬性
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
// 大小
transient int size = 0;
// 頭節點
transient Node<E> first;
// 尾節點
transient Node<E> last;
// 序列化ID
private static final long serialVersionUID = 876323262645176354L;
}
前面講了,LinkedList 是基于雙向鏈表實現的,所以屬性也很簡單,定義了 大小、頭節點和尾節點。
看下每個節點的結構:
private static class Node<E> {
// 元素值
E item;
// 下個節點的引用
Node<E> next;
// 上個節點的引用
Node<E> prev;
// 構造方法
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
很明顯的雙向鏈表的結構。
構造方法
// 空的構造方法,什么都沒做,只是生成了對象
public LinkedList() {
}
// 傳入了集合 c,并將其插入到鏈表中。
public LinkedList(Collection<? extends E> c) {
this();
// 添加方法稍后分析
addAll(c);
}
構造方法也很簡單,沒有什么特殊的操作。
前面講了,LinkedList 可以當做一個 List 使用,也可以當做隊列使用,依次進行分析:
作為列表使用的一些方法:
添加(add)的一些方法
先看下 add 方法:
// 這個方法實現的效果和 addLast(E e) 是一樣的
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++;
}
add(int index, E element)
在指定位置添加
public void add(int index, E element) {
//檢查插入位置的合法性,即是否比 0 大,比當前的 size 小
checkPositionIndex(index);
// 如果是等于當前大小,就是相當于在尾部再插入一個節點
// 否則就是插入到 index 所在位置的節點的前面
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
// 返回指定索引處的一個非空節點
// 這里是 LinkedList 做的一個優化,先判斷索引是在前半部分和后半部分
// 如果前半部分,從頭節點開始找,正序找
// 如果后半部分,從尾節點開始找,倒序找
Node<E> node(int index) {
// assert isElementIndex(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;
}
}
// 插入到指定節點的前面
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
// 取出查找到指定位置的節點
final Node<E> pred = succ.prev;
// 構建新節點,前置節點找到節點的原前置節點,e 是元素值,后置節點是根據位置找到的 succ
final Node<E> newNode = new Node<>(pred, e, succ);
// 原位置的前置節點設置為要插入的節點。
succ.prev = newNode;
// 如果原位置的前置節點為空,即原位置 succ 是頭節點,即 add(0 ,E )然后把新建節點賦值為頭節點。
if (pred == null)
first = newNode;
else
// 不為空,原位置的前置節點的后置節點設置為新節點。
pred.next = newNode;
size++;
modCount++;
}
總的來說就是: 先檢查是否在可插入的范圍內,不在拋異常,如果 index 和當前 size 相等,直接插入到尾節點,如果小于當前 size,那么就插入到 index 節點的前面。
看下 addAll
// 沒有傳入位置,直接加到最后
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
// 加入到指定位置
public boolean addAll(int index, Collection<? extends E> c) {
// 檢查 index 合法性
checkPositionIndex(index);
// 傳入的 Collection 轉換成數組
Object[] a = c.toArray();
int numNew = a.length;
// 空數組,直接返回插入失敗
if (numNew == 0)
return false;
// pred 是 succ 的前置節點 ,succ指向當前需要插入節點的位置的節點
Node<E> pred, succ;
// index 等于 size,尾插
// 不等于,找到需要插入位置的節點,以及其前置節點,pred 可能為空
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
// 依次構建并插入新節點
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
// 當前空鏈表,傳入的第一個元素設置為頭節點
if (pred == null)
first = newNode;
else
// 不為空鏈表,pred 后置節點設置為新節點
pred.next = newNode;
// 每次設置完,pred 表示剛插入的節點,依次往后插入
pred = newNode;
}
// 如果是從 size 位置開始添加,最后添加的節點成了尾節點
if (succ == null) {
last = pred;
} else {
// 如果不是從 size 開始添加,數組中最后一個元素的后置節點指向為 原 index 位置節點
// 原 index 位置節點的前置節點置為數組中最后一個元素構建的節點。
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
addFirst 、addLast
// 添加元素到鏈表頭。
public void addFirst(E e) {
linkFirst(e);
}
// 添加元素到鏈表尾
public void addLast(E e) {
linkLast(e);
}
linkLast 前面在講 add 的時候已經分析過了,再來看下 linkFirst
private void linkFirst(E e) {
// 保存頭節點
final Node<E> f = first;
// 創建新節點
final Node<E> newNode = new Node<>(null, e, f);
// 頭節點設置為新節點
first = newNode;
// 如果頭節點為空,表名鏈表為里面沒數據,尾節點也需要設置為新節點
if (f == null)
last = newNode;
else
// 頭節點不為空,設置原頭節點的前置節點為 新節點。
f.prev = newNode;
size++;
modCount++;
}
刪除(remove)的一些方法
//移除指定位置的元素
public E remove(int index) {
checkElementIndex(index);
// 先拿到指定位置的節點
return unlink(node(index));
}
// 移除指定元素
// 這里和 ArrayList 里面移除比較相似,分為 null 和 不為 null 兩種情況。先從頭節點遍歷找到要移除的元素,
// 然后執行移除第一個元素對應的節點的操作。。
// 是移除第一個相等的元素!
// 是移除第一個相等的元素!
// 是移除第一個相等的元素!
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
// 取消位置鏈接
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;
// 前置節點為 null ,表明要移除頭節點,把下一個節點設置為頭節點
// 前置節點不為 null ,x 的前置節點的后置節點指向 x 的后置節點
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
// 后置節點為 null ,表明要移除尾節點,把上一個節點設置為尾節點
// 后置節點不為 null ,x 的后置節點的前置節點指向 x 的前置節點
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
// 釋放引用的元素,gc 可回收
x.item = null;
size--;
modCount++;
return element;
}
這兩個刪除的方法基本都是先找到要刪除元素對應的節點,然后再去執行 unlink 方法去對節點的 前置節點、后置節點進行重新指向。然后把引用的元素 置為 null ,便于 gc 回收移除的元素,最后返回移除元素。
此外還有移除第一個和最后一個
// 移除第一個元素
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
// 移除最后一個元素
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
// 移除第一個元素,調整指針指向,并把頭部部元素置空,便于 gc 回收
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
// 移除最后一個元素,調整指針指向,并把尾部元素置空,便于 gc 回收
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
修改(set)的一些方法
// 修改一個元素
public E set(int index, E element) {
// 檢查index 是否在合法位置
checkElementIndex(index);
// 通過 node 函數找到要修改位置對應的節點
Node<E> x = node(index);
// 然后直接修改元素里面的 item 屬性,完成修改
E oldVal = x.item;
x.item = element;
return oldVal;
}
查找(get)的一些方法
// 查找指定位置的元素
public E get(int index) {
// 檢查index 是否在合法位置
checkElementIndex(index);
// 通過 node 函數找到要修改位置對應的節點,并返回其 item 屬性,即為元素值。
return node(index).item;
}
// 查找第一個元素
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
// 查找最后一個元素
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
上面的都比較簡單。
清除(clear) 的一些方法
// 移除列表中的所有元素
public void clear() {
// Clearing all of the links between nodes is "unnecessary", but:
// - helps a generational GC if the discarded nodes inhabit
// more than one generation
// - is sure to free memory even if there is a reachable Iterator
// 遍歷所有的不為 null 的節點,把所有數據全部置為 null,便于gc 回收
for (Node<E> x = first; x != null; ) {
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
first = last = null;
size = 0;
modCount++;
}
作為隊列使用的一些方法
隊列是什么?
隊列是一種比較特殊的線性結構。它只允許在表的前端(front)進行刪除操作,而在表的后端(rear)進行插入操作。進行插入操作的端稱為隊尾,進行刪除操作的端稱為隊頭。
隊列中最先插入的元素也將最先被刪除,對應的最后插入的元素將最后被刪除。因此隊列又稱為“先進先出”(FIFO—first in first out)的線性表,與棧(FILO-first in last out)剛好相反。
隊列的抽象是 Queue,而 LinkedList 是實現了 Deque 接口的,Deque 又繼承了 Queue,所以LinkedList 是可以當做隊列來使用的。
先看下 Queue 接口:
public interface Queue<E> extends Collection<E> {
// 增加一個元素到隊列尾部,如果隊列有大小限制,并且隊列已滿,會拋出異常 IllegalArgumentException
boolean add(E e);
// 增加一個元素到隊列尾部,和 add 不同的是:如果隊列有大小限制,并且隊列已滿,則返回 false,不拋出異常
boolean offer(E e);
// 檢索到隊列頭部元素,并且將其移出隊列。和 poll 方法不同的是如果隊列是空的,那么拋出 NoSuchElementException 異常
E remove();
// 檢索到隊列頭部元素,并且將其移出隊列。如果隊列是空的,那么返回 null;
E poll();
// 檢索隊列頭部的元素,并不會移除,和 peek 方法不同的是:如果隊列是空的,那么拋出 NoSuchElementException 異常;
E element();
// 檢索隊列頭部的元素,并不會移除,如果隊列是空的,那么返回 null;
E peek();
}
LinkedList 里面的實現
add 、offer
add 前面已經分析過了,這里來看下 offer:
public boolean offer(E e) {
return add(e);
}
前面隊列中的定義已經寫了,在 add 會在隊列滿的時候拋出異常,但是這個發現 offer 方法也調用的 add 方法,所以只是對 add 的一種包裝,實際使用效果是一樣的。這是為什么呢?
這是因為前面注釋里面講了,add 添加的時候拋出異常只會在隊列大小有限制的情況,在LinkedList 中并沒有設置大小的地方,所以也就不存在超過隊列大小的限制了。
remove 、poll
public E remove() {
return removeFirst();
}
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
同理,這兩個也不會拋出異常。
remove 會直接調用 removeFirst 從頭部移除元素,并且在 removeFirst 方法移除的過程中可能會拋出異常。
poll 則先把頭部元素取出來,進行判空。
如果為空,則返回 null,什么都不做,不會拋出異常,直接返回 null;
如果不為空,那么就執行 unlinkFirst(f) ,這個 unlinkFirst 前面已經講了,把頭部元素移除。
element、peek();
public E element() {
return getFirst();
}
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
和上面注釋的一樣,兩個都是取頭部元素,element 會拋出異常,peek 只是取頭部元素,不會拋出異常。
作為雙向隊列使用的一些方法
雙向隊列是隊列 (Queue) 的一個子接口 Deque,雙向隊列兩端的元素都可以入隊列和出隊列。可以實現先進先出或者先進后出的數據結構。
如果把 Deque 限制為只能從一端入隊和出隊,那么就可以實現 棧 的結構數據結構,遵循 先進后出 的規則。
如果不對 Deque 進行限制,用作雙向隊列,那么就是先進新出。
主要方法如下:
public interface Deque<E> extends Queue<E> {
// 將指定元素插入此雙端隊列的開頭(如果可以直接這樣做而不違反容量限制)
void addFirst(E e);
//將指定元素插入此雙端隊列的末尾(如果可以直接這樣做而不違反容量限制)。
void addLast(E e);
//在不違反容量限制的情況下,將指定的元素插入此雙端隊列的開頭。
boolean offerFirst(E e);
// 在不違反容量限制的情況下,將指定的元素插入此雙端隊列的末尾。
boolean offerLast(E e);
// 獲取并移除此雙端隊列第一個元素。
E removeFirst();
// 獲取并移除此雙端隊列的最后一個元素。
E removeLast();
//獲取并移除此雙端隊列的第一個元素;如果此雙端隊列為空,則返回 null。
E pollFirst();
//獲取并移除此雙端隊列的最后一個元素;如果此雙端隊列為空,則返回 null。
E pollLast();
// 獲取,但不移除此雙端隊列的第一個元素。
E getFirst();
// 獲取,但不移除此雙端隊列的最后一個元素。
E getLast();
// 獲取,但不移除此雙端隊列的第一個元素;如果此雙端隊列為空,則返回 null。
E peekFirst();
//獲取,但不移除此雙端隊列的最后一個元素;如果此雙端隊列為空,則返回 null。
E peekLast();
//從此雙端隊列移除第一次出現的指定元素。
boolean removeFirstOccurrence(Object o);
// 從此雙端隊列移除最后一次出現的指定元素。
boolean removeLastOccurrence(Object o);
// *** Queue methods ***
// 將指定元素插入此雙端隊列所表示的隊列(換句話說,此雙端隊列的尾部),如果可以直接這樣做而不違反容量限制的話;
// 如果成功,則返回 true,如果當前沒有可用空間,則拋出 IllegalStateException。
boolean add(E e);
// 將指定元素插入此雙端隊列所表示的隊列(換句話說,此雙端隊列的尾部),如果可以直接這樣做而不違反容量限制的話;
// 如果成功,則返回 true,如果當前沒有可用的空間,則返回 false。
boolean offer(E e);
//獲取并移除此雙端隊列所表示的隊列的頭部(換句話說,此雙端隊列的第一個元素)。
E remove();
//獲取并移除此雙端隊列所表示的隊列的頭部(換句話說,此雙端隊列的第一個元素);如果此雙端隊列為空,則返回 null。
E poll();
//獲取,但不移除此雙端隊列所表示的隊列的頭部(換句話說,此雙端隊列的第一個元素)。
E element();
//獲取,但不移除此雙端隊列所表示的隊列的頭部(換句話說,此雙端隊列的第一個元素);如果此雙端隊列為空,則返回 null。
E peek();
// *** Stack methods ***
// 將一個元素推入此雙端隊列所表示的堆棧(換句話說,此雙端隊列的頭部),如果可以直接這樣做而不違反容量限制的話;
// 如果成功,則返回 true,如果當前沒有可用空間,則拋出 IllegalStateException。
void push(E e);
// 從此雙端隊列所表示的堆棧中彈出一個元素
E pop();
// *** Collection methods ***
// 從此雙端隊列中移除第一次出現的指定元素
boolean remove(Object o);
// 是否包含一個元素
boolean contains(Object o);
// 隊列大小
int size();
// 返回此雙端隊列的迭代器
Iterator<E> iterator();
// 返回一個迭代器,該迭代器具有此雙端隊列的相反順序
Iterator<E> descendingIterator();
}
具體的就不在分析,在 LinkedList 無非是比隊列多一些操作而已,有興趣的可以去看下關于雙端隊列相關的部分源碼。
總結
- LinkedList 底層數據結構是雙向鏈表,但是頭節點不存放數據,只有后置節點的引用;
- 集合中的元素允許為 null,可以看到源碼中在查找和刪除時,都劃分為該元素為null和不為null兩種情況來處理。
- 允許內部元素重復
- 不存在擴容問題,所以是沒有擴容的方法
- 元素在內部是有序存放的,依次在鏈表上添加節點
- 實現了棧和隊列的操作方法,因此也可以作為棧、隊列和雙端隊列來使用
- 由于是鏈表實現,并且沒有實現RandomAccess ,雖然在查找的時候,會先判斷是在前半部分或者后半部分,然后依次從前或者從后查找,但是查找效率還是很低,不過增刪效率高,但是查找和修改大部分情況下不如 ArrayList。
- 線程不安全,可以用個 Collections.SynchronizedList(new LinkedList()) 返回一個線程安全的 LinkedList
大多數情況下使用 ArrayList 即可,其他的還是根據具體的業務場景根據兩者的不同特性進行不同的選擇。
關于 ArrayList 和 LinkedList 的性能對比,可以參考這篇文章