ArrayList和LinkedList如何實現的?
說真的,在 Java 使用最多的集合類中,List 絕對佔有一席之地的,它和 Map 一樣適用於很多場景,非常方便我們的日常開發,畢竟儲存一個列表的需求隨處可見。儘管如此,還是有很多同學沒有弄明白 List 中 ArrayList 和 LinkedList 有什麼區別,這簡直太遺憾了,這兩者其實都是資料結構中的 基礎內容,這篇文章會從 基礎概念開始,分析兩者在 Java 中的 具體原始碼實現,尋找兩者的不同之處,最後思考它們使用時的注意事項。
本文包含以下內容。
- 介紹線性表的概念,詳細介紹線性表中 陣列和 連結串列的資料結構。
- 進行 ArrayList 的原始碼分析,比如儲存結構、擴容機制、資料新增、資料獲取等。
- 進行 LinkedList 的原始碼分析,比如它的儲存結構、資料插入、資料查詢、資料刪除和 LinkedList 作為佇列的使用方式等。
- 進行 ArrayList 和 LinkedList 的總結。
線性表
要研究 ArrayList 和 LinkedList ,首先要弄明白什麼是 線性表,這裡引用百度百科的一段文字。
線性表是最基本、最簡單、也是最常用的一種資料結構。線性表(linear list)是資料結構的一種,一個線性表是n個具有相同特性的資料元素的有限序列。
你肯定看到了,線性表在資料結構中是一種 最基本、最簡單、最常用的資料結構。它將資料一個接一個的排成一條線(可能邏輯上),也因此線性表上的每個資料只有前後兩個方向,而在資料結構中, 陣列、連結串列、棧、佇列都是線性表。你可以想象一下整整齊齊排隊的樣子。
看到這裡你可能有疑問了,有線性表,那麼肯定有 非線性表嘍?沒錯。 二叉樹和 圖就是典型的非線性結構了。不要被這些花裡胡哨的圖嚇到,其實這篇文章非常簡單,希望同學耐心看完 點個贊。
陣列
既然知道了什麼是線性表,那麼理解陣列也就很容易了,首先陣列是線性表的一種實現。陣列是由 相同型別元素組成的一種資料結構,陣列需要分配 一段連續的記憶體用來儲存。注意關鍵詞, 相同型別, 連續記憶體,像這樣。
不好意思放錯圖了,像這樣。
上面的圖可以很直觀的體現陣列的儲存結構,因為陣列記憶體地址連續,元素型別固定,所有具有 快速查詢某個位置的元素的特性;同時也因為陣列需要一段連續記憶體,所以長度在初始化 長度已經固定,且不能更改。Java 中的 ArrayList 本質上就是一個陣列的封裝。
連結串列
連結串列也是一種線性表,和陣列不同的是連結串列 不需要連續的記憶體進行資料儲存,而是在每個節點裡同時 儲存下一個節點的指標,又要注意關鍵詞了,每個節點都有一個指標指向下一個節點。那麼這個連結串列應該是什麼樣子呢?看圖。
哦不,放錯圖了,是這樣。
上圖很好的展示了連結串列的儲存結構,圖中每個節點都有一個指標指向下一個節點位置,這種我們稱為 單向連結串列;還有一種連結串列在每個節點上還有一個指標指向上一個節點,這種連結串列我們稱為 雙向連結串列。圖我就不畫了,像下面這樣。
可以發現連結串列不必連續記憶體儲存了,因為連結串列是透過節點指標進行下一個或者上一個節點的,只要找到頭節點,就可以以此找到後面一串的節點。不過也因此,連結串列在 查詢或者訪問某個位置的節點時,需要**O(n) 的時間複雜度。但是插入資料時可以達到O(1)**的複雜度,因為只需要修改節點指標指向。
ArratList
上面介紹了線性表的概念,並舉出了兩個線性表的實際實現例子,既陣列和連結串列。在 Java 的集合類 ArrayList 裡,實際上使用的就是陣列儲存結構,ArrayList 對 Array 進行了封裝,並增加了方便的插入、獲取、擴容等操作。因為 ArrayList 的底層是陣列,所以存取非常迅速,但是增刪時,因為要移動後面的元素位置,所以增刪效率相對較低。那麼它具體是怎麼實現的呢?不妨深入原始碼一探究竟。
ArrayList 儲存結構
檢視 ArrayList 的原始碼可以看到它就是一個簡單的陣列,用來資料儲存。
/**
* 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() 方法的內部實現。
/**
* Appends the specified element to the
end of this list.
*
* @param e element to be appended to this list
* @return <tt>
true<
/tt> (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);
}
透過原始碼發現擴容邏輯還是比較簡單的,整理下具體的擴容流程如下:
- 開始檢查當前插入位置時陣列容量是否足夠
- ArrayList 是否未初始化,未初始化是則初始化 ArrayList ,容量給 10.
- 判斷當前要插入的下標是否大於容量不大於,插入新增元素,新增流程完畢。
- 如果所需的容量大於當前容量,開始擴充。擴容規則是當前容量 + 當前容量右移1位。也就是1.5倍。int newCapacity = oldCapacity + (oldCapacity >> 1);如果擴充之後還是小於需要的最小容量,則把所需最小容量作為容量。如果容量大於預設最大容量,則使用 最大值 Integer 作為容量。複製老陣列元素到擴充後的新陣列
- 插入新增元素,新增流程完畢。
ArrayList 資料新增
上面分析擴容時候已經看到了新增一個元素的具體邏輯,因為底層是陣列,所以直接指定下標賦值即可,非常簡單。
public
boolean
add
(E e) {
ensureCapacityInternal(size +
1);
// Increments modCount!!
elementData[size++] = e; // 直接賦值
return true;
}
但是還有一種新增資料得情況,就是新增時指定了要加入的下標位置。這時邏輯有什麼不同呢?
/**
* 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 的一個缺點, 隨機插入新資料時效率不高。
ArrayList 資料獲取
資料下標獲取元素值, 一步到位,不必多言。
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 可以發現它們就是儲存連結串列第一個和最後 一個的節點。
transient
int
size
=
0
;
/**
*
Pointer
to
first
node.
*
Invariant:
(first
==
null
&&
last
==
null
)
||
*
(first.prev
==
null
&&
first.item
!=
null
)
*/
transient
Node<E>
first;
/**
*
Pointer
to
last
node.
*
Invariant:
(first
==
null
&&
last
==
null
)
||
*
(last.next
==
null
&&
last.item
!=
null
)
*/
transient
Node<E>
last;
/**
*
Constructs
an
empty
list.
*/
public
LinkedList()
{
}
變數 first 和 last 都是 Node 型別,繼而檢視 Node 原始碼。
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;
}
}
可以看到這就是一個典型的 雙向連結串列結構,item 用來存放元素值;next 指向下一個 node 節點,prev 指向上一個 node 節點。
LinkedList 資料獲取
連結串列不像陣列是連續的記憶體地址,連結串列是透過next 和 prev 指向記錄連結路徑的,所以查詢指定位置的 node 只能遍歷查詢,檢視原始碼也是如此。
public E
get
(
int index) {
checkElementIndex(index);
return node(index).item;
}
/**
* Returns the (non-null) Node at the specified element index.
*/
// 遍歷查詢 index 位置的節點資訊
Node<E> node(int index) {
// assert isElementIndex(index);
// 這裡判斷 index 是在當前連結串列的前半部分還是後半部分,然後決定是從
// first 向後查詢還是從 last 向前查詢。
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;
}
}
查詢指定位置的 node 物件,這個部分要注意的是,查詢會首先判斷 index 是在當前連結串列的前半部分還是後半部分,然後決定是從 first 向後查詢還是從 last 向前查詢。這樣可以增加查詢速度。從這裡也可以看出連結串列在查詢指定位置元素時,效率不高。
LinkedList 資料新增
因為 LinkedList 是連結串列,所以 LinkedList 的新增也就是連結串列的資料新增了,這時候要根據要插入的位置的區分操作。
- 尾部插入public boolean add(E e) { linkLast(e); return true; } void linkLast(E e) { final Node<E> l = last; // 新節點,prev 為當前尾部節點,e為元素值,next 為 null, final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else // 目前的尾部節點 next 指向新的節點 l.next = newNode; size++; modCount++; } 預設的 add 方式就是尾部新增了,尾部新增的邏輯很簡單,只需要建立一個新的節點,新節點的 prev 設定現有的末尾節點,現有的末尾 Node 指向新節點 Node,新節點的 next 設為 null 即可。
- 中間新增下面是在指定位置新增元素,涉及到的原始碼部分。public void add(int index, E element) { checkPositionIndex(index); if (index == size) // 如果位置就是當前連結串列尾部,直接尾插 linkLast(element); else // 獲取 index 位置的節點,插入新的元素 linkBefore(element, node(index)); } /** * Inserts element e before non-null Node succ. */ // 在指定節點處新增元素,修改指定元素的下一個節點為新增元素,新增元素的下一個節點是查詢到得 node 的next節點指向, // 新增元素的上一個節點為查詢到的 node 節點,查詢到的 node 節點的 next 指向 node 的 prev 修改為新 Node 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++; } 可以看到指定位置插入元素主要分為兩個部分,第一個部分是查詢 node 節點部分,這部分就是上面介紹的 LinkedList 資料獲取部分,第二個部分是在查詢到得 node 物件後插入元素。主要就是修改 node 的 next 指向為新節點,新節點的 prev 指向為查詢到的 node 節點,新節點的 next 指向為查詢到的 node 節點的 next 指向。查詢到的 node 節點的 next 指向的 node 節點的 prev 修改為新節點。
LinkedList 資料刪除
依舊檢視原始碼進行分析,原始碼中看到如果節點是頭結點或者尾節點,刪除比較簡單。我們主要看刪除中間一個節點時的操作
public E
remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
/**
* Unlinks non-null node x.
*/
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;
}
node(index) 方法依舊是二分查詢目標位置,然後進行刪除操作。比如要刪除的節點叫做 X,刪除操作主要是修改 X 節點的 prev 節點的 next 指向為 X 節點的 next 指向,修改 X 節點的 next 節點的 prev 指向為 X 節點的 prev 指向,最後把 X 節點的 prev 和 next 指向清空。如果理解起來有點費勁,可以看下面這個圖,可能會比較明白。
擴充套件
你以為 LinkedList 只是一個 List,其他它不僅實現了 List 介面,還實現了 Deque ,所以它表面上是一個 List,其實它還是一個佇列。
public
class
LinkedList<E>
extends
AbstractSequentialList<E>
implements
List<E>,
Deque<E>,
Cloneable,
java.
io.
Serializable
體驗一下先進先出的佇列。
Queue<String>
queue =
new LinkedList<>();
queue.add(
"a");
queue.add(
"b");
queue.add(
"c");
queue.add(
"d");
System.out.println(
queue.poll());
System.out.println(
queue.poll());
System.out.println(
queue.poll());
System.out.println(
queue.poll());
// result:
// a
// b
// c
// d
同學可以思考一下這個佇列是怎麼實現的,其實很簡單對不對,就是先進先出嘛,poll 時刪除 first 節點不就完事了嘛。
總結
不管是 ArrayList 還是 LinkedList 都是開發中常用的集合類,這篇文章分析了兩者的底層實現,透過對底層實現的分析我們可以總結出兩者的主要優缺點。
- 遍歷,ArrayList 每次都是 直接定位,LinkedList 透過 next 節點定位,不相上下。這裡要注意的是 LinkedList 只有使用 迭代器的方式遍歷才會使用 next 節點。如果使用 get ,則因為遍歷查詢效率低下。
- 新增,ArrayList 可能會需要 擴容,中間插入時,ArrayList 需要 後移插入位置之後的所有元素。LinkedList 直接修改 node 的 prev, next 指向,LinkedList 勝出。
- 刪除,同 2.
- 隨機訪問指定位置,ArrayList 直接定位,LinkedList 從頭會尾開始查詢, 陣列勝出。
綜上所述,ArrayList 適合儲存和訪問資料,LinkedList 則更適合資料的處理,希望你以後在使用時可以合理的選擇 List 結構。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69923331/viewspace-2711554/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- [原始碼分析]ArrayList和LinkedList如何實現的?我看你還有機會!原始碼
- ArrayList和LinkedList的區別?
- ArrayList和LinkedList的區別
- ArrayList和LinkedList的比較
- LinkedList和ArrayList的區別、Vector和ArrayList的區別
- ArrayList和LinkedList區別 javaJava
- Java中ArrayList和LinkedList區別Java
- ArrayList和LinkedList的區別是什麼
- Java ArrayList 與 LinkedListJava
- java複習之 Vector、ArrayList和LinkedList 的區別Java
- 說出 ArrayList,Vector, LinkedList 的儲存效能和特性?
- ArrayList & LinkedList原始碼解析原始碼
- ArrayList和LinkedList底層原理的區別和使用場景
- Java 容器和泛型(2)ArrayList 、LinkedList和Vector比較Java泛型
- ArrayList與linkedlist插入效率分析
- java中的List介面(ArrayList、Vector、LinkedList)Java
- LinkedList 的實現原理
- ArrayList、LinkedList和Vector的原始碼解析,帶你走近List的世界原始碼
- ArrayList,LinkedList,Vector,Stack之間的區別
- Java ArrayList 與 LinkedList 的靈活選擇Java
- Java List 常用集合 ArrayList、LinkedList、VectorJava
- Java 集合 ArrayList VS LinkedList VS VectorJava
- ARRAYLIST VECTOR LINKEDLIST 區別與用法
- ArrayList,HashMap,LinkedList 初始化大小和 擴容機制HashMap
- LinkedList的底層實現
- LinkedList實現原理
- ArrayList的簡單實現
- Java-ArrayList & LinkedList的原始碼對比分析Java原始碼
- Java:基於LinkedList實現棧和佇列Java佇列
- Java資料結構之LinkedList、ArrayList的效率分析Java資料結構
- ArrayList和LinkedList的幾種迴圈遍歷方式及效能對比分析
- 【java】【集合】List的三個子類—ArrayList、Vector、LinkedList的區別和聯絡Java
- 一道關於:ArrayList、Vector、LinkedList的儲存效能和特性 的面試題面試題
- ArrayList底層的實現原理
- 資料結構--LinkedList的實現資料結構
- ArrayList、Vector、LinkedList的區別及其優缺點? (轉載)
- List集合總結,對比分析ArrayList,Vector,LinkedList
- List集合(ArrayList-LinkedList);Set集合(HashSet-TreeSet)