死磕 java集合之LinkedList源碼分析

問題

(1)LinkedList只是一個List嗎?java

(2)LinkedList還有其它什麼特性嗎?node

(3)LinkedList爲啥常常拿出來跟ArrayList比較?git

(4)我爲何把LinkedList放在最後一章來說?源碼分析

簡介

LinkedList是一個以雙向鏈表實現的List,它除了做爲List使用,還能夠做爲隊列或者棧來使用,它是怎麼實現的呢?讓咱們一塊兒來學習吧。學習

繼承體系

qrcode

經過繼承體系,咱們能夠看到LinkedList不只實現了List接口,還實現了Queue和Deque接口,因此它既能做爲List使用,也能做爲雙端隊列使用,固然也能夠做爲棧使用。this

源碼分析

主要屬性

// 元素個數
transient int size = 0;
// 鏈表首節點
transient Node<E> first;
// 鏈表尾節點
transient Node<E> last;
複製代碼

屬性很簡單,定義了元素個數size和鏈表的首尾節點。spa

主要內部類

典型的雙鏈表結構。指針

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

主要構造方法

public LinkedList() {
}

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

兩個構造方法也很簡單,能夠看出是一個無界的隊列。code

添加元素

做爲一個雙端隊列,添加元素主要有兩種,一種是在隊列尾部添加元素,一種是在隊列首部添加元素,這兩種形式在LinkedList中主要是經過下面兩個方法來實現的。繼承

// 從隊列首添加元素
private void linkFirst(E e) {
    // 首節點
    final Node<E> f = first;
    // 建立新節點,新節點的next是首節點
    final Node<E> newNode = new Node<>(null, e, f);
    // 讓新節點做爲新的首節點
    first = newNode;
    // 判斷是否是第一個添加的元素
    // 若是是就把last也置爲新節點
    // 不然把原首節點的prev指針置爲新節點
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    // 元素個數加1
    size++;
    // 修改次數加1,說明這是一個支持fail-fast的集合
    modCount++;
}

// 從隊列尾添加元素
void linkLast(E e) {
    // 隊列尾節點
    final Node<E> l = last;
    // 建立新節點,新節點的prev是尾節點
    final Node<E> newNode = new Node<>(l, e, null);
    // 讓新節點成爲新的尾節點
    last = newNode;
    // 判斷是否是第一個添加的元素
    // 若是是就把first也置爲新節點
    // 不然把原尾節點的next指針置爲新節點
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    // 元素個數加1
    size++;
    // 修改次數加1
    modCount++;
}

public void addFirst(E e) {
    linkFirst(e);
}

public void addLast(E e) {
    linkLast(e);
}

// 做爲無界隊列,添加元素老是會成功的
public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}

public boolean offerLast(E e) {
    addLast(e);
    return true;
}
複製代碼

典型的雙鏈表在首尾添加元素的方法,代碼比較簡單,這裏不做詳細描述了。

上面是做爲雙端隊列來看,它的添加元素分爲首尾添加元素,那麼,做爲List呢?

做爲List,是要支持在中間添加元素的,主要是經過下面這個方法實現的。

// 在節點succ以前添加元素
void linkBefore(E e, Node<E> succ) {
    // succ是待添加節點的後繼節點
    // 找到待添加節點的前置節點
    final Node<E> pred = succ.prev;
    // 在其前置節點和後繼節點之間建立一個新節點
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 修改後繼節點的前置指針指向新節點
    succ.prev = newNode;
    // 判斷前置節點是否爲空
    // 若是爲空,說明是第一個添加的元素,修改first指針
    // 不然修改前置節點的next爲新節點
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    // 修改元素個數
    size++;
    // 修改次數加1
    modCount++;
}

// 尋找index位置的節點
Node<E> node(int index) {
    // 由於是雙鏈表
    // 因此根據index是在前半段仍是後半段決定從前遍歷仍是從後遍歷
    // 這樣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;
    }
}

// 在指定index位置處添加元素
public void add(int index, E element) {
    // 判斷是否越界
    checkPositionIndex(index);
    // 若是index是在隊列尾節點以後的一個位置
    // 把新節點直接添加到尾節點以後
    // 不然調用linkBefore()方法在中間添加節點
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}
複製代碼

在中間添加元素的方法也很簡單,典型的雙鏈表在中間添加元素的方法。

添加元素的三種方式大體以下圖所示:

qrcode

在隊列首尾添加元素很高效,時間複雜度爲O(1)。

在中間添加元素比較低效,首先要先找到插入位置的節點,再修改先後節點的指針,時間複雜度爲O(n)。

刪除元素

