1. 程式人生 > >LinkedList實現原理以及原始碼解析(1.7)

LinkedList實現原理以及原始碼解析(1.7)

LinkedList實現原理以及原始碼解析(1.7)

在1.7之後,oracle將LinkedList做了一些優化, 將1.6中的環形結構優化為了直線型了連結串列結構。

1、LinkedList定義:
	public class LinkedList<E>
		extends AbstractSequentialList<E>
		implements List<E>, Deque<E>, Cloneable, java.io.Serializable
LinkedList 是一個繼承於AbstractSequentialList的雙向連結串列。它也可以被當作堆疊、佇列或雙端佇列進行操作。
LinkedList 實現 List 介面,能對它進行佇列操作。
LinkedList 實現 Deque 介面,即能將LinkedList當作雙端佇列使用。
LinkedList 實現了Cloneable介面,即覆蓋了函式clone(),能克隆。
LinkedList 實現java.io.Serializable介面,這意味著LinkedList支援序列化,能通過序列化去傳輸。
LinkedList 是非同步的。 如果多個執行緒同時訪問一個連結列表,而其中至少一個執行緒從結構上修改了該列表,則它必須 保持外部同步。(結構修改指新增或刪除一個或多個元素的任何操作;僅設定元素的值不是結構修改。)這一般通過對自然封裝該列表的物件進行同步操作來完成。如果不存在這樣的物件,則應該使用 Collections.synchronizedList 方法來“包裝”該列表。List list = Collections.synchronizedList(new LinkedList(...));獲取執行緒安全的list。


AbstractSequentialList 實現了get(int index)、set(int index, E element)、add(int index, E element) 和 remove(int index)這些骨幹性函式。降低了List介面的複雜度。這些介面都是隨機訪問List的,LinkedList是雙向連結串列;既然它繼承於AbstractSequentialList,就相當於已經實現了“get(int index)這些介面”。
我們若需要通過AbstractSequentialList自己實現一個列表,只需要擴充套件此類,並提供 listIterator() 和 size() 方法的實現即可。若要實現不可修改的列表,則需要實現列表迭代器的 hasNext、next、hasPrevious、previous 和 index 方法即可。

2、資料結構:
雙向連結串列,存在一種資料結構——我們可以稱之為節點,節點例項儲存業務資料,前一個節點的位置資訊和後一個節點位置資訊。

節點與節點之間相連,構成了我們LinkedList的基本資料結構,也是LinkedList的核心。

        



3、LinkedList的構造方法
    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;
LinkedList包含3個全域性引數,
size存放當前連結串列有多少個節點。
first為指向連結串列的第一個節點的引用。
last為指向連結串列的最後一個節點的引用。

LinkedList構造方法有兩個,一個是無參構造,一個是傳入Collection物件的構造。

無參構造為空實現。有參構造傳入Collection物件,將物件轉為陣列,並按遍歷順序將陣列首尾相連,全域性變數first和last分別指向這個連結串列的第一個和最後一個。

	// 什麼都沒做,是一個空實現
	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的內部私有類,它的組成很簡單,只有一個構造方法。
Node節點 一共有三個屬性:item代表節點值,prev代表節點的前一個節點,next代表節點的後一個節點。
構造方法的引數順序是:前繼節點的引用,資料,後繼節點的引用。
	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;  
		}  
	}
4、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;  
	}  
add方法:
	public boolean add(E e) {
		linkLast(e);
		return true;
	}

	void linkLast(E e) {
		final Node<E> l = last;//連結串列最後一個節點
		final Node<E> newNode = new Node<>(l, e, null);//建立節點物件,前一個(perv屬性)是last,後一個(next屬性)是null,中間item資料是輸入的引數e
		last = newNode;
		if (l == null)
			first = newNode; //如果最後一個節點 為null表示連結串列為空,則新增資料時將新加的資料作為第一個節點
		else
			l.next = newNode; //如果連結串列不為空則將最後一個連結串列的next屬性指向新新增的節點
		size++; //連結串列長度自增1
		modCount++;
	}


	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新增操作時每個新新增的物件都會被放到新建的Node物件中,Node物件中包含加入的物件和前後指標屬性(pred和next,pred和next其實都是Node物件,直接存放的就是當前物件的前一個節點物件和後一個節點物件,最後一個及節點的next值為null),然後將原來最後一個last節點的next指標指向新加入的物件。

get方法:
	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。

