1. 程式人生 > >[原始碼分析]ArrayList和LinkedList如何實現的?我看你還有機會!

[原始碼分析]ArrayList和LinkedList如何實現的?我看你還有機會!

> 文章已經收錄在 [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