Java集合源碼分析(二)Linkedlist


前言

  前面一篇我們分析了ArrayList的源碼,這一篇分享的是LinkedList。我們都知道它的底層是由鏈表實現的,所以我們要明白什么是鏈表?

一、LinkedList簡介

1.1、LinkedList概述

  

  LinkedList是一種可以在任何位置進行高效地插入和移除操作的有序序列,它是基於雙向鏈表實現的

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

1.2、LinkedList的數據結構

  1)基礎知識補充

    1.1)單向鏈表:

      element:用來存放元素

      next:用來指向下一個節點元素

      通過每個結點的指針指向下一個結點從而鏈接起來的結構,最后一個節點的next指向null

      

    1.2)單向循環鏈表

      element、next 跟前面一樣

      在單向鏈表的最后一個節點的next會指向頭節點,而不是指向null,這樣存成一個環

      

    1.3)雙向鏈表

      element:存放元素

      pre:用來指向前一個元素

      next:指向后一個元素

      雙向鏈表是包含兩個指針的,pre指向前一個節點,next指向后一個節點,但是第一個節點head的pre指向null,最后一個節點的tail指向null。

      

    1.4)雙向循環鏈表

      element、pre、next 跟前面的一樣

      第一個節點的pre指向最后一個節點,最后一個節點的next指向第一個節點,也形成一個“環”

      

  2)LinkedList的數據結構

    

    如上圖所示,LinkedList底層使用的雙向鏈表結構,有一個頭結點和一個尾結點,雙向鏈表意味着我們可以從頭開始正向遍歷,或者是從尾開始逆向遍歷,並且可以針對頭部和尾部進行相應的操作。

1.3、LinkedList的特性

  在我們平常中我們只知道一些常識性的特點:

    1)是通過鏈表實現的,

    2)如果在頻繁的插入,或者刪除數據時,就用linkedList性能會更好。

  那我們通過API去查看它的一些特性

    1)Doubly-linked list implementation of the List and Deque interfaces. Implements all optional list operations, and permits all elements (including null).

      這告訴我們,linkedList是一個雙向鏈表,並且實現了List和Deque接口中所有的列表操作並且能存儲任何元素,包括null

      這里我們可以知道linkedList除了可以當鏈表使用,還可以當作隊列使用,並能進行相應的操作。

    2)All of the operations perform as could be expected for a doubly-linked list. Operations that index into the list will traverse the list from the beginning or the end, whichever is closer to the specified index.

      這個告訴我們,linkedList在執行任何操作的時候,都必須先遍歷此列表來靠近通過index查找我們所需要的的值。通俗點講,這就告訴了我們這個是順序存取,

      每次操作必須先按開始到結束的順序遍歷,隨機存取,就是arrayList,能夠通過index。隨便訪問其中的任意位置的數據,這就是隨機列表的意思

    3)api中接下來講的一大堆,就是說明linkedList是一個非線程安全的(異步),其中在操作Interator時,如果改變列表結構(add\delete等),會發生fail-fast。

  通過API再次總結一下LinkedList的特性:  

    1)異步,也就是非線程安全

    2)雙向鏈表。由於實現了list和Deque接口,能夠當作隊列來使用。

      鏈表:查詢效率不高,但是插入和刪除這種操作性能好。

    3)是順序存取結構(注意和隨機存取結構兩個概念搞清楚)

二、LinkedList源碼分析

2.1、LinkedList的繼承結構以及層次關系

  

  分析:

    我們可以看到,linkedList在最底層,說明他的功能最為強大,並且細心的還會發現,arrayList只有四層,這里多了一層AbstractSequentialList的抽象類,為什么呢?

    通過API我們會發現:

      1)減少實現順序存取(例如LinkedList)這種類的工作,通俗的講就是方便,抽象出類似LinkedList這種類的一些共同的方法

      2)既然有了上面這句話,那么以后如果自己想實現順序存取這種特性的類(就是鏈表形式),那么就繼承這個AbstractSequentialList抽象類

        如果想像數組那樣的隨機存取的類,那么就去實現AbstracList抽象類

      3)這樣的分層,就很符合我們抽象的概念,越在高處的類,就越抽象,往在底層的類,就越有自己獨特的個性。自己要慢慢領會這種思想。

      4)LinkedList的類繼承結構很有意思,我們着重要看是Deque接口,Deque接口表示是一個雙端隊列,

        那么也意味着LinkedList是雙端隊列的一種實現,所以,基於雙端隊列的操作在LinkedList中全部有效。  

