LinkedList和ArrayList一樣是集合List的實現類,雖然較之ArrayList,其使用場景並不多,但同樣有用到的時候,那麼接下來,我們來認識一下它。
一. 定義一個LinkedList
public static void main(String[] args) {
List<String> stringList = new LinkedList<>();
List<String> tempList = new ArrayList<>();
tempList.add("牛魔王");
tempList.add("蛟魔王");
tempList.add("鵬魔王");
tempList.add("獅駝王");
tempList.add("獼猴王");
tempList.add("禺賊王");
tempList.add("美猴王");
List<String> stringList2 = new LinkedList<>(tempList);
}
上面程式碼中採用了兩種方式來定義LinkedList,可以定義一個空集合,也可以傳遞已有的集合,將其轉化為LinkedList。我們看一下原始碼
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
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.
*/
public LinkedList() {
}
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
}
LinkedList繼承了AbstractSequentialList類,實現了List介面,AbstractSequentialList中已經實現了很多方法,如get(int index)、set(int index, E element)、add(int index, E element) 和 remove(int index),這些方法是我們集合操作時使用最多的,不過這些方法在LinkedList中都已經被重寫了,而抽象方法在LinkedList中有了具體實現。因此我們回到LinkedList類
LinkedList類中定義了三個變數
size:集合的長度
first:雙向連結串列頭部節點
last:雙向連結串列尾部節點
針對first變數和last變數,我們看到是Node類的實體,這是一個靜態內部類,關於靜態內部類的講解,我們在static五大應用場景一章已經有說明
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類來體現的,類中通過item變數儲存了當前節點的值,通過next變數指向下一個節點,通過prev變數指向上一個節點。
二. LinkedList常用方法
1. get(int index)
我們知道隨機讀取元素不是LinkedList所擅長的,讀取效率比起ArrayList也低得多,那麼我來看一下為什麼
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
/**
* 返回一個指定索引的非空節點.
*/
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;
}
}
從上述程式碼中我們可以看到get(int index)方法是通過node(int index)來實現的,它的實現機制是:
比較傳入的索引引數index與集合長度size/2,如果是index小,那麼從第一個順序迴圈,直到找到為止;如果index大,那麼從最後一個倒序迴圈,直到找到為止。也就是說越靠近中間的元素,呼叫get(int index方法遍歷的次數越多,效率也就越低,而且隨著集合的越來越大,get(int index)執行效能也會指數級降低。因此在使用LinkedList的時候,我們不建議使用這種方式讀取資料,可以使用getFirst(),getLast()方法,將直接用到類中的first和last變數。
2. add(E e) 和 add(int index, E element)
大家都在說LinkedList插入、刪除操作效率比較高,以stringList.add(“豬八戒”)為例來看到底發生了什麼?
在LinkedList中我們找到add(E e)方法的原始碼
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* 設定元素e為最後一個元素
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
很好理解:
情況1:假如stringList為空,那麼新增進來的node就是first,也是last,這個node的prev和next都為null;
情況2:假如stringList不為空,那麼新增進來的node就是last,node的prev指向以前的最後一個元素,node的next為null;同時以前的最後一個元素的next.
而如果通過stringList.add(1, “豬八戒”)這種方式將元素新增到集合中呢?
//在指定位置新增一個元素
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
/**
* 在一個非空節點前插入一個元素
*/
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++;
}
其實從程式碼中看到和add(E e)的程式碼實現沒有本質區別,都是通過新建一個Node實體,同時指定其prev和next來實現,不同點在於需要呼叫node(int index)通過傳入的index來定位到要插入的位置,這個也是比較耗時的,參考上面的get(int index)方法。
其實看到這裡,大家也都明白了。
LinkedList插入效率高是相對的,因為它省去了ArrayList插入資料可能的陣列擴容和資料元素移動時所造成的開銷,但資料擴容和資料元素移動卻並不是時時刻刻都在發生的。
3. remove(Object o) 和 remove(int index)
這裡removeFirst()和removeLast()就不多說了,會用到類中定義的first和last變數,非常簡單,我們看一下remove(Object o) 和 remove(int index)原始碼
//刪除某個物件
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;
}
//刪除某個位置的元素
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
//刪除某節點,並將該節點的上一個節點(如果有)和下一個節點(如果有)關聯起來
E unlink(Node<E> x) {
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;
}
其實實現都非常簡單,先找到要刪除的節點,remove(Object o)方法遍歷整個集合,通過 == 或 equals方法進行判斷;remove(int index)通過node(index)方法。
4. LinkedList遍歷
我們主要列舉一下三種常用的遍歷方式,
普通for迴圈,增強for迴圈,Iterator迭代器
public static void main(String[] args) {
LinkedList<Integer> list = getLinkedList();
//通過快速隨機訪問遍歷LinkedList
listByNormalFor(list);
//通過增強for迴圈遍歷LinkedList
listByStrengThenFor(list);
//通過快迭代器遍歷LinkedList
listByIterator(list);
}
/**
* 構建一個LinkedList集合,包含元素50000個
* @return
*/
private static LinkedList<Integer> getLinkedList() {
LinkedList list = new LinkedList();
for (int i = 0; i < 50000; i++){
list.add(i);
}
return list;
}
/**
* 通過快速隨機訪問遍歷LinkedList
*/
private static void listByNormalFor(LinkedList<Integer> list) {
// 記錄開始時間
long start = System.currentTimeMillis();
int size = list.size();
for (int i = 0; i < size; i++) {
list.get(i);
}
// 記錄用時
long interval = System.currentTimeMillis() - start;
System.out.println("listByNormalFor:" + interval + " ms");
}
/**
* 通過增強for迴圈遍歷LinkedList
* @param list
*/
public static void listByStrengThenFor(LinkedList<Integer> list){
// 記錄開始時間
long start = System.currentTimeMillis();
for (Integer i : list) { }
// 記錄用時
long interval = System.currentTimeMillis() - start;
System.out.println("listByStrengThenFor:" + interval + " ms");
}
/**
* 通過快迭代器遍歷LinkedList
*/
private static void listByIterator(LinkedList<Integer> list) {
// 記錄開始時間
long start = System.currentTimeMillis();
for(Iterator iter = list.iterator(); iter.hasNext();) {
iter.next();
}
// 記錄用時
long interval = System.currentTimeMillis() - start;
System.out.println("listByIterator:" + interval + " ms");
}
執行結果如下:
listByNormalFor:1067 ms
listByStrengThenFor:3 ms
listByIterator:2 ms
通過普通for迴圈隨機訪問的方式執行時間遠遠大於迭代器訪問方式,這個我們可以理解,在前面的get(int index)方法中已經有過說明,那麼為什麼增強for迴圈能做到迭代器遍歷差不多的效率?
通過反編譯工具後得到如下程式碼
public static void listByStrengThenFor(LinkedList<Integer> list)
{
long start = System.currentTimeMillis();
Integer localInteger;
for (Iterator localIterator = list.iterator(); localIterator.hasNext();
localInteger = (Integer)localIterator.next()) {}
long interval = System.currentTimeMillis() - start;
System.out.println("listByStrengThenFor:" + interval + " ms");
}
很明顯了,增強for迴圈遍歷時也呼叫了迭代器Iterator,不過多了一個賦值的過程。
還有類似於pollFirst(),pollLast()取值後刪除的方法也能達到部分的遍歷效果。
三. 總結
本文基於java8從定義一個LinkList入手,逐步展開,從原始碼角度分析LinkedList雙向連結串列的結構是如何構建的,同時針對其常用方法進行分析,包括get,add,remove以及常用的遍歷方法,並簡單的說明了它的插入、刪除操作為何相對高效,而取值操作效能相對較低,若有不對之處,請批評指正,望共同進步,謝謝!