1. 程式人生 > >JAVA常用集合原始碼分析:LinkedList

JAVA常用集合原始碼分析:LinkedList

概述

上一篇我們介紹了ArrayList,我們知道它的底層是基於陣列實現的,提到陣列,我們就馬上會想到它的兄弟連結串列,今天我們要介紹的LinkedList就是基於連結串列實現的。

繼承結構

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

List介面:列表,add、set、等一些對列表進行操作的方法
Deque介面:有佇列的各種特性,可以當佇列用
Cloneable介面:能夠複製,使用那個copy方法。
Serializable介面:能夠序列化。
注意:並沒有實現RandomAccess:那麼就推薦使用iterator,在其中就有一個foreach,增強的for迴圈,其中原理也就是iterator,我們在使用的時候,使用foreach或者iterator都可以。 

原始碼分析

先看有哪些成員變數

//元素個數
transient int size = 0;
//首節點
transient Node<E> first;
//尾節點
transient Node<E> last;

在這裡,我們發現first和last都是Node型別的,我們不難想到這是節點類

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

根據原始碼,我們可以發現節點有三個屬性,分別是值和指向一前一後的兩個指標

看到這裡,我們可以得出結論,LinkedList是底層是一個雙向連結串列,但究竟是不是雙向迴圈連結串列呢?我們還得繼續往下看。

有兩個構造方法

//空實現 
public LinkedList() {
    }

//用一個集合構建
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

第二個構造方法中,出現了addAll(e)方法,顯然該方法實現了把c中所以元素加到一個空連結串列中,我們來看看它的具體實現

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

只有短短兩行,主要呼叫了我們的addAll(int, E)方法,繼續看它的實現

public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index); //檢查邊界

        Object[] a = c.toArray(); // 把c裡的值轉化為陣列
        int numNew = a.length;
        if (numNew == 0)  //如果c中無元素,則增加失敗
            return false;

        Node<E> pred, succ;  // 兩個指標,一個指前,一個後

        if (index == size) { //注意我們建構函式中一開始傳進來的就是size,所以會進入
            succ = null;
            pred = last;
        } else { //當我們傳入的不是size的時候,進入這裡
            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;
    }

程式碼有點長,但是不難,容易看懂,同時在這個方法中,我們也可以知道底層只是個雙向連結串列,並非是雙向迴圈連結串列啦,事實上JDK1.7之前一直是迴圈連結串列來著,至於為啥要改,我也不敢妄下定論....

好啦,分析完了建構函式,我們接著看他一些常用的方法的實現把

常用方法

1.add(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++;
    }

哈哈,程式碼不難,一看就懂

2.add(int , e) 在指定位置增加元素

 public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size) //如果剛好是在最後一個增加,就直接加啦
            linkLast(element);
        else
            linkBefore(element, node(index));  //在中間加
    }

//在中間加的時候利用一分為二思想,看index離頭近還是離尾近
Node<E> node(int index) {

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

 具體的思路我都寫在註釋上啦,在node(index)的時候,運用了一分為二思想,結合了雙向連結串列的優點,有點巧妙

3.remove(Object) 刪除第一次的Object

其實思路不難,就是先找到出現的位置,然後執行刪除操作

 public boolean remove(Object o) {
        if (o == null) {  // 區分是否為空,因為空值無法執行 equals方法
            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了

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;  //儲存後繼

        if (prev == null) {   //如果沒有前繼,就說嘛要刪除的點是首節點,直接讓first執向next就行啦
            first = next;
        } else {            //前繼指向它的後繼,相當於跳過它啦
            prev.next = next;
            x.prev = null;  //置空
        }

        if (next == null) {
            last = prev;   // 太簡單,跳過
        } else {
            next.prev = prev;   //next的前繼指向 原節點的前繼。
            x.next = null;
        }

        x.item = null;  //置空,讓GC回收它
        size--;
        modCount++;
        return element;
    }

可以結合上面的圖來理解

4.remove(index)  刪除給定位置的物件啦

其實原理都差不多,首先是定位,再刪除...注意刪除的點是頭或者尾節點這種特殊情況就行啦

5.get(index) 

public E get(int index) {
        checkElementIndex(index); //檢查邊界
        return node(index).item;
    }

核心定位程式碼node(index)之前已經介紹過啦,雖然進行了一些優化,效能已經變為了O(n/2),但還是非常低效的。

總結

  • LinkedList 插入,刪除都是移動指標效率很高。
  • 查詢需要進行遍歷查詢,效率較低

與ArrayList的比較

  • ArrayList 基於動態陣列實現,LinkedList 基於雙向連結串列實現;
  • ArrayList 支援隨機訪問,LinkedList 不支援;
  • LinkedList 在任意位置新增刪除元素更快。

想說的話

哈哈第二次看原始碼啦,雖然速度慢,但感覺還是不錯的啦,分析原始碼的思路感覺更加順暢了些。有些觀點參考了其他的部落格,在瀏覽其他部落格的時候,也有發現其他部落格說的不夠準確的地方,也正是這樣,讓我感覺部落格必須要嚴謹些,一定要儘量多瞭解些,不然對其他初學者產生勿擾就糟糕啦哈哈哈哈