1. 程式人生 > >jdk源碼閱讀筆記-LinkedList

jdk源碼閱讀筆記-LinkedList

當前 源碼閱讀 直接 push point equals方法 ++ ast tar

  一、LinkedList概述

  LinkedList的底層數據結構為雙向鏈表結構,與ArrayList相同的是LinkedList也可以存儲相同或null的元素。相對於ArrayList來說,LinkedList的插入與刪除的速度更快,時間復雜度為O(1),查找的速度就相對比較慢了,因為每次遍歷的時候都必須從鏈表的頭部或者鏈表的尾部開始遍歷,時間復雜度為O(n/2)。為了實現快速插入或刪除數據,LinkedList在每個節點都維護了一個前繼節點和一個後續節點,這是一種典型的以時間換空間的思想。LinkedList同時也可以實現棧與隊列的功能。

  二、LinkedList的結構圖

技術分享圖片

  在LinkedList中每個節點有會有兩個指針,一個指向前一個節點,另一個指向下一個節點。鏈表的頭部的前指針為null,尾部的後指針也為null,因此也可以說明LinkedList(基於jdk1.8)是非循環雙向鏈表結構。源碼如下:

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屬性

  1、size: 鏈表的長度

  2、first:鏈表的第一個節點

  3、last:鏈表的最後一個節點

transient int size = 0;

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    
/** * Pointer to last node. * Invariant: (first == null && last == null) || * (last.next == null && last.item != null) */ transient Node<E> last; /** * Constructs an empty list. */

  四、添加節點

  1、鏈表頭部添加新節點

/**
     * Links e as first element.
     *  鏈接頭部
     */
    private void linkFirst(E e) {
        //鏈表的第一個節點
        final Node<E> f = first;
        //創建節點
        final Node<E> newNode = new Node<>(null, e, f);
        //將新創建的節點放到鏈條頭部
        first = newNode;
        //當鏈表為null時,鏈表頭部和尾部都指向新節點
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;//把原本第一個節點的前一個節點指向新的節點
        size++;//鏈表長度加1
        modCount++;//鏈表修改次數加1
    }

  當鏈表為空的時候比較簡單,直接將鏈表的頭部和尾部都指向新節點即可,下面我來說一下在非空的情況下頭部插入新節點:

技術分享圖片

  2、往鏈表尾部插入新節點

/**
     * Links e as last element.
     */
    void linkLast(E e) {
        //原來的最後一個節點
        final Node<E> l = last;
        //創建新的節點,next為null
        final Node<E> newNode = new Node<>(l, e, null);
        //將新節點指向最後一個節點
        last = newNode;
        if (l == null)
            first = newNode;//鏈表為空時第一個節點也指向新節點
        else
            l.next = newNode;//將原最後一個節點的next指針指向新節點
        size++;
        modCount++;
    }

  具體流程:

技術分享圖片

  3、在指定節點之前插入新節點

/**
     * Inserts element e before non-null Node succ.
     *  指定節點之前插入新節點
     */
    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++;
    }

流程:

  技術分享圖片

  五、刪除節點

  1、刪除第一個節點

/**
     * Unlinks non-null first node 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;//將原節點的下一個的前一個節點設置為null,因為該節點已經設置為第一個節點,而第一個節點的前一個節點為null
        size--;
        modCount++;
        return element;
    }

  流程:

技術分享圖片

  2、刪除鏈表最後一個節點

/**
     * Unlinks non-null last node 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;//將前一個節點的下一個節點擲為null
        size--;
        modCount++;
        return element;
    }

  流程:

技術分享圖片

  3、刪除指定節點

/**
     * Unlinks non-null node 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;
    }

  流程:

技術分享圖片

  六、添加數據

  1、add方法:

    /**
     * Appends the specified element to the end of this list.
     *
     * <p>This method is equivalent to {@link #addLast}.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        //向鏈表的最後位置插入一個節點
        linkLast(e);
        return true;
    }

  2、addFirst方法:

/**
     * Inserts the specified element at the beginning of this list.
     *
     * @param e the element to add
     */
    public void addFirst(E e) {
        linkFirst(e);
    }

  具體的插入流程可參照第4部分;

  3、addLast方法:

