轉載:https://blog.csdn.net/justloveyou_/article/details/52955619
摘要:
List 是 Java Collection Framework的重要成員,具體包括List接口及其所有的實現類。由於List接口繼承了Collection接口,所以List擁有Collection的所有操作。同時,又因爲List是列表類型,所以List本身還提供了一些適合自身的方法。ArrayList 是一個動態數組,實現了數組動態擴容,隨機訪問效率高;LinkedList是一個雙向鏈表,隨機插入和刪除效率高,可用作隊列的實現。
一. 要點
- List 基礎特性與框架
- ArrayList :動態數組
- LinkedList : 雙向鏈表
二. List 基礎特性與框架
1、List 特色操作
List 包括 List接口 以及 List接口的所有實現類(ArrayList, LinkedList, Vector,Stack), 其中 Vector 和 Stack 已過時。因爲 List 接口實現了 Collection 接口(如代碼 1 所示),所以 List 接口擁有 Collection 接口提供的所有常用方法,同時,又因爲 List 是列表類型,所以 List 接口還提供了一些適合於自身的常用方法,如表1所示。
// 代碼 1
public interface List<E> extends Collection<E> { ... }
- 1
- 2
表1. List 特有的方法(以AbstractList爲例說明)
Function | Introduction | Note |
---|---|---|
boolean addAll(int index, Collection<? extends E> c) | 將指定 collection 中的所有元素都插入到列表中的指定位置 | 可選操作 |
E get(int index)| 返回列表中指定位置的元素 | 在 AbstractList 中以抽象方法 abstract public E get(int index); 存在 | AbstractList 中唯一的抽象方法 |
E set(int index, E element) | 用指定元素替換列表中指定位置的元素 | 可選操作,set 操作是 List 的特有操作 |
void add(int index, E element) | 在列表的指定位置插入指定元素 | 可選操作 |
E remove(int index) | 移除列表中指定位置的元素 | 可選操作 |
int indexOf(Object o) | 返回此列表中第一次出現的指定元素的索引;如果此列表不包含該元素,則返回 -1 | 在 AbstractList 中默認實現;在 ArrayList,LinkedList中分別重寫; |
int lastIndexOf(Object o) | 返回此列表中最後出現的指定元素的索引;如果列表不包含此元素,則返回 -1 | 在 AbstractList 中默認實現;在 ArrayList,LinkedList中分別重寫; |
ListIterator<E> listIterator() | 返回此列表元素的列表迭代器(按適當順序) | 在 AbstractList 中默認實現,ArrayList,Vector和Stack使用該默認實現,LinkedList重寫該實現; |
ListIterator<E> listIterator(int index) | 返回列表中元素的列表迭代器(按適當順序),從列表的指定位置開始 | 在 AbstractList 中默認實現,ArrayList,Vector和Stack使用該默認實現,LinkedList重寫該實現; |
List<E> subList(int fromIndex, int toIndex) | 返回列表中指定的 fromIndex(包括 )和 toIndex(不包括)之間的視圖 | 在 AbstractList 中默認實現(ArrayList,LinkedList使用該默認實現);之所以說是視圖,是因爲實際上返回的 list 是靠原來的 list 支持的; |
特別地,對於List而言:
AbstractList 給出了 iterator() 方法的通用實現,其中的 next() 和 remove() 方法底層分別由 get(int index) 和 remove(int index) 方法實現;
對於 subList 方法, 原list 和 子list 做的 非結構性修改(不涉及到list的大小改變的修改),都會影響到彼此; 對於結構性修改: 若發生結構性修改的是返回的子list,那麼原list的大小也會發生變化(modCount與expectedModCount同時變化,不會觸發Fast-Fail機制) ; 而若發生結構性修改的是原list(不包括由返回的子list導致的改變),那麼判斷式 l.modCount != expectedModCount 將返回 TRUE ,觸發 Fast-Fail 機制,拋出 ConcurrentModificationException。因此,若在調用 sublist 返回了子 list 之後,修改原list的大小,那麼之前產生的子list將會失效;
刪除一個list的某個區段: list.subList(from, to).clear();
2、List 結構
下面我們對 List 結構圖中所涉及到的類加以介紹:
AbstractList 是一個抽象類,它實現List接口並繼承於 AbstractCollection 。對於“按順序遍歷訪問元素”的需求,使用List的Iterator 即可以做到,抽象類AbstractList提供該實現;而訪問特定位置的元素(也即按索引訪問)、元素的增加和刪除涉及到了List中各個元素的連接關係,並沒有在AbstractList中提供實現(List 的類型不同,其實現方式也隨之不同,所以將具體實現放到子類);
AbstractSequentialList 是一個抽象類,它繼承於 AbstractList。AbstractSequentialList 通過 ListIterator 實現了“鏈表中,根據index索引值操作鏈表的全部函數”。此外,ArrayList 通過 System.arraycopy(完成元素的挪動) 實現了“順序表中,根據index索引值操作順序表的全部函數”;
ArrayList, LinkedList, Vector, Stack 是 List 的 4 個實現類;
ArrayList 是一個動態數組。它由數組實現,隨機訪問效率高,隨機插入、隨機刪除效率低;
LinkedList 是一個雙向鏈表(順序表)。LinkedList 隨機訪問效率低,但隨機插入、隨機刪除效率高,。可以被當作堆棧、隊列或雙端隊列進行操作;
Vector 是矢量隊列,和ArrayList一樣,它也是一個動態數組,由數組實現。但ArrayList是非線程安全的,而Vector是線程安全的;
Stack 是棧,它繼承於Vector。它的特性是:先進後出(FILO, First In Last Out).
3、List 特性:
Java 中的 List 是對數組的有效擴展,它是這樣一種結構:如果不使用泛型,它可以容納任何類型的元素,如果使用泛型,那麼它只能容納泛型指定的類型的元素。和數組(數組不支持泛型)相比,List 的容量是可以動態擴展的;
List 中的元素是“有序”的。這裏的“有序”,並不是排序的意思,而是說我們可以對某個元素在集合中的位置進行指定,包括對列表中每個元素的插入位置進行精確地控制、根據元素的整數索引(在列表中的位置)訪問元素和搜索列表中的元素;
List 中的元素是可以重複的,因爲其爲有序的數據結構;
List中常用的集合對象包括:ArrayList、Vector和LinkedList,其中前兩者是基於數組來進行存儲,後者是基於鏈表進行存儲。其中Vector是線程安全的,其餘兩個不是線程安全的;
List中是可以包括 null 的,即使使用了泛型;
List 接口提供了特殊的迭代器,稱爲 ListIterator,除了允許 Iterator 接口提供的正常操作外,該迭代器還允許元素插入和替換,以及雙向訪問。
三. ArrayList
1、ArrayList 基礎
ArrayList 實現了 List 中所有可選操作,並允許包括 NULL 在內的所有元素。除了實現 List 接口外,此類還提供一些方法來操作其支撐數組的大小。ArrayList 是基於數組實現的,是一個動態數組,其容量能自動增長,並且用 size 屬性來標識該容器裏的元素個數,而非這個被包裝數組的大小。每個 ArrayList 實例都有一個容量,該容量是指用來存儲列表元素的數組的大小,並且它總是至少等於列表的大小。隨着向 ArrayList 中不斷添加元素,其容量也自動增長。自動增長會帶來數據向新數組的重新拷貝。因此,如果可預知數據量的多少,可在構造ArrayList時指定其容量。在添加大量元素前,應用程序也可以使用 ensureCapacity 操作來增加 ArrayList 實例的容量,這可以減少遞增式再分配的數量。注意,此實現不是同步的。如果多個線程同時訪問一個 ArrayList 實例,而其中至少一個線程從結構上修改(結構上的修改是指任何添加或刪除一個或多個元素的操作,或者顯式調整底層數組的大小;僅僅設置元素的值不是結構上的修改.)了列表,那麼它必須保持外部同步。
ArrayList 實現了 Serializable 接口,因此它支持序列化,能夠通過序列化傳輸。閱讀源碼可以發現,ArrayList內置的數組用 transient 關鍵字修飾,以示其不會被序列化。當然,ArrayList的元素最終還是會被序列化的,在序列化/反序列化時,會調用 ArrayList 的 writeObject()/readObject() 方法,將該 ArrayList中的元素(即0…size-1下標對應的元素) 和 容量大小 寫入流/從流讀出。這樣做的好處是,只保存/傳輸有實際意義的元素,最大限度的節約了存儲、傳輸和處理的開銷。
ArrayList 實現了 RandomAccess 接口, 支持快速隨機訪問,實際上就是通過下標序號進行快速訪問(於是否支持get(index)訪問不同)。RandomAccess 接口是 List 實現所使用的標記接口,用來表明其支持快速(通常是固定時間)隨機訪問。此接口的主要目的是允許一般的算法更改其行爲,從而在將其應用到隨機或連續訪問列表時能提供良好的性能。特別地,在對List的遍歷算法中,要儘量來判斷是屬於 RandomAccess(如ArrayList) 還是 SequenceAccess(如LinkedList),因爲適合RandomAccess List的遍歷算法,用在SequenceAccess List上就差別很大,即對於實現了RandomAccess接口的類實例而言,此循環
for (int i=0, i<list.size(); i++)
list.get(i);
- 1
- 2
的運行速度要快於以下循環:
for (Iterator i=list.iterator(); i.hasNext(); )
i.next();
- 1
- 2
所以,我們在遍歷List之前,可以用 if( list instanceof RamdomAccess ) 來判斷一下,選擇用哪種遍歷方式。
ArrayList 實現了Cloneable接口,能被克隆。Cloneable 接口裏面沒有任何方法,只是起一個標記作用,表明當一個類實現了該接口時,該類可以通過調用clone()方法來克隆該類的實例。
ArrayList不是線程安全的,只能用在單線程環境下,多線程環境下可以考慮用 Collections.synchronizedList(List l) 函數返回一個線程安全的ArrayList類,也可以使用 concurrent 併發包下的 CopyOnWriteArrayList 類。
2、ArrayList在JDK中的定義
我們可以從 ArrayList 的源碼看到,其包括 兩個域 和 三個構造函數 , 源碼如下:
- private transient Object[] elementData : 支撐數組
- private int size : 元素個數
- public ArrayList(int initialCapacity){ … } : 給定初始容量的構造函數;
public ArrayList() { … } : Java Collection Framework 規範-默認無參的構造函數 (初始容量指定爲10);
public ArrayList(Collectionc<? extends E>) { … } : Java Collection Framework 規範-參數爲指定容器的構造函數
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
/**
* The array buffer into which the elements of the ArrayList are stored.(ArrayList 支撐數組)
* The capacity of the ArrayList is the length of this array buffer.(ArrayList 容量的定義)
*/
private transient Object[] elementData; // 瞬時域
/**
* The size of the ArrayList (the number of elements it contains).(ArrayList 大小的定義)
*/
private int size;
/**
* Constructs an empty list with the specified initial capacity
*/
public ArrayList(int initialCapacity) { // 給定初始容量的構造函數
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity]; // 泛型與數組不兼容
}
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() { // Java Collection Framework 規範:默認無參的構造函數
this(10);
}
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*/
public ArrayList(Collection<? extends E> c) { // Java Collection Framework 規範:參數爲指定容器的構造函數
elementData = c.toArray();
size = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
3、ArrayList 基本操作的保證
- 邊界檢查(即檢查 ArrayList 的 Size):涉及到 index 的操作
// 邊界檢查
private void RangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: "+size);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 調整數組容量(增加容量):向 ArrayList 中增加元素
// 調整數組容量
public void ensureCapacity(int minCapacity) {
modCount++;
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
int newCapacity = (oldCapacity * 3)/2 + 1;
if (newCapacity < minCapacity)
newCapacity = minCapacity;
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
向 ArrayList 中增加元素時,都要去檢查添加後元素的個數是否會超出當前數組的長度。如果超出,ArrayList 將會進行擴容,以滿足添加數據的需求。數組擴容通過一個 public 方法 ensureCapacity(int minCapacity) 來實現 : 在實際添加大量元素前,我也可以使用 ensureCapacity 來手動增加 ArrayList 實例的容量,以減少遞增式再分配的數量。
ArrayList 進行擴容時,會將老數組中的元素重新拷貝一份到新的數組中,每次數組容量的增長爲其原容量的 1.5 倍 + 1。這種操作的代價是很高的,因此在實際使用時,我們應該儘量避免數組容量的擴張。當我們可預知要保存的元素的多少時,要在構造ArrayList實例時,就指定其容量,以避免數組擴容的發生。或者根據實際需求,通過調用 ensureCapacity 方法來手動增加ArrayList實例的容量。
在 ensureCapacity 的源代碼中有這樣一段代碼:
Object oldData[] = elementData; //爲什麼要用到oldData[]
- 1
乍一看,後面並沒有用到oldData,這句話顯得多此一舉!但是這牽涉到了一個內存管理的問題. 而且,爲什麼這一句還在 if 的內部? 這是跟 elementData = Arrays.copyOf(elementData, newCapacity); 這句是有關係的,在下面這句 Arrays.copyOf
的實現時,新創建了 newCapacity 大小的內存,然後把老的 elementData 放入。這樣,由於舊的內存的引用是elementData, 而 elementData 指向了新的內存塊,如果有一個局部變量 oldData 變量引用舊的內存塊的話,在copy的過程中就會比較安全,因爲這樣證明這塊老的內存依然有引用,分配內存的時候就不會被侵佔掉。然後,在copy完成後,這個局部變量的生命週期也過去了,此時釋放纔是安全的。否則的話,在 copy 的時候萬一新的內存或其他線程的分配內存侵佔了這塊老的內存,而 copy 還沒有結束,這將是個嚴重的事情。
調整數組容量(減少容量):將底層數組的容量調整爲當前列表保存的實際元素的大小
在使用 ArrayList 過程中,由於 elementData 的長度會被拓展,所以經常會出現 size 很小但 elementData.length 很大的情況,造成空間的浪費。 ArrayList 通過 trimToSize 方法返回一個新的數組給 elementData ,其中:元素內容保持不變,length 和size 相同。
public void trimToSize() {
modCount++;
int oldCapacity = elementData.length;
if (size < oldCapacity) {
elementData = Arrays.copyOf(elementData, size);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
Fail-Fast機制
動機: 在 Java Collection 中,爲了防止在某個線程在對 Collection 進行迭代時,其他線程對該 Collection 進行結構上的修改。換句話說,迭代器的快速失敗行爲僅用於檢測代碼的 bug。
本質: Fail-Fast 是 Java 集合的一種錯誤檢測機制。
作用場景: 在使用迭代器時,Collection 的結構發生變化,拋出 ConcurrentModificationException 。當然,這並不能說明 Collection對象 已經被不同線程併發修改,因爲如果單線程違反了規則,同樣也有會拋出該異常。
當多個線程對集合進行結構上的改變的操作時,有可能會產生fail-fast機制。例如:假設存在兩個線程(線程1、線程2),線程1通過Iterator在遍歷集合A中的元素,在某個時候線程2修改了集合A的結構(是結構上面的修改,而不是簡單的修改集合元素的內容),那麼這個時候程序就會觸發fail-fast機制,拋出 ConcurrentModificationException 異常。在面對併發的修改時,迭代器很快就會完全失敗,而不是冒着在將來某個不確定時間發生任意不確定行爲的風險。
我們知道 fail-fast 產生的原因就在於:程序在對 collection 進行迭代時,某個線程對該 collection 在結構上對其做了修改。要想進一步瞭解 fail-fast 機制,我們首先要對 ConcurrentModificationException 異常有所瞭解。當方法檢測到對象的併發修改,但不允許這種修改時就拋出該異常。同時需要注意的是,該異常不會始終指出對象已經由不同線程併發修改,如果單線程違反了規則,同樣也有可能會拋出改異常。誠然,迭代器的快速失敗行爲無法得到保證,它不能保證一定會出現該錯誤,但是快速失敗操作會盡最大努力拋出 ConcurrentModificationException 異常,所以,爲提高此類操作的正確性而編寫一個依賴於此異常的程序是錯誤的做法,正確做法是:ConcurrentModificationException 應該僅用於檢測 bug。
下面我們以 ArrayList 爲例進一步分析 fail-fast 產生的原因:
private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
int expectedModCount = ArrayList.this.modCount;
public boolean hasNext() {
return (this.cursor != ArrayList.this.size);
}
public E next() {
checkForComodification();
/** 省略此處代碼 */
}
public void remove() {
if (this.lastRet < 0)
throw new IllegalStateException();
checkForComodification();
/** 省略此處代碼 */
}
final void checkForComodification() {
if (ArrayList.this.modCount == this.expectedModCount)
return;
throw new ConcurrentModificationException();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
從上面的源代碼我們可以看出,迭代器在調用 next() 、 remove() 方法時都是調用 checkForComodification() 方法,該方法用於判斷 “modCount == expectedModCount”:若不等,觸發 fail-fast 機制,拋出 ConcurrentModificationException 異常。所以,要弄清楚爲什麼會產生 fail-fast 機制,我們就必須要弄明白 “modCount != expectedModCount” 什麼時候發生,換句話說,他們的值在什麼時候發生改變的。
expectedModCount 是在 Itr 中定義的:“int expectedModCount = ArrayList.this.modCount;”,所以它的值是不可能會修改的,所以會變的就是 modCount。modCount 是在 AbstractList 中定義的,爲全局變量:
protected transient int modCount = 0;
- 1
從 ArrayList 源碼中我們可以看出,我們直接或間接的通過 RemoveRange 、 trimToSize 和 ensureCapcity(add,remove,clear) 三個方法完成對 ArrayList 結構上的修改,所以 ArrayList 實例每當調用一次上面的方法,modCount 的值就遞增一次。所以,我們這裏可以判斷:由於expectedModCount 的值與 modCount 的改變不同步,導致兩者之間不等,從而觸發fail-fast機制。我們可以考慮如下場景:
有兩個線程(線程A,線程B),其中線程A負責遍歷list、線程B修改list。線程A在遍歷list過程的某個時候(此時expectedModCount = modCount=N),線程啓動,同時線程B增加一個元素,這是modCount的值發生改變(modCount + 1 = N + 1)。線程A繼續遍歷執行next方法時,通告checkForComodification方法發現expectedModCount = N ,而modCount = N + 1,兩者不等,這時觸發 fail-fast機制。
3、元素的增、改、刪、查
元素的增和改
ArrayList 提供了 set(int index, E element) 用於修改元素,提供 add(E e)、add(int index, E element)、addAll(Collection<? extends E> c)、addAll(int index, Collection<? extends E> c) 這些方法用於添加元素。
// 用指定的元素替代此列表中指定位置上的元素,並返回以前位於該位置上的元素
public E set(int index, E element) {
RangeCheck(index);
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
// 將指定的元素添加到此列表的尾部
public boolean add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
return true;
}
// 將指定的元素插入此列表中的指定位置。
// 如果當前位置有元素,則向右移動當前位於該位置的元素以及所有後續元素(將其索引加1)。
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);
// 如果數組長度不足,將進行擴容。
ensureCapacity(size+1); // Increments modCount!!
// 將 elementData中從Index位置開始、長度爲size-index的元素,
// 拷貝到從下標爲index+1位置開始的新的elementData數組中。
// 即將當前位於該位置的元素以及所有後續元素右移一個位置。
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
// 按照指定collection的迭代器所返回的元素順序,將該collection中的所有元素添加到此列表的尾部。
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacity(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
// 從指定的位置開始,將指定collection中的所有元素插入到此列表中。
public boolean addAll(int index, Collection<? extends E> c) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: " + index + ", Size: " + size);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacity(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 元素的讀取
// 返回此列表中指定位置上的元素。
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
- 1
- 2
- 3
- 4
- 5
元素的刪除
ArrayList 共有 根據下標或者指定對象兩種方式的刪除功能。
romove(int index): 首先是檢查範圍,修改modCount,保留將要被移除的元素,將移除位置之後的元素向前挪動一個位置,將list末尾元素置空(null),返回被移除的元素。
// 移除此列表中指定位置上的元素
public E remove(int index) {
RangeCheck(index);
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // Let gc do its work
return oldValue;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
remove(Object o):
// 移除此列表中 “首次” 出現的指定元素(如果存在)。這是因爲 ArrayList 中允許存放重複的元素。
public boolean remove(Object o) {
// 由於ArrayList中允許存放null,因此下面通過兩種情況來分別處理。
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
// 類似remove(int index),移除列表中指定位置上的元素。
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
首先通過代碼可以看到,當移除成功後返回true,否則返回false。remove(Object o)中通過遍歷element尋找是否存在傳入對象,一旦找到就調用 fastRemove 移除對象。
爲什麼找到了元素就知道了index,不通過remove(index)來移除元素呢?因爲fastRemove跳過了判斷邊界的處理,因爲找到元素就相當於確定了index不會超過邊界,而且fastRemove並不返回被移除的元素。下面是fastRemove的代碼,基本和remove(index)一致。下面是 fastRemove(私有的) 的源碼:
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // Let gc do its work
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
4、小結
關於ArrayList的源碼,總結如下:
- 三個不同的構造方法。無參構造方法構造的ArrayList的容量默認爲10; 帶有Collection參數的構造方法的實現是將Collection轉化爲數組賦給ArrayList的實現數組elementData。
- 擴充容量的方法ensureCapacity。ArrayList在每次增加元素(可能是1個,也可能是一組)時,都要調用該方法來確保足夠的容量。當容量不足以容納當前的元素個數時,就設置新的容量爲舊的容量的1.5倍加1,如果設置後的新容量還不夠,則直接新容量設置爲傳入的參數(也就是所需的容量)。之後,用 Arrays.copyof() 方法將元素拷貝到新的數組。從中可以看出,當容量不夠時,每次增加元素,都要將原來的元素拷貝到一個新的數組中,非常之耗時,也因此建議在事先能確定元素數量的情況下,才使用ArrayList,否則建議使用LinkedList。
- ArrayList 的實現中大量地調用了Arrays.copyof() 和 System.arraycopy()方法 。我們有必要對這兩個方法的實現做下深入的瞭解。首先來看Arrays.copyof()方法。它有很多個重載的方法,但實現思路都是一樣的,我們來看泛型版本的源碼:
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
- 1
- 2
- 3
很明顯調用了另一個 copyof 方法,該方法有三個參數,最後一個參數指明要轉換的數據的類型,其源碼如下:
/**
* @param original 源數組
* @param newLength 目標數組的長度
* @param newType 目標數組的類型
*/
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
這裏可以很明顯地看出,該方法實際上是在其內部又創建了一個長度爲newlength的數組,調用System.arraycopy()方法,將原來數組中的元素複製到了新的數組中。
下面來看 System.arraycopy() 方法。該方法被標記了native,調用了系統的C/C++代碼,在JDK中是看不到的,但在openJDK中可以看到其源碼。該函數實際上最終調用了C語言的memmove()函數,因此它可以保證同一個數組內元素的正確複製和移動,比一般的複製方法的實現效率要高很多,很適合用來批量處理數組。Java強烈推薦在複製大量數組元素時用該方法,以取得更高的效率;
- ArrayList 基於數組實現,可以通過下標索引直接查找到指定位置的元素,因此 查找效率高,但每次插入或刪除元素,就要大量地移動元素,插入刪除元素的效率低;
- 在查找給定元素索引值等的方法中,源碼都將該元素的值分爲null和不爲null兩種情況處理,ArrayList中允許元素爲null。
四. LinkedList
1、LinkedList 簡介
LinkedList 是 List接口的雙向鏈表實現。LinkedList 實現了 List 中所有可選操作,並且允許所有元素(包括 null)。除了實現 List 接口外,LinkedList 爲在列表的開頭及結尾進行獲取(get)、刪除(remove)和插入(insert)元素提供了統一的訪問操作,而這些操作允許LinkedList 作爲Stack(棧)、Queue(隊列)或Deque(雙端隊列:double-ended queue)進行使用。
注意,LinkedList 不是同步的。如果多個線程同時訪問一個順序表,而其中至少一個線程從結構上(結構修改指添加或刪除一個或多個元素的任何操作;僅設置元素的值不是結構修改。)修改了該列表,則它必須保持外部同步。這一般通過對自然封裝該列表的對象進行同步操作來完成。如果不存在這樣的對象,則應該使用 Collections.synchronizedList 方法來“包裝”該列表。最好在創建時完成這一操作,以防止對列表進行意外的不同步訪問,如下所示:
List list = Collections.synchronizedList(new LinkedList(...));
- 1
LinkedList 的 iterator 和 listIterator 方法返回的迭代器是快速失敗的:在迭代器創建之後,如果從結構上對列表進行修改,除非通過迭代器自身的 remove 或 add 方法,其他任何時間任何方式的修改,都將導致ConcurrentModificationException。因此,面對併發的修改,迭代器很快就會完全失敗,而不冒將來不確定的時間任意發生不確定行爲的風險。
LinkedList 在Java中的定義如下:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
- 1
- 2
- 3
LinkedList 是一個繼承於AbstractSequentialList的雙向鏈表。它也可以被當作堆棧、隊列或雙端隊列進行操作;
LinkedList 實現 List 接口,具有 List 的所有功能;
LinkedList 實現 Deque 接口,即能將LinkedList當作雙端隊列使用;
LinkedList 實現了Cloneable接口,即覆蓋了函數clone(),能克隆;
LinkedList 實現java.io.Serializable接口,這意味着LinkedList支持序列化,能通過序列化去傳輸;
與 ArrayList 不同,LinkedList 沒有實現 RandomAccess 接口,不支持快速隨機訪問。
2、LinkedList 數據結構原理
LinkedList底層的數據結構是基於雙向鏈表的,且頭結點中不存放數據, 如下:
既然是雙向鏈表,那麼必定存在一種數據結構——我們可以稱之爲節點,節點實例保存業務數據,前一個節點的位置信息和後一個節點位置信息,如下圖所示:
3、LinkedList 在JDK中的定義
A.成員變量
- private transient Entry header = new Entry(null, null, null);
private transient int size = 0;
其中,header是雙向鏈表的頭節點,它是Entry的實例。Entry 包含三個成員變量: previous, next, element。其中,previous是該節點的上一個節點,next是該節點的下一個節點,element是該節點所包含的值。 size 是雙向鏈表中節點實例的個數。首先,我們來了解節點類Entry:
private static class Entry<E> {
E element;
Entry<E> next;
Entry<E> previous;
Entry(E element, Entry<E> next, Entry<E> previous) {
this.element = element;
this.next = next;
this.previous = previous;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
節點類 Entry 很簡單,element存放業務數據,previous與next分別是指向前後節點的指針。
B.構造函數
- public LinkedList() { … } : Java Collection Framework 規範:空鏈表
- public LinkedList(Collection<? extends E> c) { … } : Java Collection Framework 規範:參數爲指定容器的構造函數
LinkedList提供了兩個構造方法。第一個構造方法不接受參數,將header實例的previous和next全部指向header實例(注意,這個是一個雙向鏈表,如果不是循環鏈表,空鏈表的情況應該是header節點的前一節點和後一節點均爲null),這樣整個鏈表其實就只有header一個節點,用於表示一個空的鏈表。執行完構造函數後,header實例自身形成一個閉環,如下圖所示:
第二個構造方法接收一個Collection參數c,調用第一個構造方法構造一個空的鏈表,之後通過addAll將c中的元素全部添加到鏈表中,代碼如下:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
private transient Entry<E> header = new Entry<E>(null, null, null);
private transient int size = 0;
/**
* Constructs an empty list.
*/
public LinkedList() {
header.next = header.previous = header;
}
/**
* 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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
C.成員方法
- 添加數據:add()
// 將元素(E)添加到LinkedList中
public boolean add(E e) {
// 將節點(節點數據是e)添加到表頭(header)之前。
// 即,將節點添加到雙向鏈表的末端。
addBefore(e, header);
return true;
}
public void add(int index, E element) {
addBefore(element, (index==size ? header : entry(index)));
}
private Entry<E> addBefore(E e, Entry<E> entry) {
Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
newEntry.previous.next = newEntry;
newEntry.next.previous = newEntry;
size++;
modCount++;
return newEntry;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
addBefore(E e,Entry entry)方法是個私有方法,其先通過Entry的構造方法創建e的節點newEntry(包含了將其下一個節點設置爲entry,上一個節點設置爲entry.previous的操作,相當於修改newEntry的“指針”),之後修改插入位置後newEntry的前一節點的next引用和後一節點的previous引用,使鏈表節點間的引用關係保持正確。之後修改和size大小和記錄modCount,然後返回新插入的節點。
- 刪除數據remove()
幾個remove方法最終都是調用了一個私有方法:remove(Entry e),只是其他簡單邏輯上的區別。下面分析remove(Entry e)方法。
private E remove(Entry<E> e) {
if (e == header)
throw new NoSuchElementException();
// 保留將被移除的節點e的內容
E result = e.element;
// 將前一節點的next引用賦值爲e的下一節點
e.previous.next = e.next;
// 將e的下一節點的previous賦值爲e的上一節點
e.next.previous = e.previous;
// 上面兩條語句的執行已經導致了無法在鏈表中訪問到e節點,而下面解除了e節點對前後節點的引用
e.next = e.previous = null;
// 將被移除的節點的內容設爲null
e.element = null;
// 修改size大小
size--;
modCount++;
// 返回移除節點e的內容
return result;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
整個刪除操作分爲三步:
調整相應節點的前後指針信息
e.previous.next = e.next;//預刪除節點的前一節點的後指針指向預刪除節點的後一個節點。
e.next.previous = e.previous;//預刪除節點的後一節點的前指針指向預刪除節點的前一個節點。
清空預刪除節點
e.next = e.previous = null;
e.element = null;
gc完成資源回收,刪除操作結束。
由以上 add 和 remove 源碼可知,與 ArrayList 比較而言,LinkedList的添加、刪除動作不需要移動很多數據,從而效率更高。
- 數據獲取get()
public E get(int index) {
try {
return listIterator(index).next();
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
該方法涉及到 listIterator 的構造,我們再看listIterator 的源碼。
- listIterator 源碼
private class ListItr implements ListIterator<E> {
// 最近一次返回的節點,也是當前持有的節點
private Entry<E> lastReturned = header;
// 對下一個元素的引用
private Entry<E> next;
// 下一個節點的index
private int nextIndex;
private int expectedModCount = modCount;
// 構造方法,接收一個index參數,返回一個ListItr對象
ListItr(int index) {
// 如果index小於0或大於size,拋出IndexOutOfBoundsException異常
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+size);
// 判斷遍歷方向
if (index < (size >> 1)) { // 相當於除法,但比除法效率高
// next賦值爲第一個節點
next = header.next;
// 獲取指定位置的節點
for (nextIndex=0; nextIndex<index; nextIndex++)
next = next.next;
} else {
// else中的處理和if塊中的處理一致,只是遍歷方向不同
next = header;
for (nextIndex=size; nextIndex>index; nextIndex--)
next = next.previous;
}
}
// 根據nextIndex是否等於size判斷時候還有下一個節點(也可以理解爲是否遍歷完了LinkedList)
public boolean hasNext() {
return nextIndex != size;
}
// 獲取下一個元素
public E next() {
checkForComodification();
// 如果nextIndex==size,則已經遍歷完鏈表,即沒有下一個節點了(實際上是有的,因爲是循環鏈表,任何一個節點都會有上一個和下一個節點,這裏的沒有下一個節點只是說所有節點都已經遍歷完了)
if (nextIndex == size)
throw new NoSuchElementException();
// 設置最近一次返回的節點爲next節點
lastReturned = next;
// 將next“向後移動一位”
next = next.next;
// index計數加1
nextIndex++;
// 返回lastReturned的元素
return lastReturned.element;
}
public boolean hasPrevious() {
return nextIndex != 0;
}
// 返回上一個節點,和next()方法相似
public E previous() {
if (nextIndex == 0)
throw new NoSuchElementException();
lastReturned = next = next.previous;
nextIndex--;
checkForComodification();
return lastReturned.element;
}
public int nextIndex() {
return nextIndex;
}
public int previousIndex() {
return nextIndex-1;
}
// 移除當前Iterator持有的節點
public void remove() {
checkForComodification();
Entry<E> lastNext = lastReturned.next;
try {
LinkedList.this.remove(lastReturned);
} catch (NoSuchElementException e) {
throw new IllegalStateException();
}
if (next==lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = header;
expectedModCount++;
}
// 修改當前節點的內容
public void set(E e) {
if (lastReturned == header)
throw new IllegalStateException();
checkForComodification();
lastReturned.element = e;
}
// 在當前持有節點後面插入新節點
public void add(E e) {
checkForComodification();
// 將最近一次返回節點修改爲header
lastReturned = header;
addBefore(e, next);
nextIndex++;
expectedModCount++;
}
// 判斷expectedModCount和modCount是否一致,以確保通過ListItr的修改操作正確的反映在LinkedList中
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
由以上 get 和 listIterator 源碼可知,與 ArrayList 比較而言,LinkedList的隨機訪問需要從頭遍歷到指定位置元素,而不像ArrayList直接通過索引取值,效率更低一些。
4、小結
關於LinkedList的源碼,給出幾點比較重要的總結:
LinkedList 本質是一個雙向鏈表,可用作List,Queue,Stack和Deque;
LinkedList 並未實現 RandomAccess 接口;
相對於ArrayList,LinkedList有更好的增刪效率,更差的隨機訪問效率;
引用
Java集合—ArrayList的實現原理
Java中ArrayList類詳解
java中ArrayList類動態改變數組長度
Java中的Map List Set等集合類
基於Java回顧之集合的總結概述
Java提高篇(三四)—–fail-fast機制
容器關係的梳理(上)——Collection
Java集合—LinkedList源碼解析
<link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/markdown_views-ea0013b516.css">
</div>