上文書說到ArrayList的簡要源碼分析,就不得不提到和它相近的類似的LinkedList,同樣都是列表,讓我們一起來看看有何相同和不同之處。
talk is cheap,show me the code ---undefined
老規矩,先來一段代碼示例
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
public class LinkedListTest {
public static void main(String[] args) {
//無參構造函數new一個linklist
List linklist=new LinkedList();
//linklist的add操作
linklist.add("hello world");
linklist.add("world hello");
System.out.println("當前linklist的容量大小"+linklist.size());
//linklist的getter setter操作
System.out.println("getter"+linklist.get(3));
System.out.println("setter"+linklist.set(3,"huhu"));
System.out.println("當前linklist的容量大小"+linklist.size());
//linklist的addall操作
linklist.addAll(linklist);
System.out.println("當前linklist的容量大小"+linklist.size());
//linklist的index操作
System.out.println("這是索引"+linklist.indexOf("world hello"));
System.out.println("這是最後索引"+linklist.lastIndexOf("world"));
System.out.println("當前linklist的容量大小"+linklist.size());
System.out.println("是否包含有"+linklist.contains("huhu"));
System.out.println("是否包含所有"+linklist.containsAll(linklist));
System.out.println("當前linklist的容量大小"+linklist.size());
//linklist的remove操作
System.out.println("刪除元素"+linklist.remove("world hello"));
System.out.println("刪除元素"+linklist.remove(4));
System.out.println("當前linklist的容量大小"+linklist.size());
//linklist的retain操作
List linklist2=new LinkedList();
linklist2.add("hu hu");
linklist2.add("world hello");
linklist2.add(1);
linklist2.add(100);
linklist2.add(150);
System.out.println("保留指定集合中的內容"+linklist.retainAll(linklist2));
System.out.println("刪除指定集合中的內容"+linklist.removeAll(linklist2));
System.out.println("當前linklist的容量大小"+linklist.size());
LinkedListTest linklistTest=new LinkedListTest();
linklistTest.linklistop(linklist2);
}
public boolean linklistop(List linklist) {
Iterator iterator = linklist.iterator();
while (iterator.hasNext()) {
System.out.println("這是第幾個元素 \t " + iterator.next());
}
return true;
}
}
1. 無參構造函數
public LinkedList() {
}
可以看到,無參構造函數是空的,即什麼也沒有做。
注1:和ArrayList無參構造函數不同,由於採用的底層數據結構不同,ArrayList的底層數據結構爲數組,因此無參構造在初始化時,就需要提前申請一個數組的空間;而LinkedList採用的是雙向鏈表+雙向隊列,鏈表的一個特性就是即用即申請,因此在LinkedList中的無參構造就什麼也沒有做啦。
Q1:LinkedList有哪些構造函數?分別對應哪種情況?
Q2:爲什麼LinkedList的底層數據結構是雙向鏈表?爲什麼不是單向鏈表?這兩種鏈表有何處異同以及好壞處?
Q3:爲什麼LinkedList還實現了Deque(隊列接口)?實現了雙向鏈表+雙向隊列的數據存儲結構?
2. 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);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
add實現了LinkedList的末尾追加,也就是添加元素的功能,其步驟如下
1. add函數調用了linkLast函數
2. linkLast函數以插入節點數據爲形參
2.1 將LinkedList中的last(末尾標誌指針,永遠指向最後一個元素節點)賦值給l節點
2.2 創建新的節點newNode,節點的前驅指向l節點,節點的後驅設置爲空
2.3 將last指向剛剛創建的新節點newNode
2.4 判斷l節點是否是空節點,如果是將first(首部指針,永遠指向第一個元素節點)指向新創建的newNode節點,如果不是,將l的後驅指向newNode
2.5 LinkedList大小加1
2.6 標誌結構性變化的變量加1
3. 返回真值
注2:很顯然,add函數調用了linkLast函數,而linkLast函數採用的是尾插法,也就是說,作爲末尾追加元素時,LinkedList採用了尾插法作爲元素新建和插入。
Q4:爲什麼LinkedList的add是末尾追加元素呢?追加元素爲什麼會採用尾插法,而不是頭插法呢?
Q5:前面提到了LinkedList是雙向鏈表,也就說頭插法和尾插法都可以?那麼什麼情況適合頭插法?什麼情況適合尾插法呢?
3. getter setter函數
getter函數
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(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;
}
}
1. get函數調用了checkElementIndex函數
2. checkElementIndex函數調用了isElementIndex函數
3. isElementIndex則是簡單的對index索引值的判斷,判斷該值是否合法,如果不合法就拋出IndexOutOfBoundsException異常
4. 合法就繼續調用了node函數
5. node函數實現比較有技巧性(重點!請注意一下)
注3:node函數採用了二分法來循環遍歷來查找所需要的index上的元素值
5.1 在if(index<(size>>1))判斷語句中,可以看見了判斷是從size右移了一位除以2,即判斷了index是否小於size的一半,如果是從前往後遍歷,根據後驅慢慢查找,直到處於索引的位置
5.2 如果index大於size的一半,則是從後往前遍歷,根據前驅慢慢查找,直到處於索引的位置
Q6:關於node節點的查找函數爲什麼是二分法進行查找?在二分法中爲什麼又採用了位運算?
Q7:node節點的查找遍歷的時間複雜度如何?
setter函數
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
1. 先校驗索引值是否合法
2. 合法就調用了node函數
3. node函數其實就是在上文getter方法中也出現過的node函數,這個函數實際上就是利用了二分法來查找對應index位置上的node節點
4. 將該位置的節點作爲舊值返回
5. 設置傳入的新值
注4:值得注意的是,LinkedList和ArrayList不同,其索引值不能同數組下標一樣,直接返回(實際上ArrayList是隨機訪問,LinkedList則是順序訪問),因此需要設立循環,根據前驅或者後驅,循環遍歷直到該索引值的節點處,因此,時間複雜度就是O(n),比起ArrayList的O(1)要大一些。
4. addAll函數
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);
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;
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
針對addAll函數所實現的功能大概都有這麼幾個情況
1. 原列表爲空,沒有任何節點,傳入的集合節點,直接添加進去
2. 原列表有節點,在節點末尾追加傳入的集合節點,這是addAll傳入參數collection的情況
3. 原列表有節點,在指定的index位置上追加傳入的集合節點,這是allAll傳入index,collection的情況
可以看見addAll(collection)也是調用了addAll(index,collection),換句話說,addAll主要依靠addAll(index,collection)實現
那麼我們來看下這個函數的實現
1. 首先是調用了checkPositionIndex函數針對index所處位置合法性的校驗,如果不合法就要拋出異常
2. 將集合中元素轉爲數組
3. 對數組大小進行判斷,如果數組是空的,就返回false,否則就下一步
4. 設立兩個節點,一個節點pred指向要添加節點的前一個節點,一個節點succ指向要添加節點的位置
5. 如果要添加index的位置等於大小,就說明了是在末尾處添加(情況3),將要succ指向null,pred指向原列表的最後一個節點;如果不是,則將succ指向要添加的節點的位置(實際上是在添加的節點的前一個節點處),pred指向該節點的前驅節點(情況2)
6. 循環遍歷數組,遍歷一個,就創建一個節點,使用尾插法在要添加的節點處進行鏈接,此處需要對pred是否爲空進行判斷,如果爲空(情況1),就指向新創建的節點,如果不是,則將pred後驅指向新節點
7. 如果succ是null,則將last指針指向添加鏈接完成後的pred節點
8. 如果不是,則將上面的pred的後驅指向succ,然後succ的前驅指向pred
9. 改變大小,modCount++,返回真值
注5:針對批量添加的情況,比較複雜,建議讀者們手動畫圖模擬一下插入的過程
Q8:爲什麼會將集合元素要先轉爲數組,再遍歷創建新節點進行鏈接呢?和直接在集合中遍歷創建節點鏈接有什麼異同?
Q9:設立pred,succ兩個臨時節點指針是否多餘?
5. indexOf、lastindexOf、contains、containsAll函數
關於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;
}
可以看出,步驟都比較簡單
1. 設立index爲0
2. 對object進行判空
3. 如果空值,從first節點開始,按照後驅遍歷查找爲空值的節點,如果找到了,就返回該節點的index
4. 如果非空,也同樣的是從firs節點開始,按照後驅遍歷查找爲空值的節點,如果找到了,就返回該節點的index
5. 都沒找到的,就返回-1
注6:針對空值和非空值的查找,其查找方式都是一樣的,只是在對比值的時候,由於空值的特殊性只能採用==來判斷,而非空值可以直接equals來判斷
關於lastIndexOf函數實現
public int lastIndexOf(Object o) {
int index = size;
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (x.item == null)
return index;
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (o.equals(x.item))
return index;
}
}
return -1;
}
同樣可以看出,跟indexOf的實現方式是一樣的,只不過遍歷是從後驅開始慢慢查找的
再看看contains和containsAll函數實現
public boolean contains(Object o) {
return indexOf(o) != -1;
}
//AbstractCollection中的實現
public boolean containsAll(Collection<?> c) {
for (Object e : c)
if (!contains(e))
return false;
return true;
}
很顯然,contains調用了indexOf的方法,即其實現也是用indexOf實現的,將indexOf返回跟-1進行判斷就可以得出元素是否在列表中的結果了,然後返回結果
注7:需要注意的是,containsAll的實現在LinkedList中並沒有,取而代之則是AbstractCollection中的containsAll實現,而AbstractCollection中的containsAll實現也是循環調用了contains方法
Q10:不知道爲什麼在LinkedList中沒有實現containsAll的方法?
6. remove函數
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) {
// 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;
size--;
modCount++;
return element;
}
remove重載了兩個函數,一個是參數是index,一個參數是object,一個是根據index移除節點,一個是根據object移除節點
根據index索引來移除節點的函數實現中要先對index進行合法性檢查,再調用了unlink函數
根據object移除節點要對object是否爲空值進行判斷,採取不同的判斷方式,遍歷查找到要移除的節點位置,再調用unlink函數
重點來了,我們需要看看unlink是怎麼移除節點的
1. 設立要返回的元素爲該節點的元素值,設立prev爲該節點的前驅節點,next爲該節點的後驅節點
2. 對前驅節點prev進行判空,說明是否是首節點,如果不是首節點,則將前驅的後驅節點指向next的節點,然後把該節點的前驅置空
3. 對後驅節點next進行判空,說明是否是尾節點,如果不是尾節點,則將後驅的前驅節點指向prev節點,然後把該節點的後驅置空
4. 節點上的元素置空,大小減一,mouCount加一,返回刪除節點的元素值
7. retainAll、removeAll函數
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
Iterator<?> it = iterator();
while (it.hasNext()) {
if (c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
Iterator<E> it = iterator();
while (it.hasNext()) {
if (!c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}
可以看出,removeAll和retainAll其實現邏輯基本都是一樣的
1. 調用requireNonNull對集合是否爲空進行判斷
2. 把修改標誌modified設置爲false
3. 取得迭代器iterator
4. 迭代器循環獲取下一個節點元素
5. removeAll是針對集合中存在元素進行判斷,然後調用remove進行刪除;retainAll則是針對集合中非存在的元素進行判斷,不存在就調用remove刪除
6. 返回modified
Q11:爲什麼retainAll、removeAll使用了迭代器進行刪除或者保留?和ArrayList有什麼異同?
8. 其他常用的函數
9. 迭代器