Java -- 基於JDK1.8的LinkedList源碼分析

1,上週末咱們一塊兒分析了ArrayList的源碼並進行了一些總結,由於最近在看Collection這一塊的東西,下面的圖也是大體的總結了Collection裏面重要的接口和類,若是沒有意外的話後面基本上每個都會和你們一塊兒學習學習,因此今天也就和你們一塊兒來看看LinkedList吧!java

哦,不對,放錯圖了,是下面的圖,嘿嘿嘿。。。node

 

 

2,記得首次接觸LinkedList仍是在大學Java的時候,當時提及LinkedList的特性和應用場景:LinkedList基於雙向鏈表適用於增刪頻繁且查詢不頻繁的場景,線程不安全的且適用於單線程(這點和ArrayList很像)。而後還記得一個很深入的是能夠用LinkedList來實現棧和隊列,那讓咱們一塊兒看一看源碼究竟是怎麼來實現這些特色的面試

  2.1 構造函數數組

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0;
    transient Node<E> first;
    transient Node<E> last;
    
    public LinkedList() {
    }

    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }
    
    public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }
    
    public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        if (numNew == 0)
            return false;

        Node<E> pred, succ;
        if (index == size) {
            succ = null;
            pred = last;
        } else {
            succ = node(index);
            pred = succ.prev;
        }

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

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

        size += numNew;
        modCount++;
        return true;
    }
    
    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;
        }
    }
    
    Node<E> node(int index) {
        // assert isElementIndex(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;
        }
    }    
    
}

  首先咱們知道常見的構造是LinkedList()和LinkedList(Collection<? extends E> c)兩種,而後再來看看咱們繼承的類和實現的接口安全

LinkedList 集成AbstractSequentialList抽象類,內部使用listIterator迭代器來實現重要的方法
LinkedList 實現 List 接口,能對它進行隊列操做。
LinkedList 實現 Deque 接口,即能將LinkedList看成雙端隊列使用。
LinkedList 實現了Cloneable接口,即覆蓋了函數clone(),能克隆。
LinkedList 實現java.io.Serializable接口,這意味着LinkedList支持序列化,能經過序列化去傳輸。

  能夠看到,相對於ArrayList,LinkedList多實現了Deque接口而少實現了RandomAccess接口,且LinkedList繼承的是AbstractSequentialList類,而ArrayList繼承的是AbstractList類。那麼咱們如今有一個疑問,這些多實現或少實現的接口和類會對咱們LinkedList的特色產生影響嗎?這裏咱們先將這個疑問放在內心,咱們先走正常的流程,先把LinkedList的源碼看完(主要是要解釋這些東西看Deque的源碼,還要去看Collections裏面的邏輯,我怕扯遠了)dom

  第5-7行:定義記錄元素數量size,由於咱們以前說過LinkedList是個雙向鏈表,因此這裏定義了鏈表鏈表頭節點first和鏈表尾節點last函數

  第60-70行:定義一個節點Node類,next表示此節點的後置節點,prev表示側節點的前置節點,element表示元素值性能

  第22行:檢查當前的下標是否越界,由於是在構造函數中因此咱們這邊的index爲0,且size也爲0學習

  第24-29行:將集合c轉化爲數組a,並獲取集合的長度;定義節點pred、succ,pred用來記錄前置節點,succ用來記錄後置節點測試

    第70-89行:node()方法是獲取LinkedList中第index個元素,且根據index處於前半段仍是後半段 進行一個折半,以提高查詢效率

  第30-36行:若是index==size,則將元素追加到集合的尾部,pred = last將前置節點pred指向以前結合的尾節點,若是index!=size代表是插入集合,經過node(index)獲取當前要插入index位置的節點,且pred = succ.prev表示將前置節點指向於當前要插入節點位置的前置節點

  第38-46行:鏈表批量增長,是靠for循環遍歷原數組,依次執行插入節點操做,第40行之前置節點 和 元素值e,構建new一個新節點;第41行若是前置節點是空,說明是頭結點,且將成員變量first指向當前節點,若是不是頭節點,則將上一個節點的尾節點指向當前新建的節點;第45行將當前的節點爲前置節點了,爲下次添加節點作準備。這些走完基本上咱們的新節點也都建立出來了,可能這塊代碼有點繞,你們多看看

  第48-53行:循環結束後,判斷若是後置節點是null, 說明此時是在隊尾添加的,設置一下隊列尾節點last,若是不是在隊尾,則更新以前插入位置節點的前節點和當前要插入節點的尾節點

  第55-56行:修改當前集合數量、修改modCount記錄值

  ok,雖說是分析的構造函數的源碼,可是把node(int index)、addAll(int index, Collection<? extends E> c)方法也都看了,因此來小結一下:鏈表批量增長,是靠for循環遍歷原數組,依次執行插入節點操做;經過下標index來獲取節點Node是採用的折半法來提高效率的

  2.2 增長元素

  常見的方法有如下三種

