Java集合框架——LinkedList

本文基於JDK1.8,代碼中方法頭的註釋都是對照源碼翻譯過來的
自頂向下閱讀

類頭

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

LinkedList繼承了AbstractSequentialList抽象類,是一個雙向鏈表,可以被當作堆棧、隊列或雙端隊列進行操作。在AbstractSequentialList封裝好了一些方法和抽象方法,當繼承此抽象類時,只需要重寫其中的抽象方法,而不需要重寫全部方法
實現List接口(提供List接口中所有方法的實現)
實現了Deque接口,實現了Deque所有的可選的操作
實現Cloneable接口,支持克隆,可以調用clone()進行拷貝
實現java.io.Serializable接口,支持序列化

核心

LinkedList內部定義了一個Node類,Node是雙向的,LinkedList底層都是通過操作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;
    }
}

E item:值域,節點的值
Node next:next域,當前節點的後一個節點的引用(可以理解爲指向當前節點的後一個節點的指針)
Node prev:prev域,當前節點的前一個節點的引用(可以理解爲指向當前節點的前一個節點的指針)

成員變量

//記錄LinkedList的大小
transient int size = 0;

//指向頭節點
transient Node<E> first;

//指向尾結點
transient Node<E> last;

使用transient關鍵字,避免序列化

構造方法

/**
 * 默認構造器,創建一個空的列表
 */
public LinkedList() {
}
/**
 * 按照集合的迭代器返回的順序,構造包含指定集合的元素的列表
 *
 * @param  c 將其元素放入列表中的集合
 * @throws NullPointerException 如果指定集合爲空拋出異常
 */
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

首先調用一下空的構造器創建一個鏈表,然後在此構造器中調用了addAll(Collection<? extends E> c)方法,後面插入會講到

工具方法

這些方法都是private私有或protected的,不提供給用戶使用,只用作LinkedList公有方法功能的實現
下面這些方法都是頭節點、尾結點成對的,可以對照看,只要弄懂一個,其他的理解很快

1. 把元素作爲鏈表的第一個元素

注意:只有在鏈表首部和尾部插入纔有頭節點和尾結點的變化,如果在鏈表中插入則沒有

/**
 * 鏈接e作爲第一個元素
 */
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++;
}

由於將指定元素作爲頭節點,所以需要將原來的頭節點保存下來,將頭節點賦給一個新節點f,注意這裏是等號直接賦值,賦的是地址,f的改變會直接作用到頭節點first,頭節點也會改變,在LinkedList中有很多這種賦值操作,一定要搞清楚,不然就會有f的改變爲什麼會改變整個LinkedList的結構等疑惑
承接上文,再創建一個新節點newNode,將其值域設爲參數e,next域指向f,然後將自身設爲頭節點
進行判斷,如果f爲空,說明頭節點爲空,整個鏈表爲空,已經將頭節點設爲newNode,只需要將新節點設爲尾結點即可;若不爲空,此時newNode已經是頭節點,且next(下一個節點地址)指向f(正數第二個節點),但LinkList是雙向鏈表,所以下一個節點的prev(前一個節點地址)域需要指向頭節點
然後鏈表大小加1,修改次數加1

2. 把元素作爲鏈表的最後一個元素

/**
 * 鏈接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++;
}

和linkFirst(E e)方法原理類似
先保存尾結點,在構造一個新的節點,prev域指向尾結點,再將新節點newNode作爲尾結點
再進行判斷,l爲空,空鏈表,頭節點first也指向新節點;不爲空,l節點(也就是倒數第二個節點)next域指向尾結點,將鏈表鏈接起來即可

3. 刪除鏈表第一個節點(節點不爲空)

/**
 * 解開非空第一節點f
 */
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;
}

定義一個element保存f節點值域的值,定義一個新節點next指向f節點的下一節點,將f的item域和next域設爲空,由於刪除了f,f的下一節點就變爲了頭節點,所以first = next
再進行判斷,若next爲空,空鏈表,尾結點last設爲null;若不爲空,頭節點prev域設爲空即可(頭節點沒有前一個節點)
最後返回刪除的節點的值域element
總結:f節點item、next域爲空,頭節點prev域爲空

4. 刪除鏈表最後一個節點(節點不爲空)

/**
 * 解開非空最後節點l
 */
private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    final E element = l.item;
    final Node<E> prev = l.prev;
    l.item = null;
    l.prev = null; // help GC
    last = prev;
    if (prev == null)
        first = null;
    else
        prev.next = null;
    size--;
    modCount++;
    return element;
}

