[原始碼分析]ArrayList和LinkedList如何實現的?我看你還有機會!
阿新 • • 發佈:2020-08-13
> 文章已經收錄在 [Github.com/niumoo/JavaNotes](https://github.com/niumoo/JavaNotes) ,更有 Java 程式設計師所需要掌握的核心知識,歡迎Star和指教。
> 歡迎關注我的[公眾號](https://github.com/niumoo/JavaNotes#%E5%85%AC%E4%BC%97%E5%8F%B7),文章每週更新。
## 前言
說真的,在 Java 使用最多的集合類中,List 絕對佔有一席之地的,它和 Map 一樣適用於很多場景,非常方便我們的日常開發,畢竟儲存一個列表的需求隨處可見。儘管如此,還是有很多同學沒有弄明白 List 中 **ArrayList** 和 **LinkedList** 有什麼區別,這簡直太遺憾了,這兩者其實都是資料結構中的**基礎內容**,這篇文章會從**基礎概念**開始,分析兩者在 Java 中的**具體原始碼實現**,尋找兩者的不同之處,最後思考它們使用時的注意事項。
這篇文章會包含以下內容。
1. 介紹線性表的概念,詳細介紹線性表中**陣列**和**連結串列**的資料結構。
2. 進行 ArrayList 的原始碼分析,比如儲存結構、擴容機制、資料新增、資料獲取等。
3. 進行 LinkedList 的原始碼分析,比如它的儲存結構、資料插入、資料查詢、資料刪除和 LinkedList 作為佇列的使用方式等。
4. 進行 ArrayList 和 LinkedList 的總結。
## 線性表
要研究 **ArrayList** 和 **LinkedList** ,首先要弄明白什麼是**線性表**,這裡引用百度百科的一段文字。
> 線性表是最基本、最簡單、也是最常用的一種資料結構。線性表(linear list)是資料結構的一種,一個線性表是n個具有相同特性的資料元素的有限序列。
你肯定看到了,線性表在資料結構中是一種**最基本、最簡單、最常用**的資料結構。它將資料一個接一個的排成一條線(可能邏輯上),也因此線性表上的每個資料只有前後兩個方向,而在資料結構中,**陣列、連結串列、棧、佇列**都是線性表。你可以想象一下整整齊齊排隊的樣子。
![線性表](https://cdn.jsdelivr.net/gh/niumoo/cdn-assets/2020/image-20200809004119875.png)
看到這裡你可能有疑問了,有線性表,那麼肯定有**非線性表**嘍?沒錯。**二叉樹**和**圖**就是典型的非線性結構了。不要被這些花裡胡哨的圖嚇到,其實這篇文章非常簡單,希望同學耐心看完**點個贊**。
![非線性介面(圖片來自網路)](https://cdn.jsdelivr.net/gh/niumoo/cdn-assets/2020/grap.png)
### 陣列
既然知道了什麼是線性表,那麼理解陣列也就很容易了,首先陣列是線性表的一種實現。陣列是由**相同型別**元素組成的一種資料結構,陣列需要分配**一段連續的記憶體**用來儲存。注意關鍵詞,**相同型別**,**連續記憶體**,像這樣。
![陣列](https://cdn.jsdelivr.net/gh/niumoo/cdn-assets/2020/image-20200810224700319.png)
不好意思放錯圖了,像這樣。
![陣列概念](https://cdn.jsdelivr.net/gh/niumoo/cdn-assets/2020/image-20200808232102227.png)
上面的圖可以很直觀的體現陣列的儲存結構,因為陣列記憶體地址連續,元素型別固定,所有具有**快速查詢**某個位置的元素的特性;同時也因為陣列需要一段連續記憶體,所以長度在初始化**長度已經固定**,且不能更改。Java 中的 **ArrayList** 本質上就是一個數組的封裝。
### 連結串列
連結串列也是一種線性表,和陣列不同的是連結串列**不需要連續的記憶體**進行資料儲存,而是在每個節點裡同時**儲存下一個節點**的指標,又要注意關鍵詞了,每個節點都有一個指標指向下一個節點。那麼這個連結串列應該是什麼樣子呢?看圖。
![單向連結串列](https://cdn.jsdelivr.net/gh/niumoo/cdn-assets/2020/image-20200810224910849.png)
哦不,放錯圖了,是這樣。
![連結串列儲存結構(圖片來自網路)](https://cdn.jsdelivr.net/gh/niumoo/cdn-assets/2020/image-20200808233941445.png)
上圖很好的展示了連結串列的儲存結構,圖中每個節點都有一個指標指向下一個節點位置,這種我們稱為**單向連結串列**;還有一種連結串列在每個節點上還有一個指標指向上一個節點,這種連結串列我們稱為**雙向連結串列**。圖我就不畫了,像下面這樣。
![雙向連結串列](https://cdn.jsdelivr.net/gh/niumoo/cdn-assets/2020/image-20200810224500217.png)
可以發現連結串列不必連續記憶體儲存了,因為連結串列是通過節點指標進行下一個或者上一個節點的,只要找到頭節點,就可以以此找到後面一串的節點。不過也因此,連結串列在**查詢或者訪問某個位置的節點**時,需要**O(n)**的時間複雜度。但是插入資料時可以達到**O(1)**的複雜度,因為只需要修改節點指標指向。
## ArratList
上面介紹了線性表的概念,並舉出了兩個線性表的實際實現例子,既陣列和連結串列。在 Java 的集合類 ArrayList 裡,實際上使用的就是陣列儲存結構,ArrayList 對 Array 進行了封裝,並增加了方便的插入、獲取、擴容等操作。因為 ArrayList 的底層是陣列,所以存取非常迅速,但是增刪時,因為要移動後面的元素位置,所以增刪效率相對較低。那麼它具體是怎麼實現的呢?不妨深入原始碼一探究竟。
### ArrayList 儲存結構
檢視 ArrayList 的原始碼可以看到它就是一個簡單的陣列,用來資料儲存。
```java
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
```
通過上面的註釋瞭解到,ArrayList 無參構造時是會共享一個長度為 0 的陣列 DEFAULTCAPACITY_EMPTY_ELEMENTDATA. 只有當第一個元素新增時才會第一次擴容,這樣也防止了建立物件時更多的記憶體浪費。
### ArrayList 擴容機制
我們都知道陣列的大小一但確定是不能改變的,那麼 ArrayList 明顯可以不斷的新增元素,它的底層又是陣列,它是怎麼實現的呢?從上面的 ArrayList 儲存結構以及註釋中瞭解到,ArrayList 在初始化時,是共享一個長度為 0 的陣列的,當第一個元素新增進來時會進行第一次擴容,我們可以想像出 ArrayList 每當空間不夠使用時就會進行一次擴容,那麼擴容的機制是什麼樣子的呢?
依舊從原始碼開始,追蹤 add() 方法的內部實現。
```java
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return true (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
// 開始檢查當前插入位置時陣列容量是否足夠
private void ensureCapacityInternal(int minCapacity) {
// ArrayList 是否未初始化,未初始化是則初始化 ArrayList ,容量給 10.
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
// 比較插入 index 是否大於當前陣列長度,大於就 grow 進行擴容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 擴容規則是當前容量 + 當前容量右移1位。也就是1.5倍。
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 是否大於 Int 最大值,也就是容量最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 拷貝元素到擴充後的新的 ArrayList
elementData = Arrays.copyOf(elementData, newCapacity);
}
```
通過原始碼發現擴容邏輯還是比較簡單的,整理下具體的擴容流程如下:
1. 開始檢查當前插入位置時陣列容量是否足夠
2. ArrayList 是否未初始化,未初始化是則初始化 ArrayList ,容量給 10.
3. 判斷當前要插入的下標是否大於容量
1. 不大於,插入新增元素,新增流程完畢。
4. 如果所需的容量大於當前容量,開始擴充。
1. 擴容規則是當前容量 + 當前容量右移1位。也就是1.5倍。
`int newCapacity = oldCapacity + (oldCapacity >> 1);`
2. 如果擴充之後還是小於需要的最小容量,則把所需最小容量作為容量。
3. 如果容量大於預設最大容量,則使用 最大值 Integer 作為容量。
4. 拷貝老陣列元素到擴充後的新陣列
5. 插入新增元素,新增流程完畢。
### ArrayList 資料新增
上面分析擴容時候已經看到了新增一個元素的具體邏輯,因為底層是陣列,所以直接指定下標賦值即可,非常簡單。
```java
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e; // 直接賦值
return true;
}
```
但是還有一種新增資料得情況,就是新增時指定了要加入的下標位置。這時邏輯有什麼不同呢?
```java
/**
* Inserts the specified element at the specified position in this
* list. Shifts the element currently at that position (if any) and
* any subsequent elements to the right (adds one to their indices).
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
// 指定下標開始所有元素後移一位
System.arraycopy(elementData, index, elementData, index + 1,size - index);
elementData[index] = element;
size++;
}
```
可以發現這種新增多了關鍵的一行,它的作用是把從要插入的座標開始的元素都向後移動一位,這樣才能給指定下標騰出空間,才可以放入新增的元素。
比如你要在下標為 3 的位置新增資料100,那麼下標為3開始的所有元素都需要後移一位。
![ArrayList 隨機新增資料](https://cdn.jsdelivr.net/gh/niumoo/cdn-assets/2020/image-20200809004018640.png)
由此也可以看到 ArrayList 的一個缺點,**隨機插入新資料時效率不高**。
### ArrayList 資料獲取
資料下標獲取元素值,**一步到位,不必多言**。
```java
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
```
## LinkedList
LinkedList 的底層就是一個連結串列線性結構了,連結串列除了要有一個節點物件外,根據單向連結串列和雙向連結串列的不同,還有一個或者兩個指標。那麼 LinkedList 是單鏈表還是雙向連結串列呢?
### LinkedList 儲存結構
依舊深入 LinkedList 原始碼一探究竟,可以看到 LinkedList 無參構造裡沒有任何操作,不過我們通過檢視變數 first、last 可以發現它們就是儲存連結串列第一個和最後 一個的節點。
```java
transient int size = 0;
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transi