linkedList.add(E e)
linkedList.add(int index, E element)
linkedList.addAll(Collection<? extends E> c)

  來看看具體的源碼

public boolean add(E e) {
        linkLast(e);
        return true;
    }

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

public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

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

public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }

  第二、6-16行:建立一個newNode它的prev指向以前隊尾節點last,並記錄元素值e,以前的隊尾節點last的next指向當前節點,size自增,modcount自增

  第18-20,27-38行:首先去檢查下標是否越界,而後判斷若是加入的位置恰好位於隊尾就和咱們add(E element)的邏輯同樣了,若是不是則須要經過 node(index)函數定位出當前位於index下標的node,再經過linkBefore()函數建立出newNode將其插入到原先index位置

  第40-42行:就是咱們在構造函數中看過的批量加入元素的方法

  OK,添加元素也很簡單,若是是在隊尾進行添加的話只須要建立一個新Node將其前置節點指向以前的last,若是是在隊中添加節點,首選拆散原先的index-一、index、index+1之間的聯繫,新建節點插入進去便可。

  2.3 刪除元素

  常見方法有如下這幾個方法

linkedList.remove(int index)
linkedList.remove(Object o)
linkedList.remove(Collection<?> c)

  源碼以下

public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

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

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

 public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        boolean modified = false;
        Iterator<?> it = iterator();
        while (it.hasNext()) {
            if (c.contains(it.next())) {
                it.remove();
                modified = true;
            }
        }
        return modified;
    }

  第1-4,6-30行:首先根據index經過方法值node(index)來肯定出集合中的下標是index的node,咋們主要看unlink()方法,代碼感受不少,其實只是將當前要刪除的節點node的頭結點的尾節點指向node的尾節點、將node的尾結點的頭節點指向node的頭節點,可能有點繞(哈哈),看一下代碼基本上就能夠理解了,而後將下標爲index的node置空,供GC回收

  第32-49行:首先判斷一下當前要刪除的元素o是否爲空,而後進行for循環定位出當前元素值等於o的節點node,而後再走的邏輯就是上面咱們看到過的unlink()方法,也很簡單,比remove(int index) 多了一步

  第51-62行:這一塊由於涉及到迭代器Iterator,而咱們LinkedList使用的是ListItr,這個後面咱們將迭代器的時候一塊兒講,不過大體的邏輯是均可以看懂的,和咱們的ArrayList的迭代器方法的含義同樣的,能夠先那樣理解

  ok,小結一下, 按下標刪,也是先根據index找到Node,而後去鏈表上unlink掉這個Node。 按元素刪,會先去遍歷鏈表尋找是否有該Node,考慮到容許null值,因此會遍歷兩遍,而後再去unlink它。

  2.5 修改元素

public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }

  只有這一種方法,首先檢查下標是否越界,而後根據下標獲取當前Node,而後修改節點中元素值item,超級簡單

  2.6 查找元素

