Java集合框架分析(三)LinkedList分析

本篇文章主要分析一下 Java 集合框架中的 List 部分,LinkedList,該源碼分析基於JDK1.8,分析工具,AndroidStudio,文章分析不足之處,還請指正!

LinkedList簡介

類結構

首先,我們來看下 LinkedList 的類繼承結構:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

從上面我們可以看出來 LinkedList 是一個繼承於 AbstractSequentialList 的雙向鏈表。它也可以被當作堆棧、隊列或雙端隊列進行操作。 實現 List 接口,能對它進行隊列操作。實現 Deque 接口,即能將 LinkedList 當作雙端隊列使用。實現了 Cloneable 接口,即覆蓋了函數clone(),能克隆。實現 java.io.Serializable 接口,這意味着 LinkedList 支持序列化,能通過序列化去傳輸。同時 LinkedList 是非同步的。

數據結構

接下來我們來看看LinkedList的數據結構是什麼樣的。我們在分析 LinkedHashMap 的時候也會發現它的底層包含一個雙向循環鏈表,而 LinkedList 也是的,LinkedList 底層的數據結構是基於雙向循環鏈表的,且頭結點中不存放數據,如下:
在這裏插入圖片描述

圖上便是 LinkedList 的底層結構圖,基於雙向循環鏈表結構設計的。它的每一個節點都包含數據信息,上一個節點位置信息和下一個節點位置信息。

源碼分析

接下來我們分析 LinkedList 的源碼,首先來看下它內部申明的屬性。

//鏈表節點的數量
transient int size = 0;
//頭結點
transient Node<E> first;
//下一個節點
transient Node<E> last;

變量申明比較簡單,接着我們分析構造函數。如果有不大理解的,我們可以先分析後面的代碼,就能知道這些屬性的意思了。

構造函數

public LinkedList() {
   }
   
   public LinkedList(Collection<? extends E> c) {
       this();
       addAll(c);
   }

構造函數也比較簡單,沒啥可說的,我們接着分析。一開始介紹的時候我們說明了底層是雙向循環鏈表,所以肯定有節點信息,所以我們來看下它的節點數據結構。

節點信息

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;
       }
   }

節點數據結構,很簡單,一個是數據信息 item,一個是前一個位置節點 prev,另一個是後一個位置節點 next,非常簡單。接下來我們繼續查看添加元素 add 方法

add(E e)

public boolean add(E e) {
       linkLast(e);
       return true;
   }

添加數據 add 方法比較簡單,add 函數用於向 LinkedList 中添加一個元素,並且添加到鏈表尾部。我們來看看 linkLast 方法怎麼添加數據的。

//尾部添加數據e
   void linkLast(E e) {
    //保存尾部節點
       final Node<E> l = last;
       //新生成結點的前驅爲l,後繼爲null
       final Node<E> newNode = new Node<>(l, e, null);
       // 重新賦值尾結點
       last = newNode;
       // 尾節點爲空,賦值頭結點
       if (l == null)
           first = newNode;
       else
        // 尾結點的後繼爲新生成的結點
           l.next = newNode;
       //節點數量增加1
       size++;
       modCount++;
   }

上面這段代碼是什麼意思呢?我們來通過 demo 來簡單說明一下。

List<String> mLinkedList = new LinkedList();
          mLinkedList.add("A");
          mLinkedList.add("B");

首先我們先申明一個 LinkedList 類型的變量 mLinkedList,然後依次添加進兩個數據 “A” 和 “B”,這個時候,它的內部結構是怎麼操作的呢?

我們首先調用 LinkedList 的無參構造函數,進行初始化,這個時候裏面的節點都是 null,因爲沒有數據,其次,我們通過 add 添加進一條數據 “A”,這個時候結合上面的源代碼 add 方法,來解讀一下,首先會調用 linkLast 方法,其次判斷它的 last 節點是否爲null,由於是剛初始化的,所以 last=null,這樣就會調用 first = newNode;這個方法,也就是在開頭節點處插入一條數據,其次再調用 add(“B”),再次插入一條數據,我們循環上面那段代碼,發現這個時候 last!=null 了,所以調用 last.next=newNode 代碼,也就是在第一個節點的後面插入一條數據。

我們接着分析 add 其他的重載方法。

//在指定的位置上面插入一條數據
public void add(int index, E element) {
       checkPositionIndex(index);
       if (index == size)
           linkLast(element);
       else
           linkBefore(element, node(index));
   }

