LinkedList原始碼解析

迷戀著你微笑的人zz發表於2018-08-14

[TOC]

前言

linkedList是使用的雙向連結串列,今天就來研究一下;我使用的jdk1.8;

正文

avatar

LinkedList使用的資料結構如上圖,圖中的箭頭是指向的節點,不是指向節點中的資料;

成員變數

avatar

  1. size表明LinkedList中儲存資料的量;
  2. 指向第一個節點;
  3. 指向最後一個節點;
 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中定義的靜態內部類,資料的增刪查改都是用的這個資料結構,還是比較重要的

構造方法

因為LinkedList使用的是雙向連結串列,所以不用專門去寫一個擴容的方法,就不用擔心動態擴容的問題了;LinkedList有兩種構造方法;

  1. 第一種:
public LinkedList() {
}
複製程式碼

直接new一個;

  1. 第二種:
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}
複製程式碼

這種方法還是使用的第一種的構造方法,然後再呼叫其中的方法將資料填充進LinkedList;

常用方法

主要寫增刪查改的方法;

新增

新增元素有六種方法,這裡直選其中兩個進行研究

直接新增元素,就是將資料新增到末尾

 public boolean add(E e) {
        linkLast(e);
        return true;
    }
複製程式碼

linkLast方法:


    /**
     * Links e as last element.
     */
    void linkLast(E e) {
        //1
        final Node<E> l = last;
        //2 
        final Node<E> newNode = new Node<>(l, e, null);
        //3 
        last = newNode;
        //4
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        //5 
        size++;
        modCount++;
    }
複製程式碼

感覺註釋寫到程式碼中太擁擠了,就提出來單獨寫了;流程說明如下 :

  1. 用 l 來替換最後一個節點的值;
  2. 構造新節點,上一個節點指向l,儲存新增的e資料,下一個指向null;
  3. 尾節點變為新建的節點;
  4. 如果尾節點為空,第一個節點也等於新建節點,反之 l 指向新節點;
  5. 數量自加1;

外邊呼叫add(E e)方法,實際上是呼叫的linkLast(E e)方法,從方法名可以看出直接將新增的資料放到了尾節點;

新增元素到指定位置

 public void add(int index, E element) {
     // 1
        checkPositionIndex(index);
     // 2
        if (index == size)
            linkLast(element);
     // 3
        else
            linkBefore(element, node(index));
    }
複製程式碼
  1. 檢查index是否越界;
  2. 如果index等於LinkedList的size的時候,直接將元素新增到尾節點;
  3. 如果不是,則通過node(int index)方法找到該位置的節點,並呼叫linkBefore方法將元素element新增到找到的節點之前

Node<E> node(int index)方法就是找到第index個元素節點,程式碼如下:

 /**
     * Returns the (non-null) Node at the specified element index.
     */
    Node<E> node(int index) {
        // assert isElementIndex(index);
        // 1
        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;
        }
    }
複製程式碼
  1. 如果index小於size的一半,則從開頭開始查詢,反之從尾節點向前開始查詢
  2. 最後返回查詢到的節點
  3. 這個方法在後面用得較多,可以多看一看

linkBefore(E e, Node<E> succ) 方法就是將元素e新增到節點succ之前,程式碼如下:

/**
     * Inserts element e before non-null Node succ.
     */
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        // 1
        final Node<E> pred = succ.prev;
        // 2
        final Node<E> newNode = new Node<>(pred, e, succ);
        // 3
        succ.prev = newNode;
        // 4
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }
複製程式碼
  1. 找到succ節點的上一個節點pred
  2. 構造一個新的節點newNode,該節點的上個節點指向pred,儲存節點e,下一個節點指向succ;
  3. succ的父節點指向新節點newNode;
  4. 如果pred節點為空,說明succ節點就是頭結點,所以直接將第一個節點first變為新節點newNode,反之pred節點的下一個節點指向新節點newNode;

刪除

刪除節點有四個方法,這裡懸著其中兩個進行研究:

根據下標刪除節點

程式碼如下:

public E remove(int index) {
    // 1
        checkElementIndex(index);
    // 2
        return unlink(node(index));
    }