public abstract class AbstractSequentialList<E>
extends AbstractList<E>
//這里第一段就解釋了這個類的作用,這個類為實現list接口提供了一些重要的方法,
//盡最大努力去減少實現這個“順序存取”的特性的數據存儲(例如鏈表)的什么鬼,對於
//隨機存取數據(例如數組)的類應該優先使用AbstractList
//從上面就可以大概知道,AbstractSwquentialList這個類是為了減少LinkedList這種順//序存取的類的代碼復雜度而抽象的一個類,
This class provides a skeletal implementation of the List interface to minimize the effort required to implement this interface backed by a "sequential access" data store (such as a linked list). For random access data (such as an array), AbstractList should be used in preference to this class.

//這一段大概講的就是這個AbstractSequentialList這個類和AbstractList這個類是完全//相反的。比如get、add這個方法的實現
This class is the opposite of the AbstractList class in the sense that it implements the "random access" methods (get(int index), set(int index, E element), add(int index, E element) and remove(int index)) on top of the list's list iterator, instead of the other way around.

//這里就是講一些我們自己要繼承該類,該做些什么事情,一些規范。
To implement a list the programmer needs only to extend this class and provide implementations for the listIterator and size methods. For an unmodifiable list, the programmer need only implement the list iterator's hasNext, next, hasPrevious, previous and index methods.

For a modifiable list the programmer should additionally implement the list iterator's set method. For a variable-size list the programmer should additionally implement the list iterator's remove and add methods.

The programmer should generally provide a void (no argument) and collection constructor, as per the recommendation in the Collection interface specification.
AbstractSequentialList

  實現接口分析:

    

      1)List接口:列表,add、set、等一些對列表進行操作的方法

      2)Deque接口:有隊列的各種特性,

      3)Cloneable接口:能夠復制,使用那個copy方法。

      4)Serializable接口:能夠序列化。

      5)應該注意到沒有RandomAccess:那么就推薦使用iterator,在其中就有一個foreach,增強的for循環,其中原理也就是iterator,我們在使用的時候,使用foreach或者iterator都可以

2.2、類的屬性  

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

  LinkedList的屬性非常簡單,一個頭結點、一個尾結點、一個表示鏈表中實際元素個數的變量。注意,頭結點、尾結點都有transient關鍵字修飾,這也意味着在序列化時該域是不會序列化的。

2.3、LinkedList的構造方法

  兩個構造方法(兩個構造方法都是規范規定需要寫的)

  1)空參構造函數

    /**
     * Constructs an empty list.
     */
    public LinkedList() {
    }

  2)有參構造函數

/**
     * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param  c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
     */
   //將集合c中的各個元素構建成LinkedList鏈表。
    public LinkedList(Collection<? extends E> c) {
     // 調用無參構造函數
        this();
        // 添加集合中所有的元素
        addAll(c);
}

  說明:會調用無參構造函數,並且會把集合中所有的元素添加到LinkedList中。   

2.4、內部類(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;
        }
    }

  說明:內部類Node就是實際的結點,用於存放實際元素的地方。     

 2.5、核心方法

  2.5.1、add()方法

    

    1)add(E)

    public boolean add(E e) {
          // 添加到末尾
          linkLast(e);
          return true;
      }

    說明:add函數用於向LinkedList中添加一個元素,並且添加到鏈表尾部。具體添加到尾部的邏輯是由linkLast函數完成的

    分析:

      LinkLast(XXXXX)

