Java集合源碼分析之LinkedList

前言

前面一篇咱們分析了ArrayList的源碼,這一篇分享的是LinkedList。咱們都知道它的底層是由鏈表實現的,因此咱們要明白什麼是鏈表?html

1、LinkedList簡介

1.一、LinkedList概述

  •   LinkedList是一種能夠在任何位置進行高效地插入和移除操做的有序序列,它是基於雙向鏈表實現的。
  •   LinkedList 是一個繼承於AbstractSequentialList的雙向鏈表。它也能夠被看成堆棧、隊列或雙端隊列進行操做。
  •   LinkedList 實現 List 接口,能對它進行隊列操做。
  •   LinkedList 實現 Deque 接口,即能將LinkedList看成雙端隊列使用。
  •   LinkedList 實現了Cloneable接口,即覆蓋了函數clone(),能克隆。
  •   LinkedList 實現java.io.Serializable接口,這意味着LinkedList支持序列化,能經過序列化去傳輸。
  •   LinkedList 是非同步的。

1.二、LinkedList的數據結構

1)基礎知識補充java

1.1)單向鏈表:經過每一個結點的指針指向下一個結點從而連接起來的結構,最後一個節點的next指向null。node

element:用來存放元素面試

next:用來指向下一個節點元素數組

1.2)單向循環鏈表安全

element、next 跟前面同樣數據結構

在單向鏈表的最後一個節點的next會指向頭節點,而不是指向null,這樣存成一個環多線程

1.3)雙向鏈表:包含兩個指針的,pre指向前一個節點,next指向後一個節點,可是第一個節點head的pre指向null,最後一個節點的tail也指向null。dom

element:存放元素異步

pre:用來指向前一個元素

next:指向後一個元素

1.4)雙向循環鏈表

element、pre、next 跟前面的同樣

第一個節點的pre指向最後一個節點,最後一個節點的next指向第一個節點,也造成一個「環」。

2)LinkedList的數據結構

如上圖所示:LinkedList底層使用的是雙向鏈表結構,有一個頭結點和一個尾結點,雙向鏈表意味着咱們能夠從頭開始正向遍歷,或者是從尾開始逆向遍歷,而且能夠針對頭部和尾部進行相應的操做。

1.三、LinkedList的特性

  • 異步,也就是非線程安全
  • 雙向鏈表,因爲實現了list和Deque接口,可以看成隊列來使用。鏈表:查詢效率不高,可是插入和刪除這種操做性能好。
  • 是順序存儲結構(注意和隨機存取結構兩個概念搞清楚)

2、LinkedList源碼分析

2.一、LinkedList的繼承結構以及層次關係

分析:咱們能夠看到,linkedList在最底層,說明他的功能最爲強大,而且細心的還會發現,arrayList只有四層,這裏多了一層AbstractSequentialList的抽象類,爲何呢?

經過API咱們會發現:

  • 減小實現順序存取(例如LinkedList)這種類的工做,就是方便,抽象出相似LinkedList這種類的一些共同的方法
  • 那麼若是想本身實現順序存取這種特性的類(就是鏈表形式),那麼就繼承這個AbstractSequentialList抽象類,若是想像數組那樣的隨機存取的類,那麼就去實現AbstractList抽象類。
  • 這樣的分層,就很符合咱們抽象的概念,越往高處的類,就越抽象,越往底層的類,就越有本身獨特的個性。
  • 實現了Deque接口,那麼也就意味着LinkedList是雙端隊列的一種實現,因此,基於雙端隊列的操做在LinkedList中所有有效。

實現接口分析:

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

1)List接口:列表,add、set、等一些對列表進行操做的方法

2)Deque接口:有隊列的各類特性,

3)Cloneable接口:可以複製,使用那個copy方法。

4)Serializable接口:可以序列化。

5)應該注意到沒有RandomAccess:那麼就推薦使用iterator,在其中就有一個foreach,加強的for循環,其中原理也就是iterator,咱們在使用的時候,使用foreach或者iterator均可以。

2.二、類的屬性 

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;//尾結點

LinkedList屬性很是簡單,一個頭結點、一個尾結點、一個表示鏈表中實際元素的變量。注意,頭尾結點都有transient關鍵字修飾,這也意味着在序列化時該域是不會序列化的。

2.三、LinkedList的構造方法

兩個構造方法(兩個構造方法都是規範規定須要寫的)

1)空參構造函數

/**
 * Constructs an empty list.
 */
public LinkedList() {
}

2)有參構造函數

//將集合中的元素構建成LinkedList鏈表
public LinkedList(Collection<? extends E> c) {
    //調用無參構造函數
    this();
    //添加集合中全部的元素
    addAll(c);
}

2.四、內部類(Node)

//根據前面介紹雙向鏈表就知道這個表明什麼了,linkedList的奧祕就在這裏
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;
    }
}

2.五、核心方法

2.5.一、add()方法

1)add(E)

public boolean add(E e) {
    //添加到末尾
    linkLast(e);
    return true;
}

