List 集合源碼剖析
✅ ArrayList
底層是基於數組,(數組在內存中分配連續的內存空間)是對數組的升級,長度是動態的。
數組默認長度是10,當添加數據超越當前數組長度時,就會進行擴容,擴容長度是之前的1.5倍,要對之前的數組對象進行復制,所以只有每次擴容時相對性能開銷大一些。
源碼(jdk 1.8):
1. 添加元素(非指定位置)
// 1. 添加元素
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 每次添加元素都要對容量評估
elementData[size++] = e;
return true;
}
// 2. 評估容量
private void ensureCapacityInternal(int minCapacity) {
// 若果數組對象還是默認的數組對象
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
// 3. 進一步評估是否要進行擴容
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 記錄ArrayList結構性變化的次數
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
步驟3中
if (minCapacity - elementData.length > 0) grow(minCapacity);
當 minCapacity
大於當前數組對象長度時 才進行擴容操作,也就是執行步驟 4的代碼
// 4.復制產生新的數組對象並擴容
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
2. 指定位置添加元素
public void add(int index, E element) {
// 1
rangeCheckForAdd(index);
// 2
ensureCapacityInternal(size + 1);
// 3
System.arraycopy(elementData, index, elementData, index + 1,size - index);
// 4
elementData[index] = element;
// 5
size++;
}
-
rangeCheckForAdd(index);
評估插入元素位置是否合理 -
ensureCapacityInternal(size + 1);
檢查數組容量是否有當前元素數量 size +1 個大,因為后續進行數組復制要多出一個元素 -
數組復制
System.arraycopy(elementData, index, elementData, index + 1,size - index);
System.arraycopy(src, srcPos, dest, destPos , length);
src:源數組;srcPos:源數組要復制的起始位置;index 是要插入元素的位置,所以要從當前開始復制
dest:目的數組; destPos:目的數組放置的起始位置;復制好的元素要放在插入位置的后面 所以 index+1
length:復制的長度。包括插入位置和后面的元素 = 當前元素數 - 插入位置
-
步驟執行元素賦值
-
步驟元素長度+1
如果ArrayLisy集合不指定位置添加元素,默認往數組對象的后面追加,所以數組對象的其他元素位置不變,沒有什么性能開銷,如果元素插入到數組對象的前面,而且是越往前,重新排序的數組元素就會越多性能開銷越大,當然通過上述源碼介紹中看到,通過數組復制的方式排序對性能影響也不大,
3.查找元素
// 獲取指定位置元素的源碼
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
直接返回當前查找位置的數組對象對應的下標位置的元素,高性能,速度很快。
4. ArraList 和 Vector
ArraList 和 Vector 都是基於數組實現,它倆底層實現原理幾乎相同,只是Vector是線程安全的,ArrayLsit不是線程安全的,它倆的性能也相差無幾。
✅ LinkedList
- LinkedList使用循環雙向鏈表數據結構,它和基於數組的List相比是截然不同的,在內存中分配的空間不是連續的。
- LinkedList是由一系列表項組成的,包括數據內容、前驅表項、后驅表項,或者說前后兩個指針
- 不管LinkedList集合是否為空,都有一個header表項。(前驅表項指向 最后一個 元素,后驅表項指向 第一個元素)
雙向鏈表
表項
1. 添加元素
//1. 添加元素,不指定位置會添加到最后一個
public boolean add(E e) {
linkLast(e);
return true;
}
//2. 添加到最后一位(每次添加插入元素都會創建一個Node對象)
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++;
}
//3. 創建表項
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;
}
}
2. 添加指定位置元素
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++;
}
步驟一:獲取當前元素(未插入目前要插入的元素前)前指針指向的節點
步驟二:創建新節點,一個表項,前驅表項是前面的節點 步驟一得到,后驅表項指向的時當前位置的節點(未插入目前要插入的元素前)
步驟三:重新設置當前位置的節點(未插入目前要插入的元素前)的前驅指針指向的節點,也就是剛插入的創建的新節點
步驟四:是對創建新節點的前置節點的后驅表項進行修改設置
總結:對應LinkedList插入任意位置元素 我們只需創建一個新元素節點和移動前后兩個表項的指針,其他表項無需任何操作,性能高;
3. LinkedList集合中的第一個和最后一個元素位置是確定的
最后一個元素和第一個元素的位置不需要遍歷整個鏈表獲取,每個LinkedList集合無論是否為空,都會有一個Header表項,Header表項的前驅指針始終指向最后一個元素,后驅指針指向第一個元素,所以可以說LinkedList集合中的第一個和最后一個元素位置是確定的。
4. 查找刪除元素
循環雙向鏈表結構中節點位置的確定需要根據前面的元素往后遍歷查找或者后面的元素往前遍歷查找
// 1. 獲取指定位置元素
public E get(int index) {
checkElementIndex(index); // 首先判斷位置是否有效
return node(index).item;
}
// 2. 根據位置對節點遍歷查找
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;
}
}
從上面的源碼中可以看出如果LinkedList集合的長度很大,則每次查找元素都會遍歷很多次,性能影響也會更大
刪除任意位置的元素,是先要找到該元素,所以需要上一步提到的查找操作
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index)); // node(index) 是查找元素
}
ArrayList和LinkedLisy對比
🍡 實現方式
ArrayList 是基於數組實現,內存中分配連續的空間,需要維護容量大小。
LinkedList 是基於循環雙向鏈表數據結構,不需要維護容量大小。
🍪 添加插入刪除元素
ArrayList不自定義位置添加元素和LinkedList性能沒啥區別,ArrayList默認元素追加到數組后面,而LinkedList只需要移動指針,所以兩者性能相差無幾。
如果ArrayList自定義位置插入元素,越靠前,需要重寫排序的元素越多,性能消耗越大,LinkedList無論插入任何位置都一樣,只需要創建一個新的表項節點和移動一下指針,性能消耗很低。
頻繁插入刪除元素 -> 使用 LinkedList 集合
🚓 遍歷查看元素
ArrayList是基於數組,所以查看任意位置元素只需要獲取當前位置的下標的數組就可以,效率很高,然而LinkedList獲取元素需要從最前面或者最后面遍歷到當前位置元素獲取,如果集合中元素很多,就會效率很低,性能消耗大。
頻繁遍歷查看元素 -> 使用 ArrayList 集合
🍝 ArrayList和LinkedList 都時線程不安全的