前面在分析ArrayList
、Vector
容器的源碼時,發現的底層實現原理都是維護一個數組,並且自動調整數組的大小(擴容、縮小),隨機查找
效率高,但是插入
、刪除
操作效率低。在此篇博客中,博主將帶領各位小夥伴也看看LinkedList
容器的實現原理,它又有什麼優勢呢,它到底是單鏈表
還是雙鏈表
實現呢?
註明:以下源碼分析都是基於jdk 1.8.0_221
版本
Java容器之LinkedList源碼分析目錄
一、LinkedList
容器概述
LinkedList
類的申明如下:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
Java
中的LinkedList
容器既實現了List
接口,又實現了Deque
接口,所以LinkedList
即可當做List
容器使用,又可以當做雙端隊列
使用。ArrayList
、Vector
容器底層通過維護一個數組存放數據,而LinkedList
容器通過維護一個雙鏈表
存放數據。
二、LinkedList
類中的主要屬性與內部類
1、Node
內部類
Node
是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;
}
}
2、LinkedList
類中的主要屬性
/**
* 鏈表的長度
*/
transient int size = 0;
/**
* 指向鏈表的頭結點
*/
transient Node<E> first;
/**
* 指向鏈表的尾結點
*/
transient Node<E> last;
三、LinkedList
類的構造方法
由於LinkedList
類的屬性比較簡單,所以構造器也沒啥要初始化的。
public LinkedList() {
}
/**
* 複製構造器
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
四、查找
相關的方法
1、get
方法
/**
* 通過下標獲取容器中的元素
*/
public E get(int index) {
// 檢查下標的合法性[0, size)
// 肯定有不少小夥伴蒙圈了,不是說LinkedList是鏈表麼,怎麼能按小標取值呢?把表頭看成下標0,對應的index只要往後移動index此即可
checkElementIndex(index);
return node(index).item;
}
/**
* 通過下標取出鏈表中的節點
*/
Node<E> node(int index) {
// 如果 index < size / 2,則從first頭結點開始往後移動
if (index < (size >> 1)) {
Node<E> x = first;
// first看做下標0對應的位置
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
// 否則從尾節點last,從後往前移動
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
五、插入
相關的方法
1、add
方法
/**
* 從尾端插入元素
*/
public boolean add(E e) {
// 調用linkLast插入尾端
linkLast(e);
return true;
}
/**
* 將元素插入尾端
*/
void linkLast(E e) {
final Node<E> l = last;
// Node(pre指向,當前元素,next指向)
final Node<E> newNode = new Node<>(l, e, null);
// 更新尾指針指向新插入的節點
last = newNode;
// 如果l(之前的last)爲null,說明之前的鏈表爲空,更新first,否則更新l的next指向newNode
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
// 插入元素(結構性調整),此變量在AbstractList類中定義
modCount++;
}
/**
* 指定插入的下標
*/
public void add(int index, E element) {
// 檢查下標是否合法
checkPositionIndex(index);
if (index == size)
// 下標 == size,表示插入尾端
linkLast(element);
else
// 否則插入node(index)的前面
linkBefore(element, node(index));
}
/**
* 插入指定節點的前面
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
// Node(新建節點pre指向,新建元素,新建節點next指向)
final Node<E> newNode = new Node<>(pred, e, succ);
// 然後需要將succ.prev節點.next指向e,succ節點的pre指向e
succ.prev = newNode;
// 如果succ.prev == null,表示succ爲頭結點,而e是插在succ的前面,此時e爲頭節點
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
六、刪除
相關的方法
1、remove
方法
/**
* 刪除指定下標的節點
*/
public E remove(int index) {
// 檢查下標合法性[0, size)
checkElementIndex(index);
// 調用unlink方法,將節點node(index)移除鏈表
return unlink(node(index));
}
/**
* 將節點x移出鏈表
*/
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
// 記錄節點x的prev指向節點、next指向節點
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
// 節點x沒有prev,說明當前節點是頭結點
first = next;
} else {
// 否則更新prev.next指向x.next
prev.next = next;
// 釋放x的prev
x.prev = null;
}
if (next == null) {
// 節點x沒有next,說明當前節點是尾結點
last = prev;
} else {
// 否則更新next.prev指向x.prev
next.prev = prev;
// 釋放x的next
x.next = null;
}
// 釋放x指向的節點
x.item = null;
size--;
modCount++;
return element;
}
七、其它方法
LinkedList
實現了Deque
雙端隊列接口(頭、尾部都可以進行出隊、入隊)。
/**
* 返回頭部節點
*/
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
/**
* 頭部節點出隊
*/
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
/**
* 返回頭部節點
*/
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
/**
* 返回尾部節點
*/
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}
/**
* 移除頭部節點
*/
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
/**
* 移除尾部節點
*/
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
八、總結
LinkedList
容器底層實現用的是鏈表,所以節點插入、刪除只需要改變指針的指向,並不需要複製其它節點,所以效率較高,但是隨機訪問(按下標訪問)需要從頭節點(爲節點)一個個移動,效率比數組訪問低。因此,如果你需要做大量的插入、刪除,可選擇LinkedList
容器,如果容器的元素基本不變,有大量的隨機訪問操作,可選擇ArrayList
容器,如果還需要支持併發讀寫,可選擇Vector
容器。