public E get(int index) {
    checkElementIndex(index);//判斷是否越界 [0,size)
    return node(index).item; //調用node()方法 取出 Node節點,
}


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

 public int lastIndexOf(Object o) {
        int index = size;
        if (o == null) {
            for (Node<E> x = last; x != null; x = x.prev) {
                index--;
                if (x.item == null)
                    return index;
            }
        } else {
            for (Node<E> x = last; x != null; x = x.prev) {
                index--;
                if (o.equals(x.item))
                    return index;
            }
        }
        return -1;
    }

  獲取元素的源碼也很簡單,主要是經過node(index)方法獲取節點,而後獲取元素值,indexOf和lastIndexOf方法的區別在於一個是從頭向尾開始遍歷,一個是從尾向頭開始遍歷

  2.7 迭代器

 public Iterator<E> iterator() {
        return listIterator();
    }

public ListIterator<E> listIterator() {
        return listIterator(0);
    }

public ListIterator<E> listIterator(final int index) {
        rangeCheckForAdd(index);

        return new ListItr(index);
    }

private class ListItr extends Itr implements ListIterator<E> {
        ListItr(int index) {
            cursor = index;
        }

        public boolean hasPrevious() {
            return cursor != 0;
        }

        public E previous() {
            checkForComodification();
            try {
                int i = cursor - 1;
                E previous = get(i);
                lastRet = cursor = i;
                return previous;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

        public int nextIndex() {
            return cursor;
        }

        public int previousIndex() {
            return cursor-1;
        }

        public void set(E e) {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                AbstractList.this.set(lastRet, e);
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        public void add(E e) {
            checkForComodification();

            try {
                int i = cursor;
                AbstractList.this.add(i, e);
                lastRet = -1;
                cursor = i + 1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
    }

  能夠看到,其實最後使用的迭代器是使用的ListIterator類,且集成自Itr,而Itr類就是咱們昨天ArrayList內部使用的類,hasNext()方法和咱們以前的同樣,判斷不等於size大小,而後next()獲取元素主要也是E next = get(i);這行代碼,這樣就又走到咱們以前的獲取元素的源碼當中,得到元素值。

  OK,這樣咱們上面的基本方法都看完了,再來看看咱們上面遺留的問題,首先來看Deque接口有什麼做用,咱們來一塊兒看看

Deque 是 Double ended queue (雙端隊列) 的縮寫,讀音和 deck 同樣,蛋殼。
Deque 繼承自 Queue,直接實現了它的有 LinkedList, ArayDeque, ConcurrentLinkedDeque 等。
Deque 支持容量受限的雙端隊列,也支持大小不固定的。通常雙端隊列大小不肯定。
Deque 接口定義了一些從頭部和尾部訪問元素的方法。好比分別在頭部、尾部進行插入、刪除、獲取元素。

  

public interface Deque<E> extends Queue<E> {
    void addFirst(E e);//插入頭部,異常會報錯
    boolean offerFirst(E e);//插入頭部,異常不報錯
    E getFirst();//獲取頭部,異常會報錯
    E peekFirst();//獲取頭部,異常不報錯
    E removeFirst();//移除頭部,異常會報錯
    E pollFirst();//移除頭部,異常不報錯

    void addLast(E e);//插入尾部,異常會報錯
    boolean offerLast(E e);//插入尾部,異常不報錯
    E getLast();//獲取尾部,異常會報錯
    E peekLast();//獲取尾部,異常不報錯
    E removeLast();//移除尾部,異常會報錯
    E pollLast();//移除尾部,異常不報錯
}

  Deque也就是一個接口,上面是接口裏面的方法,而後瞭解Deque就必須瞭解Queue

public interface Queue<E> extends Collection<E> {
    //往隊列插入元素,若是出現異常會拋出異常
    boolean add(E e);
    //往隊列插入元素,若是出現異常則返回false
    boolean offer(E e);
    //移除隊列元素,若是出現異常會拋出異常
    E remove();
    //移除隊列元素,若是出現異常則返回null
    E poll();
    //獲取隊列頭部元素,若是出現異常會拋出異常
    E element();
    //獲取隊列頭部元素,若是出現異常則返回null
    E peek();
}

  而後咱們知道LinkedList實現了Deque接口,也就是說可使用LinkedList實現棧和隊列的功能,讓寫寫看

package com.ysten.leakcanarytest;

import java.util.Collection;
import java.util.LinkedList;

/**
 * desc    : 實現棧
 * time    : 2018/10/31 0031 19:07
 *
 * @author : wangjitao
 */
public class Stack<T>
{
    private LinkedList<T> stack;

    //無參構造函數
    public Stack()
    {
        stack=new LinkedList<T>();
    }
    //構造一個包含指定collection中全部元素的棧
    public Stack(Collection<? extends T> c)
    {
        stack=new LinkedList<T>(c);
    }
    //入棧
    public void push(T t)
    {
        stack.addFirst(t);
    }
    //出棧
    public T pull()
    {
        return stack.remove();
    }
    //棧是否爲空
    boolean isEmpty()
    {
        return stack.isEmpty();
    }

    //打印棧元素
    public void show()
    {
        for(Object o:stack)
            System.out.println(o);
    }
}

  測試功能

public static void main(String[] args){
        Stack<String> stringStack = new Stack<>();
        stringStack.push("1");
        stringStack.push("2");
        stringStack.push("3");
        stringStack.push("4");
        stringStack. show();
    }


打印結果以下:
4
3
2
1

  隊列的實現相似的,你們能夠下來本身寫一下,而後繼續咱們的問題,實現Deque接口和實現RandomAccess接口有什麼區別,咱們上面看了Deque接口,實現Deque接口能夠擁有雙向鏈表功能,那咱們再來看看RandomAccess接口

1 public interface RandomAccess {
2 }

  發現什麼都沒有,原來RandomAccess接口是一個標誌接口(Marker),然而實現這個接口有什麼做用呢?

  答案是隻要List集合實現這個接口,就能支持快速隨機訪問,然而又有人問,快速隨機訪問是什麼東西?有什麼做用?

  google是這樣定義的:給能夠提供隨機訪問的List實現去標識一下,這樣使用這個List的程序在遍歷這種類型的List的時候能夠有更高效率。僅此而已。

  這時候看一下咱們Collections類中的binarySearch方法

int binarySearch(List<? extends Comparable<? super T>> list, T key) {
        if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
            return Collections.indexedBinarySearch(list, key);
        else
            return Collections.iteratorBinarySearch(list, key);
    }

  能夠看到這時候去判斷了若是當前集合實現了RandomAccess接口就會走Collections.indexedBinarySearch方法,那麼咱們來看一下Collections.indexedBinarySearch()方法和Collections.iteratorBinarySearch()的區別是什麼呢?

int indexedBinarySearch(List<? extends Comparable<? super T>> list, T key) {
        int low = 0;
        int high = list.size()-1;

        while (low <= high) {
            int mid = (low + high) >>> 1;
            Comparable<? super T> midVal = list.get(mid);
            int cmp = midVal.compareTo(key);

            if (cmp < 0)
                low = mid + 1;
            else if (cmp > 0)
                high = mid - 1;
            else
                return mid; // key found
        }
        return -(low + 1);  // key not found
    }



int iteratorBinarySearch(List<? extends Comparable<? super T>> list, T key)
    {
        int low = 0;
        int high = list.size()-1;
        ListIterator<? extends Comparable<? super T>> i = list.listIterator();

        while (low <= high) {
            int mid = (low + high) >>> 1;
            Comparable<? super T> midVal = get(i, mid);
            int cmp = midVal.compareTo(key);

            if (cmp < 0)
                low = mid + 1;
            else if (cmp > 0)
                high = mid - 1;
            else
                return mid; // key found
        }
        return -(low + 1);  // key not found
    }

  經過查看源代碼,發現實現RandomAccess接口的List集合採用通常的for循環遍歷,而未實現這接口則採用迭代器

,那如今讓咱們以LinkedList爲例子看一下,經過for循環、迭代器、removeFirst和removeLast來遍歷的效率(以前忘記寫這一塊了,順便一塊先寫了對於LinkedList那種訪問效率要高一些)

  迭代器遍歷

LinkedList linkedList = new LinkedList();
for(int i = 0; i < 100000; i++){
          linkedList.add(i);
}
// 迭代器遍歷
 long start = System.currentTimeMillis();
 Iterator iterator = linkedList.iterator();
 while(iterator.hasNext()){
        iterator.next();
 }
 long end = System.currentTimeMillis();
  System.out.println("Iterator:"+ (end - start) +"ms");

打印結果:Iterator:28ms

  for循環get遍歷

// 順序遍歷(隨機遍歷)
 long start = System.currentTimeMillis();
 for(int i = 0; i < linkedList.size(); i++){
            linkedList.get(i);
}
long end = System.currentTimeMillis();
System.out.println("for :"+ (end - start) +"ms");

打印結果   for :6295ms

  使用加強for循環

long start = System.currentTimeMillis();
for(Object  i : linkedList);
long end = System.currentTimeMillis();
System.out.println("加強for :"+ (end - start) +"ms");

輸出結果 加強for :6ms

  removeFirst來遍歷

long start = System.currentTimeMillis();
while(linkedList.size() != 0){
            linkedList.removeFirst();
}
long end = System.currentTimeMillis();
System.out.println("removeFirst :"+ (end - start) +"ms");

輸出結果 removeFirst :3ms

  綜上結果能夠看到,遍歷LinkedList時,使用removeFirst()或removeLast()效率最高,而for循環get()效率最低,應避免使用這種方式進行。應當注意的是,使用removeFirst()或removeLast()遍歷時,會刪除原始數據,若只單純的讀取,應當選用迭代器方式或加強for循環方式。

  ok,上述的都是隻針對LinkedList而言測試的,而後咱們接着上面的RandomAccess接口來說,看看經過對比ArrayList的for循環和迭代器遍歷看看訪問效率

  ArrayList的for循環

long start = System.currentTimeMillis();
for (int i = 0; i < arrayList.size(); i++) {
         arrayList.get(i);
}
 long end = System.currentTimeMillis();
 System.out.println("for  :"+ (end - start) +"ms");

輸出結果  for  :3ms

  ArrayList的迭代遍歷

 long start = System.currentTimeMillis();
Iterator iterable = arrayList.iterator() ;
while (iterable.hasNext()){
         iterable.next();
}
 long end = System.currentTimeMillis();
 System.out.println("for  :"+ (end - start) +"ms");

輸出結果 for  :6ms

  因此讓咱們來綜上對比一下

ArrayList
    普通for循環:3ms
    迭代器:6ms
LinkedList
    普通for循環:6295ms    
    迭代器:28ms

  從上面數據能夠看出,ArrayList用for循環遍歷比iterator迭代器遍歷快,LinkedList用iterator迭代器遍歷比for循環遍歷快,因此對於不一樣的List實現類,遍歷的方式有所不用,RandomAccess接口這個空架子的存在,是爲了可以更好地判斷集合是否ArrayList或者LinkedList,從而可以更好選擇更優的遍歷方式,提升性能!

  (在這裏忽然想起在去年跳槽的時候,有家公司的面試官問我,list集合的哪種遍歷方式要快一些,而後我說我沒有每一個去試過,結果那位大佬說的是for循環遍歷最快,還叫我下去試試,如今想一想,只有在集合是ArrayList的時候for循環才最快,對於LinkedList來講for循環反而是最慢的,那位大佬,你欠我一聲對不起(手動斜眼微笑))

3,上面把咱們該看的點都看了,那麼咱們再來總結總結:

  LinkedList 是雙向列表,鏈表批量增長,是靠for循環遍歷原數組,依次執行插入節點操做。

  ArrayList基於數組, LinkedList基於雙向鏈表,對於隨機訪問, ArrayList比較佔優點,但LinkedList插入、刪除元素比較快,由於只要調整指針的指向。針對特定位置須要遍歷時,因此LinkedList在隨機訪問元素的話比較慢。

  LinkedList沒有實現本身的 Iterator,使用的是 ListIterator。

  LinkedList須要更多的內存,由於 ArrayList的每一個索引的位置是實際的數據,而 LinkedList中的每一個節點中存儲的是實際的數據和先後節點的位置。

  LinkedList也是非線程安全的,只有在單線程下才可使用。爲了防止非同步訪問,Collections類裏面提供了synchronizedList()方法。

 

  好了,也不早了,你們早點休息,下次再見。。。

相關文章
相關標籤/搜索