這個方法也不是很複雜,我們依次分析,首先判斷一下,需要插入的 index 是否越界。在插入之前我們需要先找到待插入位置的 node,通過 node(index) 方法獲得,我們看下

//判斷index是在鏈表的中部之前還是後面,如果在 1/2 前的話,從頭開始遍歷,如果在 1/2 後的話,從後往前遍歷

Node<E> node(int index) {
	//從前往後遍歷獲取index出的node
       if (index < (size >> 1)) {
           Node<E> x = first;
           for (int i = 0; i < index; i++)
               x = x.next;
           return x;
       } else {
        //從後往前遍歷獲取index出的node
           Node<E> x = last;
           for (int i = size - 1; i > index; i--)
               x = x.prev;
           return x;
       }
   }

上面很簡單,爲了提高搜索效率,先判斷是在 1/2 處的前面還是後面,然後依次循環獲得 index處的 node,我們接着分析

private void checkPositionIndex(int index) {
       if (!isPositionIndex(index))
           throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
   }
//不在[0,size]範圍內越界
private boolean isPositionIndex(int index) {
       return index >= 0 && index <= size;
   }

其次根據 index 判斷是在鏈表中的哪個地方插入數據,是尾部還是在前面位置,如果是在尾部的話,我們已經分析過了,我們來看下在前面插入數據的情況。

void linkBefore(E e, Node<E> succ) {
       //保存目標index處的前置節點
       final Node<E> pred = succ.prev;
       final Node<E> newNode = new Node<>(pred, e, succ);
       succ.prev = newNode;
       //如果是在開頭處插入的話
       if (pred == null)
           first = newNode;
       else
           pred.next = newNode;
       size++;
       modCount++;
}

我們逐一分析,首先我們獲取到了 index 處的節點 C,然後保存 C 的前置節點爲 pred (也就是 B 節點),然後我們用待插入的節點構建一個新的節點 (newNode)(新節點的前置節點是 pred,後繼節點是 C),然後我們將 index 處的節點 C 的前置節點設置爲待插入的節點,然後判斷這個 index 處的 node 是否是第一個節點,不是的話就將原先 index 處的 node 的前置節點的後繼節點設置爲待插入的節點。這裏面主要涉及到鏈表的插入操作,如果對於這不熟悉的話,可以先去複習一下鏈表的相關操作。

分析完了 add 方法,我們再來分析一下 addAll 方法。

public boolean addAll(Collection<? extends E> c) {
       return addAll(size, c);
   }

很簡單,在鏈表尾部插入一個集合,我們看一下 addAll 的重載方法,還是蠻長的,我們逐行看一下:

public boolean addAll(int index, Collection<? extends E> c) {
       //是否越界判斷
       checkPositionIndex(index);
       Object[] a = c.toArray();
       int numNew = a.length;
       if (numNew == 0)
           return false;
	
	//保存前置和後繼節點
       Node<E> pred, succ;
       //判斷是在尾部插入還是在中間或者頭部插入
       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.next = newNode;
		//將新添加進來的節點保存到結尾
           pred = newNode;
       }
	
	
       if (succ == null) {
           last = pred;
       } else {
        //中間保存後面的鏈表數據
           pred.next = succ;
           succ.prev = pred;
       }
       size += numNew;
       modCount++;
       return true;
   }

以上便是 add 的方法內容,思想其實也簡單的,類似單個插入,主要就是判斷在鏈表尾部還是中間或者頭部插入,然後再依次插入即可。

get(int index)

插入數據分析完之後,我們就開始分析獲取數據 get。

public E get(int index) {
       checkElementIndex(index);
       return node(index).item;
   }

get 方法也是隻有簡單的兩行,我們依次分析,首先分析第一行,就是判斷索引的位置是否越界,

private void checkPositionIndex(int index) {
       if (!isPositionIndex(index))
           throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
   }
private boolean isPositionIndex(int index) {
       return index >= 0 && index <= size;
   }

很簡單,看代碼就知道了,不用分析了,接着分析第二行,第二行上面也已經分析過了,所以這裏就不用再多敘述了。get 方法總體而言很簡單。我們接着分析。

remove(int index)

我們接着來分析一下刪除數據的代碼。

public E remove(int index) {
       checkElementIndex(index);
       return unlink(node(index));
   }

代碼也很簡單,首先是檢查索引是否越界,其次我們分析第二行,我們發現 node(index) 的意思就是獲取鏈表中指定位置上面的 node,所以我們看看 unlink 方法內容。

