Java集合框架之二:LinkedList源碼解析

版權聲明:本文爲博主原創文章,轉載請註明出處,歡迎交流學習!數組

     LinkedList底層是經過雙向循環鏈表來實現的,其結構以下圖所示:安全

     

     鏈表的組成元素咱們稱之爲節點,節點由三部分組成:前一個節點的引用地址、數據、後一個節點的引用地址。LinkedList的Head節點不包含數據,每個節點對應一個Entry對象。下面咱們經過源碼來分析LinkedList的實現原理。多線程

     一、Entry類源碼:源碼分析

     

 1 private static class Entry<E> {
 2     E element;
 3     Entry<E> next;
 4     Entry<E> previous;
 5 
 6     Entry(E element, Entry<E> next, Entry<E> previous) {
 7         this.element = element;
 8         this.next = next;
 9         this.previous = previous;
10     }
11     }

     Entry類包含三個屬性,其中element存放業務數據;next存放後一個節點的信息,經過next能夠找到後一個節點;previous存放前一個節點的信息,經過previous能夠找到前一個節點。學習

 

     二、LinkedList的構造方法:LinkedList提供了兩個帶不一樣參數的構造方法。this

     1) LinkedList(),構造一個空列表。spa

     2) LinkedList(Collection<? extends E> c),構造一個包含指定 collection 中的元素的列表,這些元素按其 collection 的迭代器返回的順序排列。線程

     

 1 private transient Entry<E> header = new Entry<E>(null, null, null); //聲明一個空的Entry對象
 2 private transient int size = 0; //集合中節點的個數
 3 
 4 public LinkedList() {
 5    header.next = header.previous = header; //構造一個雙向循環鏈表,header的前驅節點和後置節點都指向header本身
 6 }
 7 
 8 public LinkedList(Collection<? extends E> c) {
 9    this(); //調用不帶參數的構造方法,建立一個空的循環鏈表
10    addAll(c); //調用addAll方法將Collection的元素添加到LinkedList中
11 }

      當調用不帶參數的構造方法,header的前驅節點和後置節點都指向header本身,構成了一個雙向循環的鏈表。以下圖所示:指針

                                                          

      當調用帶集合參數的構造方法生成LinkedList對象時,會先調用不帶參數的構造方法建立一個空的循環鏈表,而後調用addAll方法將集合元素添加到LinkedList中。
 code

      三、向集合中添加元素:LinkedList提供了多種不一樣的add方法向集合添加元素。

      1) add(E e),將指定元素添加到此列表的結尾。

      2) add(int index, E element),在此列表中指定的位置插入指定的元素。

      3) addAll(Collection<? extends E> c),添加指定 collection 中的全部元素到此列表的結尾,順序是指定 collection 的迭代器返回這些元素的順序。

      4) addAll(int index, Collection<? extends E> c),將指定 collection 中的全部元素從指定位置開始插入此列表。

      5) addFirst(E e),將指定元素插入此列表的開頭。

      6) addLast(E e),將指定元素添加到此列表的結尾。 

      經過源碼來分析其底層的實現原理:  

      

 1 public boolean add(E o) {
 2     addBefore(o, header);  
 3     return true;
 4 }
 5 
 6 private Entry<E> addBefore(E o, Entry<E> e) {
 7     Entry<E> newEntry = new Entry<E>(o, e, e.previous); //建立的對象newEntry的數據爲o,後置節點爲header,前驅節點爲header.previous即header自己
 8     newEntry.previous.next = newEntry;  //newEntry.previous即爲header,也就是header.next = newEntry,把header的後置節點設置爲newEntry
 9     newEntry.next.previous = newEntry;  //newEntry.next即header,也就是header.previous = newEntry,通過以上步驟header節點和newEntry節點造成閉環
10     size++;  //節點數加1
11     modCount++;
12     return newEntry;
13 }

     經過以上源碼分析可知,add(E elment)方法新添加一個元素element時,在LinkedList底層首先建立一個Entry類型的對象newEntry,並把元素element存放在newEntry中,同時把newEntry的前驅節點設置爲header,後置節點也設置爲header。接下來,將header節點的後置節點設置爲newEntry,將header的前驅節點也設置爲newEntry,而且節點數size加1。經過以上操做,節點header和節點newEntry造成閉環(雙向循環鏈表)。其示意圖以下:

 

                              

 

    