/**
     * Appends the specified element to the end of this list.
     *
     * <p>This method is equivalent to {@link #add}.
     *
     * @param e the element to add
     */
    public void addLast(E e) {
        linkLast(e);
    }

  具體流程參照第四部分的linkLast方法解釋;

  七、獲取數據

  獲取數據也是分為3個方法,獲取鏈表頭部的節點數據,尾部節點數據和其他的節點數據。獲取頭部和尾部比簡單,直接獲取first節點或last節點就可以了,這裏我們主要看一下是怎麽獲取其他的節點:

/**
     * Returns the element at the specified position in this list.
     *
     * @param index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

  從源碼中可以看到,獲取其他節點的數據時,是根據下標來獲取的,首先先檢查輸入的index下標是否有越界的嫌疑,然後node方法,下面我們看一下node方法具體實現方式:

    /**
     * Returns the (non-null) Node at the specified element index.
     */
    Node<E> node(int index) {
        // assert isElementIndex(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;
        }
    }

  從代碼中可以看到,如果使用get(index)方法時,每一次都需要從頭部或尾部開始遍歷,效率比較低。如果要遍歷LinkedList,也不推薦這種方式。

  八、刪除數據

  刪除數據也是3中方法,只講刪除其他節點數據的方法:

    /**
     * Removes the first occurrence of the specified element from this list,
     * if it is present.  If this list does not contain the element, it is
     * unchanged.  More formally, removes the element with the lowest index
     * {@code i} such that
     * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
     * (if such an element exists).  Returns {@code true} if this list
     * contained the specified element (or equivalently, if this list
     * changed as a result of the call).
     *
     * @param o element to be removed from this list, if present
     * @return {@code true} if this list contained the specified element
     */
    public boolean remove(Object o) {
        if (o == null) {//為null的情況,從頭部開始查找
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {//非null,從頭部開始查找,然後刪除掉
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }

  從源碼中可以看到,在刪除元素的時候是從第一個節點開始一個一個遍歷,通過equals方法的來獲取到需要刪除節點,然後調用unlinke方法將節點刪除掉的。

  九、實現stack相關方法

  棧的數據結構實現了FIFO的順序,即先進先出的規則。

  1、push方法:

/**
     * Pushes an element onto the stack represented by this list.  In other
     * words, inserts the element at the front of this list.
     *
     * <p>This method is equivalent to {@link #addFirst}.
     *
     * @param e the element to push
     * @since 1.6
     */
    public void push(E e) {
        addFirst(e);
    }

  每次添加數據的時候都是添加到鏈表頭部。

  2、pop方法:

/**
     * Pops an element from the stack represented by this list.  In other
     * words, removes and returns the first element of this list.
     *
     * <p>This method is equivalent to {@link #removeFirst()}.
     *
     * @return the element at the front of this list (which is the top
     *         of the stack represented by this list)
     * @throws NoSuchElementException if this list is empty
     * @since 1.6
     */
    public E pop() {
        return removeFirst();
    }

  往棧中獲取一個數據,同時也將棧的第一個數據刪除。

  3、peek方法:

    /**
     * Retrieves, but does not remove, the head (first element) of this list.
     *
     * @return the head of this list, or {@code null} if this list is empty
     * @since 1.5
     */
    public E peek() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
    }

  查看棧中的第一個數據,跟pop方法的區別是peek方法只是查看數據,並沒有刪除數據,pop是從棧中彈出一個數據,需要從棧中刪除數據。

  十、實現queue方法

  隊列也是我們在開發的過程經常使用到數據結構,比如消息隊列等,隊列的特點是每次添加數據的時候都是添加大隊列的尾部,獲取數據時總是從頭部拉取。基於以上特點,我們可以使用LinkedList中的linkLast方式實現數據的添加,使用unLinkfirst方法實現數據的拉取,使用getFisrt方法實現數據的查看,源碼如下:

  1、添加數據:

/**
     * Adds the specified element as the tail (last element) of this list.
     *
     * @param e the element to add
     * @return {@code true} (as specified by {@link Queue#offer})
     * @since 1.5
     */
    public boolean offer(E e) {
        return add(e);
    }

  2、拉取數據:

/**
     * Retrieves and removes the head (first element) of this list.
     *
     * @return the head of this list, or {@code null} if this list is empty
     * @since 1.5
     */
    public E poll() {
        final Node<E> f = first;
        return (f == null) ? null : unlinkFirst(f);
    }

  3、查看數據:

/**
     * Retrieves, but does not remove, the head (first element) of this list.
     *
     * @return the head of this list, or {@code null} if this list is empty
     * @since 1.5
     */
    public E peek() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
    }

  

  十一、LinkedList使用註意事項

  1、LinkedList是非線程安全的,在多線程的環境下可能會發生不可預知的結果,所以在多線程環境中謹慎使用它,可以轉換成線程類,或是使用線程安全的集合類來代替LinkedList的使用。

  2、遍歷LinkedList中的數據的時候,切記別使用fori方式(即隨機順序訪問get(index))去遍歷,建議使用叠代器或foreach方式遍歷。原因在上面的源碼中也說到過,可以看一下第七部分數據獲取中,使用get(index)方法獲取數據時每次都是鏈表頭部或尾部開始遍歷,這樣是非常不合理的,時間復雜度為O(n^2)。在數據量較小的情況下是沒有什麽區別,但是數據上去之後,可能會出現程序假死的現象。測試如下:

public static void main(String[] args) throws Exception {

        List<Integer> list = new LinkedList<>();
        for (int i = 0; i < 100000; i++) {
            list.add(i);
        }

        long start = System.currentTimeMillis();
        for (int i = 0; i < list.size(); i++) {
            list.get(i);
        }
        long end = System.currentTimeMillis();
        System.out.println("使用fori方式所需時間:" + (end - start));

        start = System.currentTimeMillis();
        for (Integer integer : list) {

        }
        end = System.currentTimeMillis();
        System.out.println("使用foreach方式所需時間:" + (end - start));
        start = System.currentTimeMillis();
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()){
            Integer next = iterator.next();
        }
        end = System.currentTimeMillis();
        System.out.println("使用叠代器方式所需時間:" + (end - start));
    }

  三種遍歷10萬條數據所需要時間:

使用fori方式所需時間:5288
使用foreach方式所需時間:3
使用叠代器方式所需時間:2

  從結果中可以看到,使用叠代器或foreach方式比fori方式快的不是十倍百倍,原因是使用foreach和叠代器的時候每次獲取數據後都記錄當前的位置index,當下個循環的時候直接在index+1處獲取即可,而不需要從新在頭部或尾部開始遍歷了。

  十二、總結

  1、LinkedList是非線程安全的。

  2、LinkedList可以存儲null值或重復的數據。

  3、LinkedList底層存儲結構為雙向鏈式非循環結構,這種結構添加刪除的效率高於查詢效率。

  4、與ArrayList相比較,LinkedList的刪除添加數據效率要比ArrayList高,查詢數據效率低於ArrayList。

  5、LinkedList可以用於實現stack和queue數據結構,比如:Queue<T> queue = new LinkedList<T>();

  6、遍歷數據時切勿使用隨機訪問方式遍歷,推薦使用foreach或叠代器遍歷。

  7、如果文章中有什麽寫得不對的地方,歡迎大家指出來。

jdk源碼閱讀筆記-LinkedList