定義element保存l節點值域的值,定義一個新節點prev指向l節點的前一個節點,將l的item域和prev域設爲空,由於刪除了最後一個節點l,l的前一個節點就變爲尾結點,所以last = prev
再判斷,空鏈表,頭節點爲空;不爲空,尾結點的next域設爲空(尾結點沒有下一個節點)
最後返回刪除的節點的值域element
總結:l節點item、prev爲空,尾節點next域爲空

5. 在非空節點succ之前插入元素e

/**
 * 在非空節點succ之前插入元素e
 */
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    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++;
}

先定義一個新節點pred將succ的前一個節點保存下來,在succ節點之前插入,插入後,待插入元素e節點位於succ節點和succ前一個節點pred之間,所以構造一個待插入新節點newNode,前一個節點爲pred,下一個節點爲succ
此時只是newNode節點指向前一個節點pred和下一個節點succ,但LinkedList爲雙向鏈表,需要下一個節點前驅(prev域)指向newNode,所以succ.prev = newNode
再判斷pred是否爲空,爲空,新節點設爲頭節點;不爲空,前一個節點pred的後驅(next域)指向新節點,這樣,就成功插入元素了

6. 刪除指定節點(節點非空)

/**
 * 解開非空節點x
 */
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;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}

前面的刪除方法要麼是刪除第一個節點,要麼是刪除最後一個節點,而此方法是刪除指定結點,實現方式略有區別
首先,和前面類似,刪除某一節點,需要將指定節點的前一節點和下一節點保存下來,代碼中將節點的值也保存下來是作爲方法的返回值
然後判斷前一節點prev是否爲空,爲空,則x爲頭節點,刪除之後,x下一節點next爲頭節點,所以first = next;不爲空,將prev的後驅(next域)指向x下一節點next,這裏鏈接第一部分(前一節點指向後一節點)
再判斷後一節點next是否爲空,爲空,x爲尾結點,刪除之後,x前一節點prev爲尾結點,所以last = prev;不爲空,將next前驅(prev域)指向x前一節點prev,這裏鏈接第二部分(後一節點指向前一節點)
最後將item值域設爲空,鏈表大小減一,modCount加1,返回刪除節點的值
由於LinkedList是雙向的,需要雙向操作,所以在方法中通過兩個if判斷來分別進行鏈表的鏈接操作,這裏可以自己畫圖理解
最後返回刪除的節點的值域element

7. 得到指定位置節點

/**
 * 返回指定元素索引處的(非空)節點
 */
Node<E> node(int index) {
    // assert isElementIndex(index);

    /*
    判斷index位於鏈表的前半部分還是後半部分,從而判斷從哪個方向遍歷
    用循環方式遍歷到index那個位置,得到該位置的元素並返回
    LinkedList獲取元素採用的是遍歷鏈表的方式,雖然最多隻會循環列表大小的一半,但性能也比較低的
    */
    if (index < (size >> 1)) { //size >> 1 等同於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;
    }
}

8. 索引檢查

方法內容簡單明瞭,看註釋很容易理解

/**
 * 說明參數是否爲現有元素的索引
 */
private boolean isElementIndex(int index) {
    return index >= 0 && index < size;
}

/**
 * 說明參數是否是迭代器或添加操作的有效位置的索引
 */
private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}

注意isElementIndex()和isPositionIndex()的不同,isPositionIndex()是判斷迭代器或添加操作的位置是否有效,所以多了個等於的判斷,若index=size,則是在鏈表末尾插入
isElementIndex(int index)是鏈表元素索引的檢查,所以鏈表的索引一定是小於size元素個數的