1 public void add(int index, E element) {
2       addBefore(element, (index==size ? header : entry(index)));//調用addBefore將元素插入指定的位置
3 }

     add(int index, E element)方法的實現原理與add(E elment)方法相似,都是調用addBefore方法實現,其本質仍是生成一個新的Entry對象,而後修改指定位置的節點的前驅節點信息以及後置節點信息,這裏就再也不贅述了。

     

 1 public boolean addAll(Collection<? extends E> c) {
 2         return addAll(size, c);
 3 }
 4 
 5 public boolean addAll(int index, Collection<? extends E> c) {
 6         if (index < 0 || index > size) //檢查參數index是否合法
 7             throw new IndexOutOfBoundsException("Index: "+index+
 8                                                 ", Size: "+size);
 9         Object[] a = c.toArray(); //返回一個包含Collection集合全部元素的Object[]
10         int numNew = a.length;
11         if (numNew==0)  //若是須要插入的元素爲0,則返回false,表示沒有元素須要插入
12             return false;
13     modCount++; //不然插入元素,鏈表修改次數加1
14 
15         Entry<E> successor = (index==size ? header :entry(index)); //獲取index位置的節點。插入位置若是等人size,則在頭節點header處插入,不然在獲取的index處插入
16         Entry<E> predecessor = successor.previous; //獲取節點的前驅節點,插入時須要修改此節點的next
17     for (int i=0; i<numNew; i++) { //將數組中第一個元素插入到index處,將以後的元素按順序插在這個節點後面
18             Entry<E> e = new Entry<E>((E)a[i], successor, predecessor); //建立一個Entry對象
19             predecessor.next = e; //將新插入的節點的前一個節點的next指向當前節點
20             predecessor = e; //將新插入的節點賦值給predecessor,至關於指針向後移,實現循環
21         }
22         successor.previous = predecessor; //將斷開的index處的節點的previous指向插入的最後一個節點
23 
24         size += numNew; //修改節點個數
25         return true;
26     }

    