說明:add函數用於向LinkedList中添加一個元素,而且添加到鏈表尾部。具體添加到尾部的邏輯是由linkLast函數完成的。

分析:LinkLast(XXXXX)

/**
 * Links e as last element.
 */
void linkLast(E e) {
    final Node<E> l = last;//臨時結點保存last,也就是l指向了最後一個結點
    final Node<E> newNode = new Node<>(l, e, null);//將e封裝爲結點,而且e.prev指向了最後一個結點
    last = newNode;//newNode成爲最後一個結點,因此last指向了它
    if (l == null)//判斷是否是一開始鏈表中就什麼都沒有,若是沒有,則newNode就成爲了第一個結點,first和last都要指向它
        first = newNode;
    else//正常的在最後一個結點追加,那麼原先的最後一個結點的next就要指向如今真正最後一個結點,原先的最後一個結點就變成了倒數第二個結點
        l.next = newNode;
    size++;//添加一個結點,size自增
    modCount++;
}

說明:對於添加一個元素至鏈表中會調用add方法->LinkLast方法

舉例一:

LinkedList<Integer> lists = new LinkedList<Integer>();
lists.add(5);
lists.add(6);

首先調用無參構造函數,以後添加元素5,以後再添加元素6。具體的示意圖以下:

2.5.二、addAll方法

addAll有兩個重載函數,addAll(Collection<? extends E>)型和addAll(int, Collection<? extends E>)型,咱們平時習慣調用的addAll(Collection<? extends E>)型會轉化爲addAll(int, Collection<? extends E>)型。

1)addAll(c);

public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
    checkPositionIndex(index);
    //將集合c轉換爲Object數組a
    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)
        //集合爲空,則什麼都不作,返回false
        return false;
    //定義兩個結點(內部類),每一個結點都有三個屬性,item,next,prev.
    Node<E> pred, succ;
    //若是不指定添加結點位置,index=0,size=添加的結點個數,若結點個數爲0,則succ=null,pred=last=null
    //若是指定添加結點位置,且添加結點位置和要添加的結點size相等
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        succ = node(index);
        pred = succ.prev;
    }

    //將遍歷數組a中的元素,封裝爲一個個結點
    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        //pred就是以前構建好的,可能爲null,也可能不爲null
        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;
}

說明:參數中的index表示在索引下標爲index的結點(其實是第index + 1個結點)的前面插入。     

在addAll函數中,addAll函數中還會調用到node函數,get函數也會調用到node函數,此函數是根據索引下標找到該結點並返回,具體代碼以下:

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;//返回該結點
    }
}

說明:在根據索引查找結點時,會有一個小優化,結點在前半段則從頭開始遍歷,在後半段則從尾開始遍歷,這樣就保證了只須要遍歷最多一半結點就能夠找到指定索引的結點。

舉例說明調用addAll函數後的鏈表狀態:

 List<Integer> lists = new LinkedList<Integer>();
    lists.add(5);
    lists.addAll(0, Arrays.asList(2, 3, 4, 5));

上述代碼內部的鏈表結構以下:

addAll()中的一個問題:    

在addAll函數中,傳入一個集合參數和插入位置,而後將集合轉化爲數組,而後再遍歷數組,挨個添加數組的元素,可是問題來了,爲何要先轉化爲數組再進行遍歷,而不是直接遍歷集合呢?

從效果上二者是徹底等價的,均可以達到遍歷的效果。關於爲何要轉化爲數組的問題,個人思考以下:

1. 若是直接遍歷集合的話,那麼在遍歷過程當中須要插入元素,在堆上分配內存空間,修改指針域,這個過程當中就會一直佔用着這個集合,考慮正確同步的話,其餘線程只能一直等待。

2. 若是轉化爲數組,只須要遍歷數組,而遍歷數組過程當中不須要額外的操做,

因此佔用的時間相對是較短的,這樣就利於其餘線程儘快的使用這個集合。說白了,就是有利於提升多線程訪問該集合的效率,儘量短期的阻塞。

2.5.三、remove(Object o)

//若是咱們要移除的值在鏈表中存在多個同樣的值,那麼咱們會移除index最小的那個,也就是最早找到的那個值,若是不存在這個值,那麼什麼也不作
public boolean remove(Object o) {
    //這裏能夠知道,linkedList也能存儲null
    if (o == null) {
        //循環遍歷鏈表。直到找到null值,而後使用unlink移除該值。
        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;
}

unlink(xxxx)

E unlink(Node<E> x) {
    // assert x != null;
    //拿到結點x的三個屬性
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    //這裏開始往下就進行移除該元素的操做,也就是把指向那個結點搞定
    if (prev == null) {
        //說明移除的結點是頭結點,則first頭結點應該指向下一個結點
        first = next;
    } else {
        //不是頭結點,prev.next=next:
        prev.next = next;
        //而後解除x結點的指向
        x.prev = null;
    }

    if (next == null) {
        //說明移除的結點是尾結點
        last = prev;
    } else {
        //不是尾結點
        next.prev = prev;
        x.next = null;
    }
    //x的先後都指向爲null,也把item爲null,讓gc回收它
    x.item = null;
    //移除一個結點,size自減
    size--;
    modCount++;
    return element;//因爲一開始保存了x的值到element,因此返回
}

2.5.四、get(index)

get(index)查詢元素的方法

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}
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;
    }
}