private void checkElementIndex(int index) {
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

9. 異常信息提示

/**
 * 構造IndexOutOfBoundsException異常詳細信息
 * 在錯誤處理代碼的許多可能的重構中,這個“大綱”對服務器和客戶端VM都是最好的
 */
private String outOfBoundsMsg(int index) {
    return "Index: "+index+", Size: "+size;
}

到這裏大部分的工具方法就講完了,接下來的公有方法大多是調用上面的方法來實現相關功能的

公有方法

因爲有些方法具體分析在上面有提到,接下來已經講過的方法就不一一贅述了,結合註釋很容易明白

1. 插入

1.1 addAll(Collection<? extends E> c):boolean

/**
 * 按照指定集合的迭代器返回的順序,將指定集合中的所有元素追加到該列表的末尾。如果操作過程中指定的集合被修改,則此操作的行爲未定義(注意,如果指定的集合是這個列表,則它將是非空的。)
 *
 * @param c 包含要添加到此列表的元素的集合
 * @return {@code true} 如果此列表由於調用而更改
 * @throws NullPointerException 如果指定集合爲空
 */
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

/**
 * 將指定集合中的所有元素插入到該列表中,從指定位置開始。將當前在該位置的元素(如果有的話)和任何後續元素向右移動(增加它們的索引)。新元素將以指定集合的迭代器返回的順序出現在列表中。
 *
 * @param index 從指定集合插入第一個元素的索引
 * @param c 包含要添加到此列表的元素的集合
 * @return {@code true} 如果此列表由於調用而更改
 * @throws IndexOutOfBoundsException
 * @throws NullPointerException 如果指定集合爲空
 */
public boolean addAll(int index, Collection<? extends E> c) {
    //先進行邊界檢查
    checkPositionIndex(index);

    //將集合c轉化爲數組,判斷數組長度是否符合要求
    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)
        return false;

    //定義了兩個節點,pred(待添加節點的前一個節點)、succ(待添加位置index的節點)
    Node<E> pred, succ;
    
    //構造器調用進入if代碼塊中
    //if判斷,如果index == size,說明將元素插入到鏈表末尾(構造器如此,鏈表爲空),succ沒有意義,設爲空,待插入節點pred設爲尾結點;否則插入位置位於鏈表中,通過方法E node(int index)得到索引位置的節點,使pred指向succ的前一個節點
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        succ = node(index);
        pred = succ.prev;
    }

    //通過for循環遍歷數組,每次遍歷都新建一個節點,節點中存儲數組元素的值,使該節點的前驅(prev域)指向pred節點,進行單向鏈接,再進行if判斷,如果pred爲空,則爲空鏈表,頭節點指向新節點;若不爲空,pred節點後驅(next域)指向新節點,對應new Node<>(pred, e, null)進行鏈表雙向鏈接
    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;
    }

    //再進行succ判斷,經過前面的if判斷,succ只有爲null和node(index)兩種情況。爲空,尾結點last指向pred;不爲空,pred後驅(next域)指向succ,succ的前驅指向pred,就將集合元素成功插入指定位置了
    //注意:只有在鏈表首部和尾部元素的改變纔有頭節點和尾結點的變化,如果在鏈表中插入則沒有
    if (succ == null) {
        last = pred;
    } else {
        pred.next = succ;
        succ.prev = pred;
    }

    //最後鏈表大小加1,modCount加1,返回修改結果true
    size += numNew;
    modCount++;
    return true;
}

在構造器中就調用了addAll()方法
可以看到,addAll(int index, Collection<? extends E> c)方法纔是構造器的底層實現
方法內容有點多,將分析拆分開來寫在方法註釋中

1.2 add():boolean

/**
 * 將指定的元素追加到列表的末尾
 *
 * <p>該方法相當於addLast
 *
 * @param e 要追加到這個列表的元素
 * @return {@code true} (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    linkLast(e);
    return true;
}

2. 刪除指定元素(且只刪除第一次出現的指定的元素),如果指定的元素在集合中不存在,則返回false,否則返回true)

/**
 * 如果存在,則從該列表中移除指定元素的第一次出現。如果該列表不包含元素,則其不變。更正式地,移除具有最低索引的元素
 * 
 * 如果這個列表包含指定的元素(或者等效地,如果這個列表由於調用而改變),則返回true
 *
 * @param o 要從該列表中移除的元素,如果存在
 * @return {@code true} 如果此列表包含指定元素
 */
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;
}

3. 得到第一個、最後一個元素

/**
 * 返回此列表中的第一個元素
 *
 * @return 列表中的第一個元素
 * @throws NoSuchElementException 如果這個列表是空的拋出異常
 */
public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}

/**
 * 返回此列表中的最後一個元素
 *
 * @return 此列表中的最後一個元素
 * @throws NoSuchElementException 如果這個列表是空的拋出異常
 */
public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}

4. 刪除第一個、最後一個元素並返回

/**
 * 從這個列表中移除並返回第一個元素
 *
 * @return 列表中的第一個元素
 * @throws NoSuchElementException 如果這個列表是空的拋出異常
 */
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

/**
 * 從這個列表中移除並返回最後一個元素
 *
 * @return 列表中的最後一個元素
 * @throws NoSuchElementException 如果這個列表是空的拋出異常
 */
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}

5. 在開頭、末尾插入元素

/**
 * 在這個列表的開頭插入指定的元素
 *
 * @param e 要添加的元素
 */
public void addFirst(E e) {
    linkFirst(e);
}