1 public void addFirst(E o) {
2     addBefore(o, header.next);
3 }
4 
5 public void addLast(E o) {
6     addBefore(o, header);
7 }

     從源碼可知,這兩個方法都是調用addBefore(E e, Entry entry)方法實現,插入節點的示意圖以下:

                                              

 

     結合addBefore(E e, Entry entry)方法不難理解addFirst(E e)只須要在header下一個節點以前插入便可;addLast(E e)只須要在header以前插入,由於在循環鏈表中,header前一個節點也是鏈表的最後一個節點。

 

     四、獲取LinkedList中的元素:

     1) get(int index),返回此列表中指定位置處的元素。

     2) getFirst(),返回此列表的第一個元素。

     3) getLast(),返回此列表的最後一個元素。

     

 1 public E get(int index) {
 2         return entry(index).element;
 3 }
 4 
 5 private Entry<E> entry(int index) {  //獲取雙向鏈表指定位置的節點
 6         if (index < 0 || index >= size)  //檢查參數index合法性,當指定的位置index小於零或大於集合容量,拋出異常
 7             throw new IndexOutOfBoundsException("Index: "+index+
 8                                                 ", Size: "+size);
 9         Entry<E> e = header;
10         if (index < (size >> 1)) { //獲取index處的節點,當index小於鏈表長度的1/2,則從前向後查找;不然從後向前查找。這樣能提供查找效率。
11             for (int i = 0; i <= index; i++)
12                 e = e.next;
13         } else {
14             for (int i = size; i > index; i--)
15                 e = e.previous;
16         }
17         return e;
18 }
19 
20  public E getFirst() {
21     if (size==0)
22         throw new NoSuchElementException();
23 
24     return header.next.element; //返回header節點的下一個節點。
25  }
26 
27 public E getLast()  {
28     if (size==0)
29         throw new NoSuchElementException();
30 
31     return header.previous.element; //返回header節點的前一個節點,因爲是雙向循環鏈表,header前一個節點也是鏈表的最後一個節點。
32  }

     從LinkedList底層鏈表的實現原理可知,LinkedList集合的查找操做是從header節點開始的。能夠從header節點開始從前向後一個一個節點順序查找或者從後向前依次順序查找。而ArrayList的查找操做則是直接獲取數組下標處的元素,所以查詢效率更高。

 

     五、移除LinkedList中的元素:

     1) remove(),獲取並移除此列表的頭(第一個元素)。

     2) remove(int index),移除此列表中指定位置處的元素。

     3) remove(Object o),今後列表中移除首次出現的指定元素(若是存在)。

     4) removeFirst(),移除並返回此列表的第一個元素。此方法等同於remove()。

     5) removeLast(),移除並返回此列表的最後一個元素。

     

 1 public E remove() {
 2         return removeFirst(); //調用removeFirst()方法。
 3  }
 4 
 5 public E remove(int index) {
 6         return remove(entry(index));  //調用私有方法remove(Entry<E> e)以及entry(int index)。
 7  }
 8 
 9 public E removeFirst() {  
10     return remove(header.next);   //調用私有方法remove(Entry<E> e)。
11  }
12 
13 public E removeLast() {
14     return remove(header.previous);   //調用私有方法remove(Entry<E> e)。
15  }
16 
17 public boolean remove(Object o) {
18         if (o==null) {  若是要刪除的元素內容爲null
19             for (Entry<E> e = header.next; e != header; e = e.next) {
20                 if (e.element==null) {
21                     remove(e);  //調用私有方法remove(Entry<E> e)。
22                     return true;
23                 }
24             }
25         } else {
26             for (Entry<E> e = header.next; e != header; e = e.next) {
27                 if (o.equals(e.element)) {
28                     remove(e); 
29                     return true;
30                 }
31             }
32         }
33         return false;
34  }
35 
36 private E remove(Entry<E> e) {
37     if (e == header)  
38         throw new NoSuchElementException();
39 
40         E result = e.element;  //保存將要被移除的節點的內容。
41     e.previous.next = e.next;  //將節點e的前驅節點的後繼設置爲e的後繼節點。
42     e.next.previous = e.previous; //將e的後繼節點的前驅設置爲e的前驅節點。
43         e.next = e.previous = null;  //將e節點的後繼指向以及前驅指向設置爲空。
44         e.element = null;  //將e節點的內容清空。
45     size--; //節點個數減1。
46     modCount++; //鏈表操做次數加1。
47         return result;  //返回被刪除的節點內容。
48   }

      從源碼能夠看出,移除節點操做最終都是調用私有方法remove(Entry<E> e)。刪除某一節點的操做本質上是修改其相關節點的指針信息。其原理以下:

      e.previous.next = e.next; //預刪除節點e的前驅節點的後繼指針指向e的後繼節點。

      e.next.previous = e.previous; //預刪除節點e的後繼節點的前驅指針指向e的前驅節點。

      清空預刪除節點的指針信息以及內容:

      e.next = e.previous = null; //清空後繼指針和前驅指針的信息。

      e.element = null; //清空節點的內容。

 

     經過對以上部分關鍵源碼的分析,咱們能夠知道LinkedList集合的底層是基於雙向循環鏈表來實現的。鏈表中維護了一個個Entry對象,Entry對象由三部分組成:previous(前驅指針,指向前驅節點)、element(數據)、next(後繼指針,指向後繼節點)。對LinkedList集合元素的操做本質上是對鏈表中維護的Entry對象的操做(修改相關對象的指針信息、數據),明白了這一點,就能很容易的理解LinkedList集合的經常使用方法的底層實現原理。下面對LinkedList源碼有幾點總結:

    1) LinkedList不是線程安全的。在多線程環境下,可使用Collections.synchronizedList方法聲明一個線程安全的ArrayList,例如:

        List list = Collections.sychronizedList(new LinkedList());

    2) LinkedList是有序集合。LinkedList的查找操做是從header節點開始的,從前日後或者從後往前依次逐個節點查找,所以查詢效率低,而ArrayList能夠直接經過下標查找,查詢效率高;因爲LinkedList集合元素的添加、刪除操做只須要修改節點的指針信息,所以添加、刪除元素的效率高,而ArrayList的添加、刪除操做會涉及大量元素位置的遷移,所以添加、刪除元素的效率低。

相關文章
相關標籤/搜索