Java容器框架(三)--LinkedList實現原理
1. 簡介
如果對Java容器家族成員不太熟悉,可以先閱讀Java容器框架(一)--概述篇這邊文章,LinkedList類在List家族中具有重要的位置,基本上可以和ArrayList平起平坐,在功能上甚至比ArrayList還要強大。下面我們先來看看LinkedList繼承關係:
public abstract class AbstractSequentialList<E> extends AbstractList<E> public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
從類繼承關係可以看到它實現了List介面、Deque介面(是一個雙向佇列),因此具有的特性也就顯而易見。本篇文章從下面方法入手,分析其實現過程從而瞭解LinkedList的實現原理:
下面我們一一分析這些方法的實現原理。
2. LinkedList()&LinkedList(Collection<? extends E> c)
LinkedList為我們提供了兩種建構函式,一個為無參構造,一個傳入一個容器作為入參,下面來看看具體的程式碼實現:
public LinkedList() { } public LinkedList(Collection<? extends E> c) { this(); addAll(c); }
暫時不去分析addAll函式,直接看建構函式發現其實什麼也沒有做,只是另一個容器作為入參的時候,會呼叫addAll函式,不用看原始碼也能猜測到是將容器元素新增到當前LinkList容器中,addAll我們接下來會做分析。
3. add(E e)&add(int index, E element)&addAll(int index, Collection<? extends E> c)
add函式相信我們在熟悉不過,向List中新增元素。
那addAll(int index, Collection<? extends E> c)是什麼意思呢?
它是向List容器中第index+1(由於是從0開始計算的)位置開始將容器c中的元素插入到List容器中,List中index+1開始後面的元素後移。
下面我們看看具體的程式碼實現:
- add(E e)
public boolean add(E e) {
linkLast(e);
return true;
}
transient int size = 0; // 連結串列節點(元素)的個數
transient Node<E> first; // 第一個節點(元素)
transient Node<E> last; // 最後一個節點(元素)
// 連結串列中每一個元素(節點)
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;
}
}
void linkLast(E e) {
final Node<E> l = last; // 用一個臨時變數指向最後一個節點
// 建立一個新的節點,新的節點prev指向當前連結串列的最後一個元素,next為null
final Node<E> newNode = new Node<>(l, e, null);
last = newNode; // 連結串列的last指向新的節點,也就是最後一個節點
if (l == null) // 表明剛開始的連結串列是一個空連結串列,則first 也指向新建立的節點(因為當前連結串列只有一個元素)
first = newNode;
else
l.next = newNode; // 實現雙向連結串列
size++;
modCount++;
}
程式碼中註釋非常清楚,add函式中呼叫的是linkLast函式,也就是向連結串列末尾新增元素。因此我們在向LinkedList中呼叫add來新增元素時,預設是新增到連結串列尾部。
- add(int index, E element)
通過函式名大致能夠猜測到該函式的作用是向LinkedList中某個位置新增元素,那麼具體是怎麼實現的,下面看看原始碼實現:
public void add(int index, E element) {
checkPositionIndex(index); // 檢查index 是否合法
// 如果是最後一個位置,則直接最連結串列尾部新增
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
private void checkPositionIndex(int index) {
// 檢查index 是否合法
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
// 相當於查詢某個位置的節點(元素)
Node<E> node(int index) {
// assert isElementIndex(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;
}
}
// 關鍵是這個函式, 在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++;
}
總結:當向LinkedList的index位置新增元素時,首先判斷index位置是否合法,合法則順序查詢index位置的節點(元素),此處的查詢有一個小小的優化,只需要順序查詢到連結串列一半位置即可,找到該節點後,則利用連結串列的特性直接插入元素,因此在效能上要優於ArrayList,首先LinkedList不需要考慮擴容,其次不需要移動插入位置之後的元素。
- addAll(int index, Collection<? extends E> c)
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);
// 為了通用性,可以新增其他型別的資料結構,因此先把傳入的c轉化為陣列;
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
Node<E> pred, succ;
// 如果是在預設新增
if (index == size) {
succ = null;
pred = last;
} else {
// 找到對應的節點元素
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;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
看程式碼不難理解,由於傳入引數是Collection 型別,因此為了通用,首先轉化為具體的陣列,然後將陣列轉化為Node結構新增到連結串列中,至此將新增元素的相關方法分析完成了。
4. remove()&remove(int index)&remove(Object o)
很明顯,這三個函式都是刪除元素的作用,那它們具體是怎樣實現的呢?其實有了瞭解新增元素原理的基礎,刪除元素也就不難了,下面看看具體原始碼:
- remove()
public E remove() {
// 預設刪除的是連結串列開始的元素
return removeFirst();
}
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;
}
remove()函式,預設從連結串列的頭部開始刪除資料,remove(int index)函式也很容易理解,刪除指定位置的元素,此處就不在分析了,比較好奇的是remove(Object o)這個函式,當連結串列中存在相同的兩個元素,那麼是如何刪除的呢?
- remove(Object o)
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
// 作用是刪除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; // x節點的資料域、next、prev都設定為null,方便垃圾回收
size--;
modCount++;
return element;
}
從程式碼可以看到,刪除某一個元素是從頭部開始查詢,當找到時就刪除對應節點,即便之後還有相同的元素也不會刪除,刪除成功則返回true,否則為false。
5. set(int index, E element)&get(int index)&listIterator(int index)
- set(int index, E element)
set函式是用來更新index節點的值,返回舊值,由於存在需要順序遍歷到第index位置,因此時間複雜度為n/2也即為n,原始碼如下:
public E set(int index, E element) {
checkElementIndex(index); // 檢查index 位置的合法性
Node<E> x = node(index); // 遍歷獲取index位置的節點
E oldVal = x.item;
x.item = element;
return oldVal;
}
- get(int index)
get函式是返回index位置節點的資料,同set很類似,也需要遍歷到index位置,因此時間複雜度為n/2也即為n,原始碼實現如下:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
- listIterator(int index)
這是返回一個LinkedList的迭代器,通常我們不會直接呼叫此函式,一般是直接呼叫List的iterator(),它最終就是呼叫listIterator(int index),只不過index為0而已,通過迭代器對連結串列進行遍歷,相當於C語言裡面的指標一樣,指向某個元素順序遍歷,因此複雜度為n。此處就不在展示對應的原始碼。
我們都知道對List容器進行遍歷通常有兩種方式,一種為for迴圈直接遍歷,一種通過迭代器方式進行遍歷,那麼到底哪種遍歷方式比較好呢?
- for迴圈方式遍歷
int size = list.size() for(int i=0; i<size;i++){ System.out.println(list.get(i)+" "); }
- 迭代器方式遍歷
Iterator iter = list.iterator(); while(iter.hasNext()) { String value = (String)iter.next(); System.out.print(value + " "); }
這兩種方式到底哪種效能更優化,還需要看具體是對哪種List容器進行遍歷,如果是ArrayList,由於get函式時間複雜度為1,因此採用for迴圈遍歷要優於迭代器方式,如果是LinkedList,由於get函式(上面已經分析過)還需要對List進行遍歷找到對應位置,因此採用迭代器方式遍歷效能更好,總之,對於陣列結構的線性表採用for迴圈方式遍歷,對於連結串列結構的線性表採用迭代器方式進行遍歷。
分析到此處,我們還需要注意一個點,大家知道for和for-each的區別嗎?
for迴圈在熟悉不過,沒什麼好說的,但是for-each的實現原理有必要了解下,這裡只是給出原理,需要知道具體實現請自行探索,for-each迴圈其實最終是轉化為迭代器的遍歷方式,我們可以通過對ArrayList遍歷檢視:
List<Person> list = new ArrayList(); for (Person per:list) { System.out.println(per); } 我們看看最後轉化為class檔案的程式碼如下: List<Person> list = new ArrayList(); Iterator var5 = list.iterator(); while(var5.hasNext()) { Person per = (Person)var5.next(); System.out.println(per); }
總結:因此我們在遍歷ArrayList的時候,最好不要使用for-each而是for,對於LinkedList的遍歷,則建議使用for-each或者直接迭代器遍歷。
6. push(E e)&pop()
這兩個函式其實是屬於Deque範疇,在最開始將LinkedList類結構的時候,可以看到LinkedList實現了Deque介面,也即具有雙向連結串列結構。下面看看這兩個函式的具體實現,其他也有許多函式,僅此拋磚引玉,程式碼都很簡單。
public void push(E e) {
// 向連結串列頭部新增元素
addFirst(e);
}
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++;
}
// 以上為push的實現
public E pop() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
總結:以上其實無非都是對連結串列進行操作,只是push和pop都是對頭部節點進行操作,因此類似於棧的功能。
總結
至此LinkedList的原始碼分析就結束了,LinkedList是基於雙向連結串列實現,可以快速插入刪除元素,由於儲存有連結串列頭部和尾部的應用(C/C++ 角度可以理解為指標),因此可以方便實現佇列和棧的功能,同時在遍歷連結串列時,建議使用迭代器來完成,而不是通過for+get(index)這種形式來遍歷。