addAll操作:
	public boolean addAll(int index, Collection<? extends E> c) {
		checkPositionIndex(index);//判斷index是否越界,越界則丟擲異常
		Object[] a = c.toArray();//轉成陣列
		int numNew = a.length;//要插入的集合的長度
		if (numNew == 0)
			return false;
		Node<E> pred, succ;//宣告pred和succ兩個Node物件,用於標識要插入元素的前一個節點和最後一個節點
		if (index == size) { //如果size等於原陣列長度則表示在結尾新增
			succ = null;
			pred = last;
		} else {
			succ = node(index);//index位置上的Node物件
			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; //如果要插入的位置的前一個節點為null表示是第一個節點,則直接將newNode賦給第一個節點
			else
				pred.next = newNode; //將要插入的集合元素節點物件賦給此位置原節點物件的前一個物件的後一個,即更改前一個節點物件的next指標指到新插入的節點上
			pred = newNode;//更改指向後將新節點物件賦給pred作為下次迴圈中新插入節點的前一個物件節點,依次迴圈
		}
		//此時pred代表集合元素的插入完後的最後一個節點物件
		if (succ == null) { //結尾新增的話在新增完集合元素後將最後一個集合的節點物件pred作為last
			last = pred;
		} else {
			pred.next = succ;//將集合元素的最後一個節點物件的next指標指向原index位置上的Node物件
			succ.prev = pred;//將原index位置上的pred指標物件指向集合的最後一個物件
		}
		size += numNew;
		modCount++;
		return true;
	}
LinkedList在某個位置插入元素是通過將原位置節點的前一個節點的後一個指標指向新插入的元素,然後將原位置的節點的前一個指標指向新元素來實現的,相當於插入元素後後面的元素後移了,但是不是像ArrayList那樣將所有插入位置後面的元素都後移,此處只是改變其前後節點的指向。

5、addAll函式集合引數轉陣列:
在addAll函式中,傳入一個集合引數和插入位置,然後將集合轉化為陣列,然後再遍歷陣列,挨個新增陣列的元素,為什麼要先轉化為陣列再進行遍歷,而不是直接遍歷集合呢?

這樣是為了避免在putAll過程中Collection的內容又發生了改變。除了多執行緒外,還有一種可能是,你傳入的Collection的內容又間接依賴了正在被putAll的list。
1. 如果直接遍歷集合的話,那麼在遍歷過程中需要插入元素,在堆上分配記憶體空間,修改指標域,這個過程中就會一直佔用著這個集合,考慮正確同步的話,其他執行緒只能一直等待。
2. 如果轉化為陣列,只需要遍歷陣列,而遍歷集合過程中不需要額外的操作,所以佔用的時間相對是較短的,這樣就利於其他執行緒儘快的使用這個集合。
說白了,就是有利於提高多執行緒訪問該集合的效率,儘可能短時間的阻塞。

6、總結:
1:LinkedList的實現是基於雙向迴圈連結串列,實現的 List和Deque 介面。實現所有可選的列表操作,並允許所有元素(包括null)。
2:LinkedList是非執行緒安全的,只在單執行緒下適合使用。
3:這個類的iterator和返回的迭代器listIterator方法是fail-fast ,要注意ConcurrentModificationException 。
4:LinkedList實現了Serializable介面,因此它支援序列化,能夠通過序列化傳輸,實現了Cloneable介面,能被克隆。
5:在查詢和刪除某元素時,都分為該元素為null和不為null兩種情況來處理,LinkedList中允許元素為null。
6:由於是基於列表的,LinkedList的沒有擴容方法!預設加入元素是尾部自動擴容!
7:LinkedList還實現了棧和佇列的操作方法,因此也可以作為棧、佇列和雙端佇列來使用,如peek 、push、pop等方法。
8:LinkedList是基於連結串列實現的,因此插入刪除效率高,查詢效率低!(因為查詢需要遍歷整個連結串列)
9:LinkedList和ArrayList一樣實現了List介面,但是它執行插入和刪除操作時比ArrayList更加高效,因為它是基於連結串列的。基於連結串列也決定了它在隨機訪問方面要比ArrayList遜色一點。
10:LinkedList繼承了 AbstractSequentialList抽象類,而不是像 ArrayList和 Vector那樣實現 AbstractList,實際上,java類庫中只有 LinkedList繼承了這個抽象類,正如其名,它提供了對序列的連續訪問的抽象










參考資料:
JDK API LinkedList
LinkedList 原始碼 
java原始碼分析之LinkedList
深入Java集合學習系列:LinkedList的實現原理
http://blog.csdn.net/seu_calvin/article/details/53012654


每天努力一點,每天都在進步。