做爲雙端隊列,刪除元素也有兩種方式,一種是隊列首刪除元素,一種是隊列尾刪除元素。

做爲List,又要支持中間刪除元素,因此刪除元素一個有三個方法,分別以下。

// 刪除首節點
private E unlinkFirst(Node<E> f) {
    // 首節點的元素值
    final E element = f.item;
    // 首節點的next指針
    final Node<E> next = f.next;
    // 添加首節點的內容,協助GC
    f.item = null;
    f.next = null; // help GC
    // 把首節點的next做爲新的首節點
    first = next;
    // 若是隻有一個元素,刪除了,把last也置爲空
    // 不然把next的前置指針置爲空
    if (next == null)
        last = null;
    else
        next.prev = null;
    // 元素個數減1
    size--;
    // 修改次數加1
    modCount++;
    // 返回刪除的元素
    return element;
}
// 刪除尾節點
private E unlinkLast(Node<E> l) {
    // 尾節點的元素值
    final E element = l.item;
    // 尾節點的前置指針
    final Node<E> prev = l.prev;
    // 清空尾節點的內容,協助GC
    l.item = null;
    l.prev = null; // help GC
    // 讓前置節點成爲新的尾節點
    last = prev;
    // 若是隻有一個元素,刪除了把first置爲空
    // 不然把前置節點的next置爲空
    if (prev == null)
        first = null;
    else
        prev.next = null;
    // 元素個數減1
    size--;
    // 修改次數加1
    modCount++;
    // 返回刪除的元素
    return element;
}
// 刪除指定節點x
E unlink(Node<E> x) {
    // x的元素值
    final E element = x.item;
    // x的前置節點
    final Node<E> next = x.next;
    // x的後置節點
    final Node<E> prev = x.prev;
    
    // 若是前置節點爲空
    // 說明是首節點,讓first指向x的後置節點
    // 不然修改前置節點的next爲x的後置節點
    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    // 若是後置節點爲空
    // 說明是尾節點,讓last指向x的前置節點
    // 不然修改後置節點的prev爲x的前置節點
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    // 清空x的元素值,協助GC
    x.item = null;
    // 元素個數減1
    size--;
    // 修改次數加1
    modCount++;
    // 返回刪除的元素
    return element;
}
// remove的時候若是沒有元素拋出異常
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
// remove的時候若是沒有元素拋出異常
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}
// poll的時候若是沒有元素返回null
public E pollFirst() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}
// poll的時候若是沒有元素返回null
public E pollLast() {
    final Node<E> l = last;
    return (l == null) ? null : unlinkLast(l);
}
// 刪除中間節點
public E remove(int index) {
    // 檢查是否越界
    checkElementIndex(index);
    // 刪除指定index位置的節點
    return unlink(node(index));
}
複製代碼

刪除元素的三種方法都是典型的雙鏈表刪除元素的方法,大體流程以下圖所示。

qrcode

在隊列首尾刪除元素很高效,時間複雜度爲O(1)。

在中間刪除元素比較低效,首先要找到刪除位置的節點,再修改先後指針,時間複雜度爲O(n)。

前面咱們說了,LinkedList是雙端隊列,還記得雙端隊列能夠做爲棧使用嗎?

public void push(E e) {
    addFirst(e);
}

public E pop() {
    return removeFirst();
}
複製代碼

棧的特性是LIFO(Last In First Out),因此做爲棧使用也很簡單,添加刪除元素都只操做隊列首節點便可。

總結

(1)LinkedList是一個以雙鏈表實現的List;

(2)LinkedList仍是一個雙端隊列,具備隊列、雙端隊列、棧的特性;

(3)LinkedList在隊列首尾添加、刪除元素很是高效,時間複雜度爲O(1);

(4)LinkedList在中間添加、刪除元素比較低效,時間複雜度爲O(n);

(5)LinkedList不支持隨機訪問,因此訪問非隊列首尾的元素比較低效;

(6)LinkedList在功能上等於ArrayList + ArrayDeque;

彩蛋

java集合部分的源碼分析所有完結,整個專題以ArrayList開頭,以LinkedList結尾,我以爲很是合適,由於ArrayList表明了List的典型實現,LInkedList表明了Deque的典型實現,同時LinkedList也實現了List,經過這兩個類一首一尾正好能夠把整個集合貫穿起來。

還記得咱們一共分析了哪些類嗎?

下一章,筆者將對整個java集合作一個總結,並提出一些閱讀源碼過程當中的問題,敬請期待^^


歡迎關注個人公衆號「彤哥讀源碼」,查看更多源碼系列文章, 與彤哥一塊兒暢遊源碼的海洋。

qrcode
相關文章
相關標籤/搜索