前面一篇咱們分析了ArrayList的源碼,這一篇分享的是LinkedList。咱們都知道它的底層是由鏈表實現的,因此咱們要明白什麼是鏈表?html
- LinkedList是一種能夠在任何位置進行高效地插入和移除操做的有序序列,它是基於雙向鏈表實現的。
- LinkedList 是一個繼承於AbstractSequentialList的雙向鏈表。它也能夠被看成堆棧、隊列或雙端隊列進行操做。
- LinkedList 實現 List 接口,能對它進行隊列操做。
- LinkedList 實現 Deque 接口,即能將LinkedList看成雙端隊列使用。
- LinkedList 實現了Cloneable接口,即覆蓋了函數clone(),能克隆。
- LinkedList 實現java.io.Serializable接口,這意味着LinkedList支持序列化,能經過序列化去傳輸。
- 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底層使用的是雙向鏈表結構,有一個頭結點和一個尾結點,雙向鏈表意味着咱們能夠從頭開始正向遍歷,或者是從尾開始逆向遍歷,而且能夠針對頭部和尾部進行相應的操做。
- 異步,也就是非線程安全
- 雙向鏈表,因爲實現了list和Deque接口,可以看成隊列來使用。鏈表:查詢效率不高,可是插入和刪除這種操做性能好。
- 是順序存儲結構(注意和隨機存取結構兩個概念搞清楚)
分析:咱們能夠看到,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均可以。
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關鍵字修飾,這也意味着在序列化時該域是不會序列化的。
兩個構造方法(兩個構造方法都是規範規定須要寫的)
1)空參構造函數
/** * Constructs an empty list. */ public LinkedList() { }
2)有參構造函數
//將集合中的元素構建成LinkedList鏈表 public LinkedList(Collection<? extends E> c) { //調用無參構造函數 this(); //添加集合中全部的元素 addAll(c); }
//根據前面介紹雙向鏈表就知道這個表明什麼了,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.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; }
在LinkedList中除了有一個Node的內部類外,應該還能看到另外兩個內部類,那就是ListItr,還有一個是DescendingIterator。
public ListIterator<E> listIterator(int index) { checkPositionIndex(index); return new ListItr(index); }
看一下他的繼承結構,發現只繼承了一個ListIterator,到ListIterator中一看:
看到方法名以後,就發現不止有向後迭代的方法,還有向前迭代的方法,因此咱們就知道了這個ListItr這個內部類幹嗎用的了,就是能讓linkedList不光能像後迭代,也能向前迭代。
看一下ListItr中的方法,能夠發現,在迭代的過程當中,還能移除、修改、添加值得操做。
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。
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; }
輸出結果:
1)linkedList本質上是一個雙向鏈表,經過一個Node內部類實現的這種鏈表結構。
2)能存儲null值
3)跟arrayList相比較,就真正的知道了,LinkedList在刪除和增長等操做上性能好,而ArrayList在查詢的性能上好
4)從源碼中看,它不存在容量不足的狀況
5)linkedList不光可以向前迭代,還能像後迭代,而且在迭代的過程當中,能夠修改值、添加值、還能移除值。
6)linkedList不光能當鏈表,還能當隊列使用,這個就是由於實現了Deque接口