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、如果文章中有什麽寫得不對的地方,歡迎大家指出來。
jdk源碼閱讀筆記-LinkedList