本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連接 html
![]()
上節咱們介紹了ArrayList,ArrayList隨機訪問效率很高,但插入和刪除性能比較低,咱們提到了一樣實現了List接口的LinkedList,它的特色與ArrayList幾乎正好相反,本節咱們就來詳細介紹LinkedList。java
除了實現了List接口外,LinkedList還實現了Deque和Queue接口,能夠按照隊列、棧和雙端隊列的方式進行操做,本節會介紹這些用法,同時介紹其實現原理。node
咱們先來看它的用法。編程
LinkedList的構造方法與ArrayList相似,有兩個,一個是默認構造方法,另一個能夠接受一個已有的Collection,以下所示:數組
public LinkedList() public LinkedList(Collection<? extends E> c) 複製代碼
好比,能夠這麼建立:微信
List<String> list = new LinkedList<>();
List<String> list2 = new LinkedList<>(
Arrays.asList(new String[]{"a","b","c"}));
複製代碼
LinkedList與ArrayList同樣,一樣實現了List接口,而List接口擴展了Collection接口,Collection又擴展了Iterable接口,全部這些接口的方法都是可使用的,使用方法與上節介紹的同樣,本節就再也不贅述了。數據結構
LinkedList還實現了隊列接口Queue,所謂隊列就相似於平常生活中的各類排隊,特色就是先進先出,在尾部添加元素,從頭部刪除元素,它的接口定義爲:函數
public interface Queue<E> extends Collection<E> {
boolean add(E e);
boolean offer(E e);
E remove();
E poll();
E element();
E peek();
}
複製代碼
Queue擴展了Collection,它的主要操做有三個:性能
每種操做都有兩種形式,有什麼區別呢?區別在於,對於特殊狀況的處理不一樣。特殊狀況是指,隊列爲空或者隊列爲滿,爲空容易理解,爲盡是指隊列有長度大小限制,並且已經佔滿了。LinkedList的實現中,隊列長度沒有限制,但別的Queue的實現可能有。this
在隊列爲空時,element和remove會拋出異常NoSuchElementException,而peek和poll返回特殊值null,在隊列爲滿時,add會拋出異常IllegalStateException,而offer只是返回false。
把LinkedList當作Queue使用也很簡單,好比,能夠這樣:
Queue<String> queue = new LinkedList<>();
queue.offer("a");
queue.offer("b");
queue.offer("c");
while(queue.peek()!=null){
System.out.println(queue.poll());
}
複製代碼
輸出爲:
a
b
c
複製代碼
咱們在介紹函數調用原理的時候介紹過棧,棧也是一種經常使用的數據結構,與隊列相反,它的特色是先進後出、後進先出,相似於一個儲物箱,放的時候是一件件往上放,拿的時候則只能從上面開始拿。
Java中有一個類Stack,用於表示棧,但這個類已通過時了,咱們再也不介紹,Java中沒有單獨的棧接口,棧相關方法包括在了表示雙端隊列的接口Deque中,主要有三個方法:
void push(E e);
E pop();
E peek();
複製代碼
解釋下:
把LinkedList當作棧使用也很簡單,好比,能夠這樣:
Deque<String> stack = new LinkedList<>();
stack.push("a");
stack.push("b");
stack.push("c");
while(stack.peek()!=null){
System.out.println(stack.pop());
}
複製代碼
輸出爲:
c
b
a
複製代碼
棧和隊列都是在兩端進行操做,棧只操做頭部,隊列兩端都操做,但尾部只添加、頭部只查看和刪除,有一個更爲通用的操做兩端的接口Deque,Deque擴展了Queue,包括了棧的操做方法,此外,它還有以下更爲明確的操做兩端的方法:
void addFirst(E e);
void addLast(E e);
E getFirst();
E getLast();
boolean offerFirst(E e);
boolean offerLast(E e);
E peekFirst();
E peekLast();
E pollFirst();
E pollLast();
E removeFirst();
E removeLast();
複製代碼
xxxFirst操做頭部,xxxLast操做尾部。與隊列相似,每種操做有兩種形式,區別也是在隊列爲空或滿時,處理不一樣。爲空時,getXXX/removeXXX會拋出異常,而peekXXX/pollXXX會返回null。隊列滿時,addXXX會拋出異常,offerXXX只是返回false。
棧和隊列只是雙端隊列的特殊狀況,它們的方法均可以使用雙端隊列的方法替代,不過,使用不一樣的名稱和方法,概念上更爲清晰。
Deque接口還有一個迭代器方法,能夠從後往前遍歷
Iterator<E> descendingIterator();
複製代碼
好比,看以下代碼:
Deque<String> deque = new LinkedList<>(
Arrays.asList(new String[]{"a","b","c"}));
Iterator<String> it = deque.descendingIterator();
while(it.hasNext()){
System.out.print(it.next()+" ");
}
複製代碼
輸出爲
c b a
複製代碼
LinkedList的用法是比較簡單的,與ArrayList用法相似,支持List接口,只是,LinkedList增長了一個接口Deque,能夠把它看作隊列、棧、雙端隊列,方便的在兩端進行操做。
若是隻是用做List,那應該用ArrayList仍是LinkedList呢?咱們須要瞭解下LinkedList的實現原理。
咱們知道,ArrayList內部是數組,元素在內存是連續存放的,但LinkedList不是。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;
}
}
複製代碼
Node類表示節點,item指向實際的元素,next指向下一個節點,prev指向前一個節點。
LinkedList內部組成就是以下三個實例變量:
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
複製代碼
咱們暫時忽略transient關鍵字,size表示鏈表長度,默認爲0,first指向頭節點,last指向尾節點,初始值都爲null。
LinkedList的全部public方法內部操做的都是這三個實例變量,具體是怎麼操做的?連接關係是如何維護的?咱們看一些主要的方法,先來看add方法。
add方法的代碼爲:
public boolean add(E e) {
linkLast(e);
return true;
}
複製代碼
主要就是調用了linkLast,它的代碼爲:
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++;
}
複製代碼
代碼的基本步驟是:
Node<E> newNode = new Node<>(l, e, null);
複製代碼
last = newNode;
複製代碼
if (l == null)
first = newNode;
else
l.next = newNode;
複製代碼
size++
複製代碼
modCount++的目的與ArrayList是同樣的,記錄修改次數,便於迭代中間檢測結構性變化。
咱們經過一些圖示來更清楚的看一下,好比說,代碼爲:
List<String> list = new LinkedList<String>();
list.add("a");
list.add("b");
複製代碼
執行完第一行後,內部結構以下所示:
添加完"b"後,內部結構以下所示:
添加了元素,若是根據索引訪問元素呢?咱們看下get方法的代碼:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
複製代碼
checkElementIndex檢查索引位置的有效性,若是無效,拋出異常,代碼爲:
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
複製代碼
若是index有效,則調用node方法查找對應的節點,其item屬性就指向實際元素內容,node方法的代碼爲:
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;
}
}
複製代碼
size>>1
等於size/2
,若是索引位置在前半部分 (index<(size>>1))
,則從頭節點開始查找,不然,從尾節點開始查找。
能夠看出,與ArrayList明顯不一樣,ArrayList中數組元素連續存放,能夠直接隨機訪問,而在LinkedList中,則必須從頭或尾,順着連接查找,效率比較低。
咱們看下indexOf的代碼:
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
複製代碼
代碼也很簡單,從頭節點順着連接日後找,若是要找的是null,則找第一個item爲null的節點,不然使用equals方法進行比較。
add是在尾部添加元素,若是在頭部或中間插入元素呢?可使用以下方法:
public void add(int index, E element) 複製代碼
它的代碼是:
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
複製代碼
若是index爲size,添加到最後面,通常狀況,是插入到index對應節點的前面,調用方法爲linkBefore,它的代碼爲:
void linkBefore(E e, Node<E> succ) {
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++;
}
複製代碼
參數succ表示後繼節點。變量pred就表示前驅節點。目標就是在pred和succ中間插入一個節點。插入步驟是:
Node<E> newNode = new Node<>(pred, e, succ);
複製代碼
succ.prev = newNode;
複製代碼
if (pred == null)
first = newNode;
else
pred.next = newNode;
複製代碼
咱們經過圖示來更清楚的看下,仍是上面的例子,好比,添加一個元素:
list.add(1, "c");
複製代碼
圖示結構會變爲:
咱們再來看刪除元素,代碼爲:
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
複製代碼
經過node方法找到節點後,調用了unlink方法,代碼爲:
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;
}
複製代碼
刪除x節點,基本思路就是讓x的前驅和後繼直接連接起來,next是x的後繼,prev是x的前驅,具體分爲兩步:
咱們再經過圖示看下,仍是上面的例子,若是刪除一個元素:
list.remove(1);
複製代碼
圖示結構會變爲:
以上,咱們介紹了LinkedList的內部組成,以及幾個主要方法的實現代碼,其餘方法的原理也都相似,咱們就不贅述了。
前面咱們提到,對於隊列、棧和雙端隊列接口,長度可能有限制,LinkedList實現了這些接口,不過LinkedList對長度並無限制。
LinkedList內部是用雙向鏈表實現的,維護了長度、頭節點和尾節點,這決定了它有以下特色:
理解了LinkedList和ArrayList的特色,咱們就能比較容易的進行選擇了,若是列表長度未知,添加、刪除操做比較多,尤爲常常從兩端進行操做,而按照索引位置訪問相對比較少,則LinkedList就是比較理想的選擇。
本節詳細介紹了LinkedList,先介紹了用法,而後介紹了實現原理,最後咱們分析了LinkedList的特色,並與ArrayList進行了比較。
用法上,LinkedList是一個List,但也實現了Deque接口,能夠做爲隊列、棧和雙端隊列使用。實現原理上,內部是一個雙向鏈表,並維護了長度、頭節點和尾節點。
不管是ArrayList仍是LinkedList,按內容查找元素的效率都很低,都須要逐個進行比較,有沒有更有效的方式呢?
未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。