LinkedList實現原理
LinkedList實現原理
LinkedList
LinkedList都是List下面的集合子類,LinkedList中和ArrayList在實現上有很大的區別,ArrayList是維護了一個Object的陣列,而LinkedList在實現上維護了一個頭指標節點物件Node first 和一個尾部指標節點物件Node last,而Node物件是LinkedList的靜態內部類,為什麼要是靜態內部類?因為防止記憶體洩露,因為如果是普通的內部類,那麼就會被其他物件鎖例項化,就會出現記憶體洩露;
在上一篇筆記中,提到了ArrayList和LinkedList的比較,其實在很多場景下,ArrayList用的比較多,而LinkedList使用的場景比較少,因為LinkedList和ArrayList在新增的操作中,資料量沒有達到一定的級別,效能是沒有ArrayList好的,都說LinkedList是使用在寫多讀上的場景下,但是在資料量沒有達一定的情況下,還是ArrayList效能較好;ArrayList 新增大批量資料的時候,耗時主要在資料的動態擴容進行陣列複製的時候比較消耗效能,而LinkedList是每一個新增元素的時候都會建立一個Node節點物件,相當於就是說使用LinkedList的時候,要建立很多Node節點,而這些節點不像普通陣列那樣會存放在一塊連續的記憶體空間中,LinkedList建立的Node物件在記憶體中各個區域中,通過前後指標進行迭代訪問。
LinkedList原始碼分析
LinkedList本身也是List的子類,實現了List介面,具有List中的末尾新增元素、按位置新增元素、刪除元素、迭代元素,而LinkedList還提供了從指定位置進行迭代的功能。
我們都知道ArrayList是通過Object陣列來儲存元素的,而LinkedList是通過Node來儲存元素的,我的理解是這些Node都分配在不連續的空間上面,就是散落在記憶體塊Cell中,當我們要用的時候通過遊標來獲取,我的理解如下:
LinkedList的繼承關係
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
繼承了AbstractSequentialList,本身和AbstractList沒有什麼區別,但是AbstractSequentialList完善了AbstractList中沒有的功能,ArrayList繼承之AbstractList,而AbstractSequentialList是AbstractList的子類,所以AbstractSequentialList就是對AbstractLIst中沒有的功能進行完善。
其中的Cloneable和Serializable和ArrayList一樣,都是標記介面,作用是標記可以複製克隆和序列化。
Deque:實現了Collection中的大家庭介面Queue佇列介面。說明了它具有雙端佇列的功能(LinkedLIst和ArrayList的最大區別就是LinkedList實現了Deque介面,使的LinkedList具有了雙端佇列的功能)
基本屬性
我這邊提一下transient的作用,簡單點說就是這個關鍵字就是序列化的作用域,加了這個關鍵字,那麼被修飾的屬性就不會被序列化了。為什麼呢?因為我們的size是根據我們的集合大小而動態變化的,我們如果序列化出來過後,或者clone出來過後都是初始為0,我們不可能我們對集合序列化過後得到的是0,而且這樣就節省空間,所以加了這個關鍵字就不會被序列化出來了。
//當前集合有多少個元素,泛指集合大小
transient int size = 0;
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
//集合中的第一個元素
transient Node<E> first;
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
//集合中最後一個元素
transient Node<E> last;
//靜態內部類,為了防止記憶體洩漏,設定為靜態內部類
private static class Node<E> {
E item;//節點中的具體元素資料
Node<E> next;//後繼指標遊標
Node<E> prev;//前驅指標遊標
//一般通過Node構造來建立Node節點物件
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
構造方法
//這個構造方法是預設將一個集合轉換成一個LinkedList集合,
//完成過後,原有的集合物件就轉成了LinkedList物件了
public LinkedList(Collection<? extends E> c) {
this();//呼叫預設的構造,現在預設的構造是一個空殼方法
//將集合全部新增到LinkedList中
addAll(c);
}
addAll方法
public boolean addAll(Collection<? extends E> c) {
//這裡會把當前的size傳入,如果是通過構造方法進入的,
//那麼這個size是為0、
//如果是先建立了一個LinkedList過後,並且已經新增了元素過後,
//再來呼叫這個addall,
//那麼這個size就不是空的,這個size就等於LinkedLIst的集合大小
return addAll(size, c);
}
//真正的新增集合到LinkedList的核心方法
public boolean addAll(int index, Collection<? extends E> c) {
//這裡檢查下標是否越界的方法
//判斷的核心就是當前index是否是大於等於0並且小於等於size的
//因為這個方法入口不僅僅是通過構造呼叫的,還可以通過外部的
//LinkedList物件進行呼叫,因為這個方法是public
checkPositionIndex(index);
//先將要新增到LinkedList的集合collection轉成一個普通的Object陣列
Object[] a = c.toArray();
//得到轉換過後的陣列長度
int numNew = a.length;
//如果陣列是等於0的,那麼要新增的集合是空的,就不用執行後面的方法
if (numNew == 0)
return false;
//什麼Node節點的前驅pre和後繼next
Node<E> pred, succ;
//如果index == size,那麼就有兩種可能
//1.通過構造方法進行呼叫,這個時候index和size都是0
//2.通過外部物件呼叫時,從尾部進行新增,也就是傳入的index
//是通過LinkedList的物件.size()獲取到的,就是新增到尾部
if (index == size) {
//這裡如果是通過構造進來的,那麼succ=last=first=null
//如果是通過外部呼叫的,那麼就是last=pred,也就是說
//從尾部開始新增,尾部的最後一個元素為前驅
succ = null;
pred = last;
} else {
//否則的話,從集合的某一個位置進行新增的,先獲取到這個位置的
//遊標指標,找到過後把遊標指標的前驅賦值給pred
succ = node(index);
pred = succ.prev;
}
for (Object o : a) {
//e=Node.item
@SuppressWarnings("unchecked") E e = (E) o;
//建立一個Node物件,設定前驅,元素,後驅=null
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
//如果pred為空,則證明是通過構造呼叫的,last=first=null
//新建立Node賦值給first,這裡也只有第一次能進入,第二次
//迴圈就不會進到這裡了
first = newNode;
else
//當前元素指向pred的後繼
pred.next = newNode;
//將當前建立的node賦值給pred,作為下一個Node前驅
//這裡很重要,這塊要自己去意會設計者的思路
//比如方法是通過構造進來的:那麼pred肯定是空的,
//所以第一個節點first也就是第一次迴圈建立的node,
//把當前建立的node作為Node的第一個元素node,而第二次迴圈開始
//每個節點的next為新建立的node,而上一個元素node作為下一個、
//元素pred,這塊只有去意會,好好理解一番,從你是設計者的角度
//去思考為什麼要這麼做
pred = newNode;
}
if (succ == null) {
//如果succ為空,則有兩種情況:、
//1.從構造呼叫進來,那麼first=last=null,
//當執行完成過後,last就是pred
//第二種情況是從尾部新增的
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
//元素個數計算
size += numNew;
//運算元+1,記錄集合的運算元
modCount++;
return true;
}
Node<E> node(int index) {
// assert isElementIndex(index);
// if (index < (size >> 1)) 這行程式碼就是程式碼設計者的高明之處
//說白了就是通過看你的index到底是在集合的前半部分還是後半部分
//簡單來說設計者是這樣考慮的:比如集合是20,你傳入的index是3
//那麼我就從開頭開始尋找,如果你傳入的是15,那麼我就從後面開始尋找
//就是定址快,能快速找到我們的Node節點
if (index < (size >> 1)) {//二分查詢方法
//如果index小於size的一半,那麼從first開始找
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;
}
}
Add
//新增元素,從尾部新增
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
//新增元素,判斷是否是第一個元素,如果是LinkedList
//建立過後的第一次新增,這first就是當前建立的節點node
//否則設定下一個節點也就是last.next就是當前建立的節點物件newNode
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指定位置新增
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
//指位新增方法核心邏輯 操作新節點,
//緊接修改原有節點的前驅屬性,最後再修改前驅節點的後繼屬性
void linkBefore(E e, Node<E> succ) {
final Node<E> pred = succ.prev;//原位置節點的前驅pred
final Node<E> newNode = new Node<>(pred, e, succ);//建立新節點,設定新節點其前驅為原位置節點的前驅pred,其後繼為原位置節點succ
succ.prev = newNode;//將新節點設定到原位置節點的前驅
if (pred == null)//前驅如果為空,空連結串列,則新節點設定為first
first = newNode;
else
pred.next = newNode;//將新節點設定到前驅節點的後繼
size++;//修改當前list的size
modCount++;//記錄該list物件被執行修改的次數。
}
final修飾,不希望在執行時對變數做重新賦值
LinkedList 在插入資料優於ArrayList ,主要是因為他只需要修改指標的指向即可,而不需要將整個陣列的資料進行轉移。而LinkedList 由於沒有實現 RandomAccess,或者說不支援索引搜尋的原因,他在查詢元素這一操作,需要消耗比較多的時間進行操作(n/2)。
GET
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
從上面的獲取元素get方法就可以看出來為什麼LinkedList的讀取效率不如ArrayList了,每一次LinkedList都要通過index去尋找Node,雖然已經做了優化處理,比如是從前還是從後去找,但是肯定是沒有ArrayList直接通過下標獲取元素來的快,所以|ArrayList實現了RandomAccess隨機訪問標記介面,而LinkedList是沒有的。
SET
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
在指定位置替換原有的元素,太簡單了,不做介紹了,就是原來位置=3的地方放入的是“abc”,而通過set(3,“ert”)成功過後,位置還是3,值變成了ert.
通過指定位置迭代
public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);
return new ListItr(index);
}
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned;
private Node<E> next;
private int nextIndex;
private int expectedModCount = modCount;
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}
public boolean hasNext() {
return nextIndex < size;
}
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
public boolean hasPrevious() {
return nextIndex > 0;
}
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
lastReturned = next = (next == null) ? last : next.prev;
nextIndex--;
return lastReturned.item;
}
public int nextIndex() {
return nextIndex;
}
public int previousIndex() {
return nextIndex - 1;
}
public void remove() {
checkForComodification();
if (lastReturned == null)
throw new IllegalStateException();
Node<E> lastNext = lastReturned.next;
unlink(lastReturned);
if (next == lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}
public void set(E e) {
if (lastReturned == null)
throw new IllegalStateException();
checkForComodification();
lastReturned.item = e;
}
public void add(E e) {
checkForComodification();
lastReturned = null;
if (next == null)
linkLast(e);
else
linkBefore(e, next);
nextIndex++;
expectedModCount++;
}
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (modCount == expectedModCount && nextIndex < size) {
action.accept(next.item);
lastReturned = next;
next = next.next;
nextIndex++;
}
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
迭代都差不多,都有一個共同特徵就是檢查異常,fast-fail快速失敗
linkedlist提供了向前迭代和向後迭代,這是和ArrayList的主要區別之一。
刪除元素
1.AbstractSequentialList的remove
public E remove(int index) {
checkElementIndex(index);
//node(index)找到index位置的元素
return unlink(node(index));
}
//remove(Object o)這個刪除元素的方法的形參o是資料本身,而不是LinkedList集合中的元素(節點),所以需要先通過節點遍歷的方式,找到o資料對應的元素,然後再呼叫unlink(Node x)方法將其刪除
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) {
//x的資料域element
final E element = x.item;
//x的下一個結點
final Node<E> next = x.next;
//x的上一個結點
final Node<E> prev = x.prev;
//如果x的上一個結點是空結點的話,那麼說明x是頭結點
if (prev == null) {
first = next;
} else {
prev.next = next;//將x的前後節點相連 雙向連結串列
x.prev = null;//x的屬性置空
}
//如果x的下一個結點是空結點的話,那麼說明x是尾結點
if (next == null) {
last = prev;
} else {
next.prev = prev;//將x的前後節點相連 雙向連結串列
x.next = null;
}
x.item = null;//指向null 方便GC回收
size--;
modCount++;
return element;
}
2.Deque 中的Remove
//將first 節點的next 設定為新的頭節點,然後將 f 清空。 removeLast 操作也類似。
private E unlinkFirst(Node<E> f) {
final E element = f.item;
//獲取到頭結點的下一個結點
final Node<E> next = f.next;
f.item = null;
f.next = null; // 方便 GC
//頭指標指向的是頭結點的下一個結點
first = next;
//如果next為空,說明這個連結串列只有一個結點
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
雙端連結串列(佇列Queue)
java中佇列的實現就是LinkedList: 我們之所以說LinkedList 為雙端連結串列,是因為他實現了Deque 介面;我們知道,佇列是先進先出的,新增元素只能從隊尾新增,刪除元素只能從隊頭刪除,Queue中的方法就體現了這種特性。 支援佇列的一些操作,我們來看一下有哪些方法實現:
pop()是棧結構的實現類的方法,返回的是棧頂元素,並且將棧頂元素刪除
poll()是佇列的資料結構,獲取對頭元素並且刪除隊頭元素
push()是棧結構的實現類的方法,把元素壓入到棧中
peek()獲取隊頭元素 ,但是不刪除佇列的頭元素
offer()新增隊尾元素
可以看到Deque 中提供的方法主要有上述的幾個方法,接下來我們來看看在LinkedList 中是如何實現這些方法的。
1.佇列的增
offer()新增隊尾元素
public boolean offer(E e) {
return add(e);
}
具體的實現就是在尾部新增一個元素
2、佇列的刪
poll()是佇列的資料結構,獲取對頭元素並且刪除隊頭元素
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
具體的實現前面已經講過,刪除的是佇列頭部的元素
3.佇列的查
peek()獲取隊頭元素 ,但是不刪除佇列的頭元素
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
4.棧的增
push()是棧結構的實現類的方法,把元素壓入到棧中
push() 方法的底層實現,其實就是呼叫了 addFirst(Object o)
public void push(E e) {
addFirst(e);
}
5.棧的刪
pop()是棧結構的實現類的方法,返回的是棧頂元素,並且將棧頂元素刪除
public E pop() {
return removeFirst();
}
public E removeFirst() {
final Node f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
相關文章
- LinkedList 的實現原理
- LinkedList的底層實現
- ArrayList和LinkedList如何實現的?
- 資料結構--LinkedList的實現資料結構
- 初識LinkedList底層原理
- Java集合 LinkedList的原理及使用Java
- 原始碼閱讀之LinkedList實現細節原始碼
- Java集合詳解(三):LinkedList原理解析Java
- 資料結構分析及其實現(Stack、Queue、Tree、LinkedList)資料結構
- block實現原理BloC
- ReentrantLock實現原理ReentrantLock
- synchronized實現原理synchronized
- AsyncTask實現原理
- jQuery實現原理jQuery
- Synchronized 實現原理synchronized
- Condition實現原理
- AQS實現原理AQS
- 寫一個簡單的Linkedlist,實現增刪改查
- AOP如何實現及實現原理
- 【Redis系列3】Redis列表物件之linkedlist(雙端列表)和ziplist(壓縮列表)及quicklick(快速列表)實現原理分析Redis物件UI
- 「必知必會」最細緻的 LinkedList 原理分析
- MySQL——索引實現原理MySql索引
- Greys主要實現原理
- Spring AOP實現原理Spring
- (201)Atomic*實現原理
- 水波圖實現原理
- Category的實現原理Go
- Vitepress 的實現原理Vite
- 【Spring】AOP實現原理Spring
- golang reflect 實現原理Golang
- Python 字典實現原理Python
- synchronized 的實現原理synchronized
- RunTime實現原理剖析
- 理解 Block 實現原理BloC
- JAVA AQS 實現原理JavaAQS
- 前端路由實現原理前端路由
- Lombok 原理與實現Lombok
- MySQL MVCC實現原理MySqlMVC