/**
 * 將指定的元素追加到列表的末尾
 *
 * <p>這個方法相當於add
 *
 * @param e 要添加的元素
 */
public void addLast(E e) {
    linkLast(e);
}

6. 清空鏈表

/**
 * 從該列表中移除所有元素
 * 此調用返回後,列表將爲空
 */
public void clear() {
    // 清除節點之間的所有鏈接是"不必要的",但是:
    // - 如果丟棄的節點駐留一代以上,則有助於生成GC
    // - 即使有一個可達迭代器,也可以釋放內存
    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,把每個節點都置空。最後要把頭節點和尾節點設置爲空,siz也設置爲空,但modCount仍自增

7. 判斷是否包含某一元素

/**
 * 如果這個列表包含指定的元素,則返回true
 *
 * @param o 元素在列表中的存在將被測試
 */
public boolean contains(Object o) {
    return indexOf(o) != -1;
}

/**
 * 返回此列表中指定元素的第一次出現的索引,或如果該列表不包含元素,則爲-1
 *
 * @param o 要搜索的元素
 * @return 此列表中指定元素的第一次出現的索引,或如果該列表不包含元素,則爲-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;
}

contains(Object o)底層調用indexOf(Object o)方法
在indexOf(Object o)方法中,先判斷obejct是否爲空,分爲兩種情況:
然後在每種情況下,從頭節點開始遍歷LinkedList,判斷是否有與object相等的元素,如果有,則返回對應的位置index,如果找不到,則返回-1

8. 獲取對應索引節點的值

/**
 * 返回該列表中指定位置的元素
 *
 * @param index 返回元素的索引
 * @return 列表中指定位置的元素
 * @throws IndexOutOfBoundsException
 */
public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

9. 轉化爲數組

9.1 轉化爲數組(不含參數)

/**
 * 返回包含該列表中所有元素的數組(從第一個元素到最後一個元素)
 *
 * <p>返回的數組將是“安全的”,因爲該列表沒有維護它(換句話說,這個方法必須分配一個新的數組)。因此,調用方可以修改返回的數組
 *
 * <p>該方法作爲基於數組和基於集合的橋樑
 *
 * @return 以適當順序包含該列表中的所有元素的數組
 */
public Object[] toArray() {
    Object[] result = new Object[size];
    int i = 0;
    for (Node<E> x = first; x != null; x = x.next)
        result[i++] = x.item;
    return result;
}

9.2 轉化爲數組(含數組參數)

/**
 * 返回一個數組,該數組按適當的順序(從第一個元素到最後一個元素)包含該列表中的所有元素;返回的數組的運行時類型是指定數組的運行時類型。如果列表適合於指定的數組,則返回該數組。否則,將使用指定數組的運行時類型和該列表的大小分配新的數組
 *
 * <p>如果列表適合於具有空閒空間的指定數組(即,數組具有比列表更多的元素),則緊跟在列表末尾的數組中的元素被設置爲null
 * 如果調用者知道列表不包含任何空元素,則這對於確定列表的長度很有用
 *
 * <p>與toArray()方法一樣,該方法在基於數組和基於集合的API之間起到橋樑的作用。此外,該方法允許對輸出陣列的運行時類型進行精確控制,並且可以在某些情況下用於節省分配成本
 *
 * <p>假設x是一個已知的僅包含字符串的列表。下面的代碼可以用來將列表轉儲到新分配的{@代碼字符串}數組中:
 *
 * String[] y = x.toArray(new String[0]);</pre>
 *
 * 注意,toArray(new Object[0])在函數上與toArray()相同
 *
 * @param a 如果列表足夠大,則要存儲列表元素的數組;否則,將爲此目的分配一個新的相同運行時類型的數組
 * @return 包含列表元素的數組
 * @throws ArrayStoreException 如果指定數組的運行時類型不是該列表中每個元素的運行時類型的超類型
 * @throws NullPointerException 如果指定的數組爲空
 */
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
    if (a.length < size)
        a = (T[])java.lang.reflect.Array.newInstance(
                            a.getClass().getComponentType(), size);
    int i = 0;
    Object[] result = a;
    for (Node<E> x = first; x != null; x = x.next)
        result[i++] = x.item;

    if (a.length > size)
        a[size] = null;

    return a;
}

在此方法中,通過反射創建一個數組,把集合中的所有元素添加到數組中,再將此數組返回

總結

由於源碼方法太多,就不一一列舉了,如果理解了核心方法,舉一反三,相信理解其他的方法實現不會有什麼問題

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