1. 程式人生 > >Java集合框架——LinkedList

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[] re