2.5.五、indexOf(Object o)

//這個很簡單,就是經過實體元素來查找到該元素在鏈表中的位置
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;
}

3、LinkedList的迭代器

在LinkedList中除了有一個Node的內部類外,應該還能看到另外兩個內部類,那就是ListItr,還有一個是DescendingIterator。

3.一、ListItr內部類

public ListIterator<E> listIterator(int index) {
    checkPositionIndex(index);
    return new ListItr(index);
}

看一下他的繼承結構,發現只繼承了一個ListIterator,到ListIterator中一看:

看到方法名以後,就發現不止有向後迭代的方法,還有向前迭代的方法,因此咱們就知道了這個ListItr這個內部類幹嗎用的了,就是能讓linkedList不光能像後迭代,也能向前迭代。

看一下ListItr中的方法,能夠發現,在迭代的過程當中,還能移除、修改、添加值得操做。

3.二、DescendingIterator內部類    

private class DescendingIterator implements Iterator<E> {
    private final ListItr itr = new ListItr(size());
    public boolean hasNext() {
        return itr.hasPrevious();
    }
    public E next() {
        return itr.previous();
    }
    public void remove() {
        itr.remove();
    }
}

看下這個類,仍是調用ListItr,做用是封裝一下Itr中幾個方法,讓使用者以正常的思惟去寫代碼,例如,在從後往前遍歷的時候,也是跟從前日後遍歷同樣,使用next等操做,而不使用previous。

微軟面試題:

掌握LinkedList的特殊方法,用LinkedList實現字符串反轉輸出,實現字符串0到k 和k+1到2k反轉,再合併輸出

public class Linked001 {
    public static void main(String[] args) {
        String str="abcdefgh";
        System.out.println("要反轉的字符串:"+str);
        LinkedList<Character> list = new LinkedList<>();
        for (int i = 0; i < (str.length()>>1); i++) {
            char c=str.charAt(i);
            list.addFirst(c);
        }
        System.out.println("list,0->k之間:"+list);

        LinkedList<Character> list2 = new LinkedList<>();
        for (int i = str.length()>>1; i < str.length(); i++) {
            char c=str.charAt(i);
            list2.addFirst(c);
        }
        System.out.println("list2,k+1->2k之間:"+list2);
        list.addAll(list2);
        System.out.println("合併後,2k:"+list);
        String newStr="";
        while (list.size()>0){
            newStr+=list.removeFirst();
        }
        System.out.println("newStr:"+newStr);
    }
}
public void addFirst(E e) {
    linkFirst(e);
}
private void linkFirst(E e) {
    //先記錄first結點,若第一次添加,first=null,則f=null
    final Node<E> f = first;
    //建立一個新的結點,prev=null,item=e,next=f,若f=null.則結點爲,[null,e,null],若f結點不爲null,則[null,e,next]
    final Node<E> newNode = new Node<>(null, e, f);
    //first指向newNode
    first = newNode;
    //判斷f是否爲null,如果第一次添加則爲null,不然不爲null
    if (f == null)
        //last指向新結點
        last = newNode;
    else
        //f的前結點,也就是第一個結點指向了新的結點
        f.prev = newNode;
    //鏈表長度加1
    size++;
    modCount++;
}
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    //獲取第一個結點的值
    final E element = f.item;
    //獲取第一個節點的下一個結點
    final Node<E> next = f.next;
    //將第一個結點的值置爲null
    f.item = null;
    //將第一個結點的next指向null
    f.next = null; // help GC
    //first指向第一個要刪除的結點的下一個結點
    first = next;
    //若是第一個結點的下一個結點爲null,說明只有一個結點,則last指向null,刪了這個結點就沒了
    if (next == null)
        last = null;
    else
        //說明還有下一個結點,則下一個結點的前置置爲null,說明這個結點是首結點
        next.prev = null;
    //鏈表長度減小1
    size--;
    modCount++;
    //返回刪除的第一個節點的值
    return element;
}

輸出結果:

4、總結

  1)linkedList本質上是一個雙向鏈表,經過一個Node內部類實現的這種鏈表結構。
  2)能存儲null值
  3)跟arrayList相比較,就真正的知道了,LinkedList在刪除和增長等操做上性能好,而ArrayList在查詢的性能上好
  4)從源碼中看,它不存在容量不足的狀況
  5)linkedList不光可以向前迭代,還能像後迭代,而且在迭代的過程當中,能夠修改值、添加值、還能移除值。
  6)linkedList不光能當鏈表,還能當隊列使用,這個就是由於實現了Deque接口

 

本文參考:https://www.cnblogs.com/zhangyinhua/p/7688304.html

相關文章
相關標籤/搜索