複製程式碼
  1. 檢查index是否越界
  2. 先呼叫node 方法找到節點,再呼叫unlink方法刪除節點

unlink方法程式碼如下:

 E unlink(Node<E> x) {
        // assert x != null;
     //1
        final E element = x.item;
     //2
        final Node<E> next = x.next;
     //3
        final Node<E> prev = x.prev;
     //4
        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }
     //5
        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }
    // 6
        x.item = null;
        size--;
        modCount++;
        return element;
    }
複製程式碼
  1. 找到該節點儲存的資料item;
  2. 找到該節點指向的下一個節點next;
  3. 找到該節點的上一個節點prev;
  4. 判斷該節點的父節點prev是否是null,如果為null,說明該節點為頭結點,則first變為該節點的下一個節點,反之不為null,則父節點prev的子節點指向next節點,然後清空該節點的prev元素
  5. 判斷該節點的next是否為null,即是否為尾節點,如果是尾節點,則last就等於prev,反之不為null,則next.prev就等於prev節點,清空該節點的next元素
  6. 清空該節點的item元素

根據節點刪除節點

程式碼如下:

 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;
    }
複製程式碼

首先判斷物件o是否為null,如果為null,則遍歷連結串列刪除第一個itemnull的節點,如果不為null,則遍歷連結串列呼叫物件oequals方法來和節點的item作比較,刪除第一個比較為true的節點;

查詢

查詢就比較簡單了

根據下標查詢節點

程式碼如下:

 public E get(int index) {
     //1
        checkElementIndex(index);
     //2
        return node(index).item;
    }
複製程式碼
  1. 呼叫checkElementIndex檢驗index是否越界
  2. 呼叫node方法找到節點,並返回該節點的item

修改

修改也比較簡單

根據下標修改節點

程式碼如下:

public E set(int index, E element) {
    // 1
        checkElementIndex(index);
    // 2
        Node<E> x = node(index);
    // 3
        E oldVal = x.item;
    // 4
        x.item = element;
    // 5
        return oldVal;
    }
複製程式碼
  1. 檢查index是否越界;
  2. 呼叫node方法找到節點;
  3. 獲取該節點的itemoldVal;
  4. element替換掉原來的值;
  5. 返回修改前的值oldVal;

其他方法

clear方法

遍歷該連結串列,每個節點的元素清空,清空該連結串列;

程式碼如下:

 public void clear() {
        // Clearing all of the links between nodes is "unnecessary", but:
        // - helps a generational GC if the discarded nodes inhabit
        //   more than one generation
        // - is sure to free memory even if there is a reachable Iterator
        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++;
    }
複製程式碼

size方法

返回該連結串列中儲存節點的數量

程式碼如下:

 public int size() {
        return size;
    }
複製程式碼

indexOf方法

該方法是查詢節點的下標位置;

程式碼如下:

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

複製程式碼

流程大概就是遍歷連結串列,如果為null,則直接查詢為null的節點,反之就呼叫equals方法來查詢節點

最後

閱讀的方法並不多,也許其他的沒有閱讀的方法並不簡單,但是個人感覺LinkedList整體來說還是不難的,主要還是要理解連結串列的操作,這些理解了就差不多了;

總結

  • LinkedList底層資料結構採用雙向連結串列,所以記憶體空間並不一定是連續的;
  • 可以儲存null,可以儲存重複值
  • 有序的,按儲存順序排列
  • 執行緒非安全的

相比於ArrayList

  • 兩者都是按儲存順序來排列的

  • 兩者都是執行緒非安全的

  • 兩者都可以儲存重複元素,都可以存null

  • ArrayList使用的陣列,LinkedList採用雙向連結串列,所以相對於ArrayList定址快,LinkedList定址慢

  • 兩者在新增元素時,ArrayList底層採用陣列,所以可以將元素直接儲存進去,新增的元素需要新建(new)一個節點

  • 當插入或刪除一個元素時,ArrayList需要copy陣列,會呼叫 Arrays.copyof()方法,而LinkedList是連結串列,則可以直接修改,而這個快慢考慮的因數就比較多了,例如:元素的位置,元素的大小等等;

  • ArrayList是陣列,所以到了一定數量就需要擴容,而LinkedList留不用擔心擴容的問題

相關文章