jdk原始碼閱讀筆記-LinkedList
一、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 ? get(i)==null : 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、如果文章中有什麼寫得不對的地方,歡迎大家指出來。