E unlink(Node<E> x) {
 
      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;
          x.prev = null;
      }
//尾節點刪除
      if (next == null) {
          last = prev;
      } else {
    //要刪除節點的的後繼節點設置爲要刪除節點的前置節點
          next.prev = prev;
          x.next = null;
      }
//gc
      x.item = null;
      size--;
      modCount++;
      return element;
  }

上面主要是進行雙向鏈表的刪除操作。我們接着分析它的重載方法,

//刪除指定元素,默認從first節點開始,刪除第一次出現的那個元素
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;
   }

相應代碼已經分析過了,我們就不再說了。我們分析一下 set 方法

public E set(int index, E element) {
      checkElementIndex(index);
      Node<E> x = node(index);
      E oldVal = x.item;
      x.item = element;
      return oldVal;
  }

首先是判斷是否越界,然後獲取需要替換的位置上面的 node,然後 update 一下,返回舊的數據。

我們來分析一下清空鏈表的方法 clear

clear

public void clear() {
       //遍歷鏈表,依次設置爲null
       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++;
   }

基本上,LinkedList 的方法分析完了,還有一些簡單的方法,我們一併列出來,做個簡單的註釋。

在首節點處添加一個數據

//在首節點處添加一個數據
   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++;
   }
   
public void addFirst(E e) {
       linkFirst(e);
   }

在尾節點插入一個節點

//在尾節點插入一個節點
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++;
   }
   
 public void addLast(E e) {
       linkLast(e);
   }

移除第一個節點

//移除第一個節點
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;
   }
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);
   }

返回第一個節點

//返回第一個節點
 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;
   }

是否包含某個節點信息

//是否包含某個節點信息
public boolean contains(Object o) {
       return indexOf(o) != -1;
   }

遍歷鏈表,存在則返回索引不存在則返回-1

//遍歷鏈表,存在則返回索引不存在則返回-1
public int indexOf(Object o) {
       int index = 0;
       if (o == null) {
           for (Node<E> x = first; x != null; x = x.next) {
               if (x.item == null)
                   return index;
               index++;
           }
       } else {
           for (Node<E> x = first; x != null; x = x.next) {
               if (o.equals(x.item))
                   return index;
               index++;
           }
       }
       return -1;
   }

從後往前遍歷,第一次出現則返回索引,不存在返回-1

//從後往前遍歷,第一次出現則返回索引,不存在返回-1
public int lastIndexOf(Object o) {
       int index = size;
       if (o == null) {
           for (Node<E> x = last; x != null; x = x.prev) {
               index--;
               if (x.item == null)
                   return index;
           }
       } else {
           for (Node<E> x = last; x != null; x = x.prev) {
               index--;
               if (o.equals(x.item))
                   return index;
           }
       }
       return -1;
   }

返回鏈表第一個節點,但是不刪除節點

//返回鏈表第一個節點,但是不刪除節點
 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);
   }

//省略迭代部分的源碼…

以上便是 LinkedList 的基本源碼,我們基本都給出了註釋,相關重要的功能都加以圖解,接下來我們來總結一下關於 LinkedList 相關重要知識。

總結

從源碼中可以看出,LinkedList 的實現是基於雙向循環鏈表的。卻別與 ArrayList 的數組,以及 HashMap 的線性表和散列表以及 LinkedHashMap 的線性表和散列表以及雙向循環鏈表。
我們在搜索鏈表中的數據時,都會進行判斷是否爲 null 的情況,所以 LinkedList 是允許元素爲 null 的情況的。

LinkedList 是基於鏈表實現的,因此不存在容量不足的問題,所以這裏沒有擴容的方法。
源碼中還實現了棧和隊列的操作方法,因此也可以作爲棧、隊列和雙端隊列來使用。
LinkedList 是一個繼承於 AbstractSequentialList 的雙向鏈表。它也可以被當作堆棧、隊列或雙端隊列進行操作。 實現 List 接口,能對它進行隊列操作。實現 Deque 接口,即能將 LinkedList 當作雙端隊列使用。實現了 Cloneable 接口,即覆蓋了函數 clone(),能克隆。實現 java.io.Serializable 接口,這意味着 LinkedList 支持序列化,能通過序列化去傳輸。同時LinkedList 是非同步的。

關於作者

專注於 Android 開發多年,喜歡寫 blog 記錄總結學習經驗,blog 同步更新於本人的公衆號,歡迎大家關注,一起交流學習~

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章