/**
     * Links e as last element.
     */
    void linkLast(E e) {
        final Node<E> l = last;    //臨時節點l(L的小寫)保存last,也就是l指向了最后一個節點
        final Node<E> newNode = new Node<>(l, e, null);//將e封裝為節點,並且e.prev指向了最后一個節點
        last = newNode;//newNode成為了最后一個節點,所以last指向了它
        if (l == null)    //判斷是不是一開始鏈表中就什么都沒有,如果沒有,則newNode就成為了第一個節點,first和last都要指向它
            first = newNode;
        else    //正常的在最后一個節點后追加,那么原先的最后一個節點的next就要指向現在真正的最后一個節點,原先的最后一個節點就變成了倒數第二個節點
            l.next = newNode;
        size++;//添加一個節點,size自增
        modCount++;
    }

    說明:對於添加一個元素至鏈表中會調用add方法 -> linkLast方法

    舉例一:

    List<Integer> lists = new LinkedList<Integer>();
    lists.add(5);
    lists.add(6);

     首先調用無參構造函數,之后添加元素5,之后再添加元素6。具體的示意圖如下:

      

      上圖的表明了在執行每一條語句后,鏈表對應的狀態。

  2.5.2、addAll方法

    addAll有兩個重載函數,addAll(Collection<? extends E>)型和addAll(int, Collection<? extends E>)型,我們平時習慣調用的addAll(Collection<? extends E>)型會轉化為addAll(int, Collection<? extends E>)型。

    1)addAll(c);

    public boolean addAll(Collection<? extends E> c) {
    //繼續往下看
        return addAll(size, c);
    }

    2)addAll(size,c):這個方法,能包含三種情況下的添加,我們這里分析的只是構造方法,空鏈表的情況(情況一)看的時候只需要按照不同的情況分析下去就行了。

//真正核心的地方就是這里了,記得我們傳過來的是size,c
    public boolean addAll(int index, Collection<? extends E> c) {
//檢查index這個是否為合理。這個很簡單,自己點進去看下就明白了。
        checkPositionIndex(index);
//將集合c轉換為Object數組 a
        Object[] a = c.toArray();
//數組a的長度numNew,也就是由多少個元素
        int numNew = a.length;
        if (numNew == 0)
//集合c是個空的,直接返回false,什么也不做。
            return false;
//集合c是非空的,定義兩個節點(內部類),每個節點都有三個屬性,item、next、prev。注意:不要管這兩個什么含義,就是用來做臨時存儲節點的。這個Node看下面一步的源碼分析,Node就是linkedList的最核心的實現,可以直接先跳下一個去看Node的分析
        Node<E> pred, succ;
//構造方法中傳過來的就是index==size
        if (index == size) {
//linkedList中三個屬性:size、first、last。 size:鏈表中的元素個數。 first:頭節點  last:尾節點,就兩種情況能進來這里

//情況一、:構造方法創建的一個空的鏈表,那么size=0,last、和first都為null。linkedList中是空的。什么節點都沒有。succ=null、pred=last=null

//情況二、:鏈表中有節點,size就不是為0,first和last都分別指向第一個節點,和最后一個節點,在最后一個節點之后追加元素,就得記錄一下最后一個節點是什么,所以把last保存到pred臨時節點中。
            succ = null;
            pred = last;
        } else {
//情況三、index!=size,說明不是前面兩種情況,而是在鏈表中間插入元素,那么就得知道index上的節點是誰,保存到succ臨時節點中,然后將succ的前一個節點保存到pred中,這樣保存了這兩個節點,就能夠准確的插入節點了
 //舉個簡單的例子,有2個位置,1、2、如果想插數據到第二個位置,雙向鏈表中,就需要知道第一個位置是誰,原位置也就是第二個位置上是誰,然后才能將自己插到第二個位置上。如果這里還不明白,先看一下文章開頭對於各種鏈表的刪除,add操作是怎么實現的。
            succ = node(index);
            pred = succ.prev;
        }
//前面的准備工作做完了,將遍歷數組a中的元素,封裝為一個個節點。
        for (Object o : a) {
            @SuppressWarnings("unchecked") E e = (E) o;
//pred就是之前所構建好的,可能為null、也可能不為null,為null的話就是屬於情況一、不為null則可能是情況二、或者情況三
            Node<E> newNode = new Node<>(pred, e, null);
//如果pred==null,說明是情況一,構造方法,是剛創建的一個空鏈表,此時的newNode就當作第一個節點,所以把newNode給first頭節點
            if (pred == null)
                first = newNode;
            else
//如果pred!=null,說明可能是情況2或者情況3,如果是情況2,pred就是last,那么在最后一個節點之后追加到newNode,如果是情況3,在中間插入,pred為原index節點之前的一個節點,將它的next指向插入的節點,也是對的
                pred.next = newNode;
//然后將pred換成newNode,注意,這個不在else之中,請看清楚了。
            pred = newNode;
        }
        if (succ == null) {
//如果succ==null,說明是情況一或者情況二,
情況一、構造方法,也就是剛創建的一個空鏈表,pred已經是newNode了,last=newNode,所以linkedList的first、last都指向第一個節點。
情況二、在最后節后之后追加節點,那么原先的last就應該指向現在的最后一個節點了,就是newNode。
            last = pred;
        } else {
//如果succ!=null,說明可能是情況三、在中間插入節點,舉例說明這幾個參數的意義,有1、2兩個節點,現在想在第二個位置插入節點newNode,根據前面的代碼,pred=newNode,succ=2,並且1.next=newNode,
1已經構建好了,pred.next=succ,相當於在newNode.next = 2; succ.prev = pred,相當於 2.prev = newNode, 這樣一來,這種指向關系就完成了。first和last不用變,因為頭節點和尾節點沒變
            pred.next = succ;
//。。
            succ.prev = pred;
        }
//增加了幾個元素,就把 size = size +numNew 就可以了
        size += numNew;
        modCount++;
        return true;
    }

    說明:參數中的index表示在索引下標為index的結點(實際上是第index + 1個結點)的前面插入。     

       在addAll函數中,addAll函數中還會調用到node函數,get函數也會調用到node函數,此函數是根據索引下標找到該結點並返回,具體代碼如下:

Node<E> node(int 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; // 返回該結點
        }
    }

    說明:在根據索引查找結點時,會有一個小優化,結點在前半段則從頭開始遍歷,在后半段則從尾開始遍歷,這樣就保證了只需要遍歷最多一半結點就可以找到指定索引的結點。

    舉例說明調用addAll函數后的鏈表狀態:

    List<Integer> lists = new LinkedList<Integer>();
    lists.add(5);
    lists.addAll(0, Arrays.asList(2, 3, 4, 5));

      上述代碼內部的鏈表結構如下:

        

  addAll()中的一個問題

    在addAll函數中,傳入一個集合參數和插入位置,然后將集合轉化為數組,然后再遍歷數組,挨個添加數組的元素,但是問題來了,為什么要先轉化為數組再進行遍歷,而不是直接遍歷集合呢?

    從效果上兩者是完全等價的,都可以達到遍歷的效果。關於為什么要轉化為數組的問題,我的思考如下:1. 如果直接遍歷集合的話,那么在遍歷過程中需要插入元素,在堆上分配內存空間,修改指針域,

    這個過程中就會一直占用着這個集合,考慮正確同步的話,其他線程只能一直等待。2. 如果轉化為數組,只需要遍歷集合,而遍歷集合過程中不需要額外的操作,

    所以占用的時間相對是較短的,這樣就利於其他線程盡快的使用這個集合。說白了,就是有利於提高多線程訪問該集合的效率,盡可能短時間的阻塞。

  2.5.3、remove(Object o)

/**
     * Removes the first occurrence of the specified element from this list,
     * if it is present.  If this list does not contain the element, it is
     * unchanged.  More formally, removes the element with the lowest index
     * {@code i} such that
     * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
     * (if such an element exists).  Returns {@code true} if this list
     * contained the specified element (or equivalently, if this list
     * changed as a result of the call).
     *
     * @param o element to be removed from this list, if present
     * @return {@code true} if this list contained the specified element
     */
