今天在看LinkedList的原始碼的時候,遇到了一個坑。我研究源碼時,發現LinkedList是一個直線型的鏈表結構,但是我在baidu搜尋資料的時候,關於這部分的源碼解析,全部都說LinkedList是一個環形鏈表結構。。我糾結了好長時間,還以為我理解錯了,最後還是在Google搜到了結果:因為我看的源碼是1.7的而baidu出來的幾乎全部都是1.6的。而且也沒有對應的說明。在1.7之後,oracle將LinkedList做了一些最佳化,將1.6中的環形結構最佳化為了直線型了鏈表結構。這裡要提示一下朋友們,看源碼的時候,一定要看版本,有的情況是屬於小改動,有的地方可能有大改動,這樣只會越看越迷糊。
好,言歸正傳。我們來分析一下Java中LinkedList的部分源碼。(本文針對的是1.7的源碼)
LinkedList的基本結構 之前我一直在說鏈錶鏈表,那什麼是鏈表。顧名思義,鏈表就和鏈子一樣,每一環都要串連著後邊的一環和前邊的一環,這樣,當我們需要找這根鏈子的某一環的時候,只要我們能找到鏈子的任意一環,都可以找到我們需要的那一環。我們看一個圖,就能很好的理解了。
在LinkedList中,我們把鏈子的“環”叫做“節點”,每個節點都是同樣的結構。節點與節點之間相連,構成了我們LinkedList的基本資料結構,也是LinkedList的核心。 我們再來看一下LinkedList在jdk1.6和1.7直接結構的區別 首先看1.7中的結構
再來看1.6中的結構
對比一下,知道區別在哪裡了吧。在1.7中,去掉了環形結構,自然在代碼中的也會有部分的改變。 理解了上邊的結構,在分析的時候就會容易許多。
LinkedList的構造方法 LinkedList包含3個全域參數, size存放當前鏈表有多少個節點。 first為指向鏈表的第一個節點的引用。 last為指向鏈表的最後一個節點的引用。
LinkedList構造方法有兩個,一個是無參構造,一個是傳入Collection對象的構造。
// 什麼都沒做,是一個空實現 public LinkedList() { } public LinkedList(Collection<? extends E> c) { this(); addAll(c); } public boolean addAll(Collection<? extends E> c) { return addAll(size, c); } public boolean addAll(int index, Collection<? extends E> c) { // 檢查傳入的索引值是否在合理範圍內 checkPositionIndex(index); // 將給定的Collection對象轉為Object數組 Object[] a = c.toArray(); int numNew = a.length; // 數組為空白的話,直接返回false if (numNew == 0) return false; // 數組不為空白 Node<E> pred, succ; if (index == size) { // 構造方法調用的時候,index = size = 0,進入這個條件。 succ = null; pred = last; } else { // 鏈表非空時調用,node方法返回給定索引位置的節點對象 succ = node(index); pred = succ.prev; } // 遍曆數組,將數組的對象插入到節點中 for (Object o : a) { @SuppressWarnings("unchecked") E e = (E) o; Node<E> newNode = new Node<>(pred, e, null); if (pred == null) first = newNode; else pred.next = newNode; pred = newNode; } if (succ == null) { last = pred; // 將當前鏈表最後一個節點賦值給last } else { // 鏈表非空時,將斷開的部分串連上 pred.next = succ; succ.prev = pred; } // 記錄當前節點個數 size += numNew; modCount++; return true; } 這裡要說明一下,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; } }構造方法的參數順序是:前繼節點的引用,資料,後繼節點的引用。
有了上邊的說明,我們來看LinkedList的構造方法。
這段代碼還是很好理解的。我們可以配合圖片來深入理解。 這段代碼分為了2種情況,一個是原來的鏈表是空的,一個是原來的鏈表有值。我們分別來看 原來有值的情況
配合代碼來看,是不是思路清晰了許多。 原來鏈表是空的話就更好辦了,直接把傳入的Collection對象轉化為數組,數組的第一個值就作為頭結點,即head,之後的順序往裡加入即可。並且節省了改變原節點指向的的操作。
對與兩種構造方法,總結起來,可以概括為:
無參構造為空白實現。有參構造傳入Collection對象,將對象轉為數組,並按遍曆順序將數組首尾相連,全域變數first和last分別指向這個鏈表的第一個和最後一個。
LinkedList部分方法分析 addFirst/addLast分析 我們來看代碼
public void addFirst(E e) { linkFirst(e); } private void linkFirst(E e) { final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); // 建立新的節點,新節點的後繼指向原來的前端節點,即將原前端節點向後移一位,新節點代替頭結點的位置。 first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; modCount++; }其實只要理解了上邊的資料結構,這段代碼是很好理解的。 加入一個新的節點,看方法名就能知道,是在現在的鏈表的頭部加一個節點,既然是頭結點,那麼頭結點的前繼必然為null,所以這也是Node<E> newNode = new Node<>(null, e, f);這樣寫的原因。 之後將first指向了當前鏈表的頭結點,之後對之前的前端節點進行了判斷,若在插入元素之前頭結點為null,則當前加入的元素就是第一個幾點,也就是頭結點,所以當前的狀況就是:頭結點=剛剛加入的節點=尾節點。若在插入元素之前頭結點不為null,則證明之前的鏈表是有值的,那麼我們只需要把新加入的節點的後繼指向原來的頭結點,而尾節點則沒有發生變化。這樣一來,原來的頭結點就變成了第二個節點了。達到了我們的目的。
addLast方法在實現上是個addFirst是一致的,這裡就不在贅述了。有興趣的朋友可以看看原始碼。
其實,LinkedList中add系列的方法都是大同小異的,都是建立新的節點,改變之前的節點的指向關係。僅此而已。
getFirst/getLast方法分析
public E getFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return f.item; } public E getLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return l.item; }這段代碼即不需要解析了吧。。很簡單的。
get方法分析 這裡主要看一下它調用的node方法
public E get(int index) { // 校正給定的索引值是否在合理範圍內 checkElementIndex(index); return node(index).item; } Node<E> node(int 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; } }一開始我很費解,這是要幹嘛。後來我才明白,代碼要做的是:判斷給定的索引值,若索引值大於整個鏈表長度的一半,則從後往前找,若索引值小於整個鏈表的長度的一般,則從前往後找。這樣就可以保證,不管鏈表長度有多大,搜尋的時候最多隻搜尋鏈表長度的一半就可以找到,大大提升了效率。
removeFirst/removeLast方法分析
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; f.item = null; f.next = null; // help GC first = next; if (next == null) last = null; else next.prev = null; size--; modCount++; return element; }
摘迴轉結點,將原來的第二個節點變為頭結點,改變frist的指向,若之前僅剩一個節點,移除之後全部置為了null。
對於LinkedList的其他方法,大致上都是封裝了以上這幾個方法,沒有什麼其他的大的變動。