1. 程式人生 > >java jdk1.7版本的LinkedList底層原理解析

java jdk1.7版本的LinkedList底層原理解析

LinkedList集合與ArrayList集合的區別是底層的實現原理也不一樣。LinkedList底層是通過一個雙向連結串列實現(在jdk1.6及以前,是一個迴圈的雙向連結串列),而ArrayList是通過陣列實現的。這裡暫且不討論ArrayList的相關知識,先研究下LinkedList的實現原理,而且是以jdk1.7為基礎的。在jdk1.6及之前的版本,LinkedList底層的實現是一個迴圈的雙向連結串列,並且每個元素是Entry類,而在jdk1.7裡每個元素是Node類,這些區別不詳細討論,只需要知道這裡討論的LinkedList是以jdk1.7為基礎的。
根據原始碼裡的註釋,我們可以知道:
(1)、LinkedList可以進行所有List的操作,因為其實現了List介面,同時LinkedList可以存放任何元素,包括null;
(2)、所有根據索引的查詢操作都是按照雙向連結串列的需要執行的,根據索引從前或從後開始搜尋,並且從最靠近索引的一端開始。例如一個LindedList有5個元素,如果呼叫了get(2)方法,LinkedList將會從頭開始搜尋;如果呼叫get(4)方法,那麼LinkedList將會從後向前搜尋。這樣做的目的可以提升查詢效率。那如何做到這一點呢?在LinkedList內部有一個Node(int index)方法,它會判斷從頭或者從後開始查詢比較快。程式碼如下:
這裡寫圖片描述


(3)、LinkedList不是執行緒安全的,所以在多執行緒的環境下使用LinedList需要注意LinkedList型別變數的執行緒同步問題。當然,有一種方式可以建立一個執行緒安全的LinkedList:
List list = Collections.synchronizedList(new LinkedList(…));
(4)、LinedList的迭代器 iterator 和 listIterator 方法返回的迭代器是快速失敗 的。所謂快速失敗,意思就是如果在迭代器已經建立了的情況下,任何時刻對LinkedList結構的修改,迭代器將會丟擲一個ConcurrentModificationException異常。如下面程式碼中迭代器已經生成,但是還往LinedList新增資料,那麼此後的迭代過程是會丟擲ConcurrentModificationException異常的:
這裡寫圖片描述

下面將研究LinkedList底層的實現原理並瞭解一些常用方法的實現過程:
在瞭解LinkedList的實現原理之前,我們首先需要明白LinkedList中的一個節點是什麼以及它的具體資料結構。LinkedList每個節點是一個Node型別的例項,每個Node例項除了儲存節點的真實值(即真實資料)外,還儲存了這個節點的前一個節點的引用和後一個節點的引用,這樣就實現了雙線連結串列的資料結構。Node資料結構如下:
這裡寫圖片描述
從程式碼中我們可以看到,當建立一個Node節點時,我們需要傳入三個引數,第一個引數就是當前節點的前驅節點,第二個就是節點的真實資料,第三個就是節點的後繼節點。

瞭解完一個元素的具體資料結構,那麼下面我們看看常用方法的具體實現:
1、add(E e)方法:add(E e)方法實際上呼叫的是linkLast(E e)方法,意思是把方法加到連結串列的最後。下面看看linkLast(E e)方法的具體實現:
這裡寫圖片描述
從程式碼中我們可以看到,程式碼中使用了變數first和last,這兩個變數分別儲存了當前連結串列第一個節點和最後一個節點的引用。在新增一個節點之前,首先把指向最後一個節點的引用(即變數 last)儲存起來(即變數 l),然後新建一個節點,指定前驅節點是原來連結串列的最後一個節點,然後把指向最後一個節點的引用(last)指向新建的節點。緊接著就設定新建節點的前驅節點的後繼節點指向新增的節點,最後把整個連結串列的總數加1,完成了新增一個節點的操作。完成新增後的連結串列結構類似如下:
這裡寫圖片描述

2、add(int index, Ee)方法:add(int index, Ee)方法可以指定把某個資料插入指定的位置。我們首先看下原始碼:
這裡寫圖片描述
從原始碼中我們可以看到,方法一開始呼叫了checkPositionIndex(index)方法,這個方法主要的作用是判斷指定的index是否越界,如果越界就丟擲IndexOutOfBoundsException異常,從方法的註釋我們也能看出來。在LinkedList原始碼中,很多方法都會呼叫這個方法去判斷指定的index是否越界,比如set(int index, E e)方法。如果指定的index是合法的,那麼接下來就判斷指定的index是否與LinkedList的size是否相等,如果相等,那麼就直接把節點加到後面即可。如果不相等,則呼叫linkBefore(element, node(index))方法。這裡我們可以看到,linkBefore方法的第二個引數是通過呼叫node(index)的返回值當作引數的,這個方法前面已經講解過。通過node(index)方法獲取指定索引的節點,其實這個方法返回的節點會當作新增節點的後繼節點,通過檢視linkBefore裡的具體原始碼我們就知道原因:
這裡寫圖片描述
從linkBefore方法我們可以看到,在建立新節點時,同時指定了前驅節點和後繼節點,這裡後繼節點就是我們傳進來的第二個引數,所以驗證了我們之前所說的第二個引數即為新增節點的後繼節點的說法。在建立節點之前,我們把後繼節點的前驅節點的引用先儲存到一個pred變數中,然後建立一個新的節點,並指定前驅節點和後繼節點;接著把後繼節點的前驅節點引用指向了新的節點,這樣新的節點就可以找到後繼節點,接下來根據前驅節點如果為空,則更改first指向新建的節點,否則將前驅節點指向後繼節點的引用指向新建的節點,最後把LinkedList的size加1。下面通過圖感受一下linkBefore裡具體的過程:
現在有一個有三個節點的LinkedList:
這裡寫圖片描述

然後我呼叫add(1,”4”)方法,即新增的節點取代了二號位節點(資料為2)的那個節點的位置,後面的節點將往後移:
當執行了final Node pred = succ.prev; final Node newNode = new Node<>(pred, e, succ);這兩句程式碼,LinkedList的結構變成這樣:
這裡寫圖片描述
接著執行:succ.prev = newNode;即把三號位的節點指向前驅節點的引用指向新的節點:
這裡寫圖片描述
這裡pred很明顯不為空,所以會執行pred.next = newNode;這時候LinkedList結構:
這裡寫圖片描述
遮蔽掉pred和succ,我們將看得更直觀:
這裡寫圖片描述
執行到這裡,我們發現新增的節點已經正確的插入到了指定的位置,最後只需要把size加1即可。

通過以上兩個方法,我們可以知道java中的LinkedList的底層實現原理,底層是用一個雙向連結串列的資料結構來儲存資料。連結串列中一個節點是一個Node型別的資料結構,其儲存了一個指向前驅節點的引用、真實資料和一個指向後繼節點的引用。對LinkedList的操作實際上是對指向前驅節點和後繼節點的引用操作。因為LinkedList採用雙線連結串列作為底層的資料結構,所以其插入和刪除效率較高,但是隨機訪問效率較差,因為要遍歷。
下面再看幾個方法的實現原始碼:
remove(int index):
這裡寫圖片描述

這裡寫圖片描述

indexOf(Object o):
這裡寫圖片描述

get(int index):
這裡寫圖片描述