//首先通過看上面的注釋,我們可以知道,如果我們要移除的值在鏈表中存在多個一樣的值,那么我們會移除index最小的那個,也就是最先找到的那個值,如果不存在這個值,那么什么也不做
    public boolean remove(Object o) {
//這里可以看到,linkedList也能存儲null
        if (o == null) {
//循環遍歷鏈表,直到找到null值,然后使用unlink移除該值。下面的這個else中也一樣
            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;
    }

    unlink(xxxx)

/**
     * Unlinks non-null node x.
     */
//不能傳一個null值過,注意,看之前要注意之前的next、prev這些都是誰。
    E unlink(Node<E> x) {
        // assert x != null;
//拿到節點x的三個屬性
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;

//這里開始往下就進行移除該元素之后的操作,也就是把指向哪個節點搞定。
        if (prev == null) {
//說明移除的節點是頭節點,則first頭節點應該指向下一個節點
            first = next;
        } else {
//不是頭節點,prev.next=next:有1、2、3,將1.next指向3
            prev.next = next;
//然后解除x節點的前指向。
            x.prev = null;
        }

        if (next == null) {
//說明移除的節點是尾節點
            last = prev;
        } else {
//不是尾節點,有1、2、3,將3.prev指向1. 然后將2.next=解除指向。
            next.prev = prev;
            x.next = null;
        }
//x的前后指向都為null了,也把item為null,讓gc回收它
        x.item = null;
        size--;    //移除一個節點,size自減
        modCount++;
        return element;    //由於一開始已經保存了x的值到element,所以返回。
    }

  2.5.4、get(index)

    get(index)查詢元素的方法

/**
     * Returns the element at the specified position in this list.
     *
     * @param index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
//這里沒有什么,重點還是在node(index)中
    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

    node(index)

/**
     * Returns the (non-null) Node at the specified element index.
     */
//這里查詢使用的是先從中間分一半查找
    Node<E> node(int index) {
        // assert isElementIndex(index);
//"<<":*2的幾次方 “>>”:/2的幾次方,例如:size<<1:size*2的1次方,
//這個if中就是查詢前半部分
         if (index < (size >> 1)) {//index<size/2
            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;
        }
    }

  2.5.5、indexOf(Object o)

//這個很簡單,就是通過實體元素來查找到該元素在鏈表中的位置。跟remove中的代碼類似,只是返回類型不一樣。
   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;
    }

三、LinkedList的迭代器

  在LinkedList中除了有一個Node的內部類外,應該還能看到另外兩個內部類,那就是ListItr,還有一個是DescendingIterator。

  3.1、ListItr內部類

    

    看一下他的繼承結構,發現只繼承了一個ListIterator,到ListIterator中一看:

    

    看到方法名之后,就發現不止有向后迭代的方法,還有向前迭代的方法,所以我們就知道了這個ListItr這個內部類干嘛用的了,就是能讓linkedList不光能像后迭代,也能向前迭代。

     看一下ListItr中的方法,可以發現,在迭代的過程中,還能移除、修改、添加值得操作。

    

  3.2、DescendingIterator內部類    

/**
     * Adapter to provide descending iterators via ListItr.previous
     */
    看一下這個類,還是調用的ListItr,作用是封裝一下Itr中幾個方法,讓使用者以正常的思維去寫代碼,例如,在從后往前遍歷的時候,也是跟從前往后遍歷一樣,使用next等操作,而不用使用特殊的previous。 private class DescendingIterator implements Iterator<E> {
        private final ListItr itr = new ListItr(size());
        public boolean hasNext() {
            return itr.hasPrevious();
        }
        public E next() {
            return itr.previous();
        }
        public void remove() {
            itr.remove();
        }
    }

四、總結

  1)linkedList本質上是一個雙向鏈表,通過一個Node內部類實現的這種鏈表結構
  2)能存儲null值
  3)跟arrayList相比較,就真正的知道了,LinkedList在刪除和增加等操作上性能好,而ArrayList在查詢的性能上好
  4)從源碼中看,它不存在容量不足的情況
  5)linkedList不光能夠向前迭代,還能像后迭代,並且在迭代的過程中,可以修改值、添加值、還能移除值
  6)linkedList不光能當鏈表,還能當隊列使用,這個就是因為實現了Deque接口

 

喜歡就點個“推薦”!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM