Java集合框架的Collection分支詳解
Collection 接口概覽
在Java集合框架中,Collection 是最基礎(chǔ)的根接口。需要注意的是,雖然 Map 同屬集合框架范疇,但它與Collection并非繼承關(guān)系,而是兩個(gè)獨(dú)立的頂級(jí)接口。
Collection 接口定義在 java.util 包中,是一個(gè)泛型接口,繼承自 Iterable 接口。
package java.util;
// 繼承了 Iterable,意味著所有 Collection 實(shí)現(xiàn)類都可以使用 for-each 循環(huán)
public interface Collection<E> extends Iterable<E> {
// .......
}從架構(gòu)層面來看,Collection 接口主要擴(kuò)展為三大核心子接口:
- List:有序、可重復(fù)的集合
- Set:無序、不可重復(fù)的集合
- Queue:隊(duì)列,通常遵循FIFO(先進(jìn)先出)規(guī)則
Collection 定義了所有單列集合共有的操作。根據(jù)功能可分為四大類:添加、刪除、查詢(判斷)及其他操作:
添加操作
| 方法簽名 | 描述 | 注意事項(xiàng) |
|---|---|---|
| boolean add(E e) | 向集合中添加一個(gè)元素 e | 若集合發(fā)生變化(例如 Set 成功插入元素),則返回 true;若集合未發(fā)生變化(例如 Set 中已存在該元素),則返回 false |
| boolean addAll(Collection<? extends E> c) | 將指定集合 c 中的所有元素添加到當(dāng)前集合 | 相當(dāng)于將另一個(gè)集合合并到當(dāng)前集合中 |
刪除操作
| 方法簽名 | 描述 | 注意事項(xiàng) |
|---|---|---|
| boolean remove(Object e) | 刪除集合中指定的單個(gè)元素 o | 如果存在并刪除成功返回 true;不存在返回false |
| boolean removeAll(Collection<? extends E> c) | 刪除當(dāng)前集合中包含在集合 c 里的所有元素 | 執(zhí)行差集運(yùn)算(當(dāng)前集合減去集合c) |
| void clear() | 清空集合中所有元素 | 集合中的元素清空,但集合對象本身還存在 |
| default boolean removeIf(Predicate<? super E> filter) | (JDK 8 新增) 刪除滿足給定條件的所有元素 | 配合 Lambda 表達(dá)式使用,非常強(qiáng)大 |
查詢與判斷
| 方法簽名 | 描述 | 注意事項(xiàng) |
|---|---|---|
| int size() | 返回集合中元素的個(gè)數(shù) | 最大值是 Integer.MAX_VALUE(2^{31} - 1) |
| boolean isEmpty() | 判斷集合是否為空(size == 0) | 建議用此方法判斷,而不是 size() == 0,因?yàn)檎Z義更清晰 |
| boolean contains(Object o) | 判斷集合是否包含指定元素 o | 底層依賴 equals() 方法 |
| boolean containsAll(Collection<?> c) | 判斷當(dāng)前集合是否包含集合 c 中的所有元素 | 判斷當(dāng)前集合是否包含子集 c |
數(shù)組轉(zhuǎn)換與遍歷
| 方法簽名 | 描述 | 注意事項(xiàng) |
|---|---|---|
| Object[] toArray() | 將集合轉(zhuǎn)換為Object類型數(shù)組 | 類型丟失,通常不推薦使用 |
| T[] toArray(T[] a) | 將集合轉(zhuǎn)換為指定類型的數(shù)組 | 推薦使用。當(dāng)數(shù)組 |
| Iterator iterator() | 返回在此集合上進(jìn)行迭代的迭代器 | 用于遍歷和刪除元素 |
自 JDK 8 起,Collection 接口新增了多個(gè) default 方法,顯著提升了集合操作的函數(shù)式編程能力和使用便捷性:
- default boolean removeIf(Predicate filter):用于刪除滿足條件的元素
- default Stream<E> stream():用于將集合轉(zhuǎn)換為流(Stream)。它返回一個(gè)順序流(非并行流)
- default Spliterator<E> spliterator():為集合創(chuàng)建一個(gè)分割迭代器,用于并行遍歷
注意:以上方法在 List 和 Set 中的具體行為可能略有不同(例如 List允許元素重復(fù),add操作始終返回true;而Set不允許重復(fù)元素,當(dāng)嘗試添加已存在元素時(shí)會(huì)返回false)。
一句話總結(jié):Collection 定義了單列集合 “能干什么”,而 List、Set、Queue 定義了 "怎么干"(具體的行為約束)。
Collection 家族詳解

1. List
List 是一個(gè)有序的集合(序列),可以精確控制每個(gè)元素的插入位置。用戶可以通過索引來訪問元素。
核心特點(diǎn):
- 有序:元素按插入順序排列,每個(gè)元素都有索引。
- 可重復(fù):可以存儲(chǔ)任意多個(gè)相同的元素。
1.1 ArrayList
ArrayList 是 List 接口的一個(gè)可變長數(shù)組實(shí)現(xiàn)。它可以動(dòng)態(tài)地增長和縮減其容量,以容納任意數(shù)量的的元素。
核心特點(diǎn):
- 基于數(shù)組實(shí)現(xiàn):其底層是一個(gè) Object[] 數(shù)組,這決定了它的大部分性能特性。
- 有序:元素按照插入順序排列,每個(gè)元素都有一個(gè)精確的索引(從 0 開始)。
- 可重復(fù):可以存儲(chǔ)任意數(shù)量的重復(fù)元素,包含 null。
- 非線程安全:多個(gè)線程同時(shí)修改一個(gè) ArrayList 實(shí)例時(shí),需要外部同步。否則可能會(huì)導(dǎo)致數(shù)據(jù)不一致。
底層原理:動(dòng)態(tài)數(shù)組的奧秘
ArrayList 的精髓在于其 "動(dòng)態(tài)" 二字,那它是如何實(shí)現(xiàn)一個(gè)可以動(dòng)態(tài)擴(kuò)容的數(shù)組呢?
核心存儲(chǔ):elementData 與 size
要理解ArrayList,首先要區(qū)分兩個(gè)看似相似但意義完全不同的概念:容量 和 大小。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// 默認(rèn)初始容量
private static final int DEFAULT_CAPACITY = 10;
// 共享的空數(shù)組實(shí)列
private static final Object[] EMPTY_ELEMENTDATA = {};
//默認(rèn)大小的空數(shù)組實(shí)列
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 用于存儲(chǔ)元素的數(shù)組緩沖區(qū)。ArrayList 的容量就是這個(gè)數(shù)組的長度。
// transient 關(guān)鍵字表示該字段不會(huì)被序列化
transient Object[] elementData;
// ArrayList 中包含的元素?cái)?shù)量
private int size;
// ..........
}- elementData(容量):這是ArrayList的倉庫。它是一個(gè) Object[] 數(shù)組,其長度(elementData.length)代表了 ArrayList 當(dāng)前最多能夠裝多少個(gè)元素。
- size(大小):這是 ArrayList 的庫存清單。它是一個(gè) int 值,記錄了倉庫中實(shí)際存放了多少個(gè)元素。它永遠(yuǎn)小于或等于 elementData.length.
誕生之初
ArrayList 提供了三種構(gòu)造函數(shù),它們決定了 elementData 的初始狀態(tài)。
ArrayList()
這是最常用的無參構(gòu)造函數(shù)。
public ArrayList(){
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}這里關(guān)鍵點(diǎn)在于理解為什么會(huì)初始化一個(gè)空數(shù)組,而不是一個(gè)有默認(rèn)容量10 的數(shù)組。這是一種延遲初始化(懶加載)的優(yōu)化策略。在舊版本(JDK1.6及之前)中,它會(huì)直接創(chuàng)建一個(gè)容量為10的數(shù)組。這就意味著,即使你創(chuàng)建了一個(gè)ArrayList,但馬上就丟棄了,或者只往里面放了一兩個(gè)元素,系統(tǒng)也會(huì)預(yù)先分配10個(gè)對象引用的內(nèi)存空間,這可能就造成了內(nèi)存浪費(fèi)。故而從JDK1.7開始,ArrayList的實(shí)現(xiàn)進(jìn)行了優(yōu)化,采用了懶加載策略。當(dāng)你 new ArrayList() 時(shí),它只是將 elementData 指向一個(gè)共享的空數(shù)組,此時(shí)幾乎沒有占用額外的內(nèi)存空間(除對象本身的少量開銷)。真正的容量10 是在第一次添加元素時(shí)進(jìn)行分配的,ArrayList 會(huì)檢查當(dāng)前的 elementData 是不是那個(gè)默認(rèn)的空數(shù)組,如果是,它就會(huì)為這個(gè)數(shù)組分配初始容量的內(nèi)存空間,然后將新元素放入其中。
//JDK 1.6
public ArrayList() {
this(10); // 直接調(diào)用帶容量的構(gòu)造函數(shù),傳入 10
}
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
this.elementData = new Object[initialCapacity]; // 立即創(chuàng)建一個(gè)長度為 10 的數(shù)組
}ArrayList(int initialCapacity)
這個(gè)構(gòu)造函數(shù)接受一個(gè)整數(shù)參數(shù),用于初始化 ArrayList 的底層數(shù)組大小。
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 如果初始容量大于0,創(chuàng)建對應(yīng)大小的Object數(shù)組
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 如果初始容量為0,使用預(yù)定義的空數(shù)組常量
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 如果初始容量為負(fù)數(shù),拋出非法參數(shù)異常
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}當(dāng)你知道大概要存儲(chǔ)多少元素時(shí),可以預(yù)先指定容量,避免頻繁擴(kuò)容帶來的性能開銷。
注意:
- 初始容量過小會(huì)導(dǎo)致頻繁擴(kuò)容,影響性能
- 初始容量過大會(huì)浪費(fèi)內(nèi)存空間
ArrayList(Collection<? extends E> c)
這個(gè)構(gòu)造函數(shù)接收一個(gè)實(shí)現(xiàn)Collection接口的集合作為參數(shù),用于創(chuàng)建一個(gè)新的ArrayList實(shí)列。
public ArrayList(Collection<? extends E> c) {
// 使用Collection的toArray()方法將傳入的集合轉(zhuǎn)換為數(shù)組
Object[] a = c.toArray();
// 將數(shù)組長度賦值給 size,同時(shí)判斷數(shù)組長度是否為0
// 如果數(shù)組長度不等于 0,則向下繼續(xù)執(zhí)行
// 否則,就使用預(yù)定義的空數(shù)組EMPTY_ELEMENTDATA
if ((size = a.length) != 0) {
//判斷集合本身是否是ArrayList
//如果是,就直接使用其內(nèi)部數(shù)組(elementData)
//否則,使用Arrays.copyOf()創(chuàng)建一個(gè)新的數(shù)組副本
//其目的是為了保證新創(chuàng)建的ArrayList的內(nèi)部數(shù)組完全獨(dú)立,不受原集合影響
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
elementData = EMPTY_ELEMENTDATA;
}
}該構(gòu)造函數(shù)主要用于將其他類型的集合轉(zhuǎn)換為ArrayList、創(chuàng)建一個(gè)已存在集合的ArrayList副本和快速初始化一個(gè)包含特定元素的ArrayList。在使用時(shí)需要注意類型安全的問題,雖然使用了<? extends E>通配符,但在編譯時(shí)會(huì)進(jìn)行類型檢查,如果傳入的集合包含不兼容的類型,在編譯時(shí)會(huì)報(bào)錯(cuò)。
擴(kuò)容機(jī)制
擴(kuò)容是 ArrayList 最核心、也最耗時(shí)的操作。接下來通過add(E e)方法追蹤下 ArrayList 的擴(kuò)容過程。
步驟 1:添加元素 add(E e)
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 關(guān)鍵:檢查容量是否足夠
elementData[size++] = e; // 將元素放入數(shù)組,并增加 size
return true;
}在放入元素前,它會(huì)先調(diào)用ensureCapacityInternal( size +1 ),其目的是在放入第 size + 1 個(gè)元素前,檢查下容量夠不夠。
步驟 2:容量檢查 ensureCapacityInternal(size + 1)
private void ensureCapacityInternal(int minCapacity) {
// 如果elementData 是否是默認(rèn)的空數(shù)組
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 如果是,則將最小容量設(shè)置為默認(rèn)容量(10)和請求容量中的較大值
// 確保了第一次添加元素時(shí),數(shù)組至少有10個(gè)元素的空間
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}這就是 延遲初始化(懶加載)的體現(xiàn):第一次調(diào)用add(E e)方法時(shí),minCapacity會(huì)被直接提升到10,為后續(xù)的元素預(yù)留空間
步驟 3:觸發(fā)擴(kuò)容判斷 ensureExplicitCapacity(int minCapacity)
private void ensureExplicitCapacity(int minCapacity) {
// 修改計(jì)數(shù)器,用于迭代器的快速失敗機(jī)制
// 快速失敗機(jī)制(fail-fast):是一種錯(cuò)誤檢測機(jī)制。當(dāng)在迭代過程中,
// 如果檢測到集合結(jié)構(gòu)被修改(非迭代器自身的方法導(dǎo)致的修改)
// 迭代器會(huì)立即拋出ConcurrentModificationException異常,
// 而不是繼續(xù)執(zhí) 行可能導(dǎo)致不確定行為。
modCount++;
// 檢查是否需要擴(kuò)容 (所需最小容量大于當(dāng)前數(shù)組的長度)
if (minCapacity - elementData.length > 0)
// 執(zhí)行擴(kuò)容操作 grow(int minCapacity)
grow(minCapacity);
}步驟 4:擴(kuò)容的核心 grow(int minCapacity)
private void grow(int minCapacity) {
// overflow-conscious code
// 舊容量
int oldCapacity = elementData.length;
// 新容量 = 舊容量 + 舊容量/2 (即 1.5 倍)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果 1.5 倍后還不夠,就用所需的最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 處理極端情況,防止新容量超過 Integer.MAX_VALUE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 關(guān)鍵:復(fù)制數(shù)組
elementData = Arrays.copyOf(elementData, newCapacity);
}性能的權(quán)衡:為何擴(kuò)容因子是1.5倍:
- 避免頻繁擴(kuò)容:如果擴(kuò)容因子過?。ㄈ?1.1 倍),那么擴(kuò)容會(huì)非常頻繁,容易導(dǎo)致大量的數(shù)組復(fù)制開銷。
- 避免內(nèi)存浪費(fèi):如果擴(kuò)容因子過大(如 2.0 倍,Vector的選擇),可能會(huì)導(dǎo)致一次擴(kuò)容后預(yù)留大量未用空間,造成內(nèi)存浪費(fèi)。對于超大列表,這種內(nèi)存浪費(fèi)是驚人的。
- 數(shù)學(xué)上的合理性:1.5 倍(
oldCapacity + (oldCapacity >> 1))可以通過位運(yùn)算快速完成,比乘法效率略高。它在空間和時(shí)間成本之間取得了很好的平衡。
Arrays.copyOf():
它是java.util.Arrays 工具類中的一個(gè)核心方法,主要功能是復(fù)制一個(gè)指定的數(shù)組,并可以指定新數(shù)組的長度,從而實(shí)現(xiàn)截取和擴(kuò)容。這個(gè)方法是ArrayList 動(dòng)態(tài)數(shù)組實(shí)現(xiàn)擴(kuò)容的關(guān)鍵。
Arrays.copyOf()的內(nèi)部工作流:
- 創(chuàng)建新的數(shù)組:在堆內(nèi)存中開辟一塊新的、連續(xù)的內(nèi)存空間,大小為newCapacity。
- 數(shù)據(jù)復(fù)制:使用System.arraycopy() 這個(gè)本地方法,將 elementData 中的所有元素(從索引0 至 size -1 ) 按字節(jié)拷貝到新的數(shù)組中。這是一個(gè)O(n)的操作。
- 引用切換:將 elementData 的引用指向新的數(shù)組,舊數(shù)組會(huì)在下一次GC時(shí)被回收。
使用建議
- 擴(kuò)容操作涉及數(shù)組復(fù)制,相對耗時(shí)。如果能預(yù)估元素?cái)?shù)量,建議在初始化時(shí)指定容量,避免多次擴(kuò)容。
- 1.5 倍的擴(kuò)容策略也存在可能導(dǎo)致內(nèi)存浪費(fèi),如不需要添加元素時(shí),可以使用 trimToSize() 釋放多余空間。
- ArrayList不是線程安全的,在多線程環(huán)境下使用可能會(huì)導(dǎo)致出現(xiàn)數(shù)據(jù)不一致的情況。
1.2 Vector
它是最早的集合類之一,是線程安全列表的鼻祖。然而在如今的現(xiàn)代java開發(fā)中,它的身影卻是越來越少見了,甚至被貼上了 “過時(shí)” 的標(biāo)簽。Vector 與 ArrayList 非常相似,它也是一個(gè)基于動(dòng)態(tài)數(shù)組實(shí)現(xiàn)的 List 。它同樣是有序的、允許重復(fù)元素和 null 值的。
Vector 最獨(dú)一無二的特點(diǎn)是:它是線程安全的。
這就意味著,它的所有公共方法都被 synchronized 關(guān)鍵字修飾。當(dāng)一個(gè)線程訪問 Vector 的任何一個(gè)方法時(shí),它會(huì)先獲取 Vector 實(shí)例的對象鎖,其他試圖訪問的線程必須等待,直至鎖被釋放。
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
public synchronized boolean removeElement(Object obj) {
modCount++;
int i = indexOf(obj);
if (i >= 0) {
removeElementAt(i);
return true;
}
return false;
}
public synchronized void removeAllElements() {
modCount++;
// Let gc do its work
for (int i = 0; i < elementCount; i++)
elementData[i] = null;
elementCount = 0;
}
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
public synchronized E set(int index, E element) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
public synchronized E remove(int index) {
modCount++;
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
E oldValue = elementData(index);
int numMoved = elementCount - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--elementCount] = null; // Let gc do its work
return oldValue;
}
//...........性能分析:
Vector的性能瓶頸在于其粗粒度的鎖。即使只是進(jìn)行一個(gè)簡單的 get() 讀操作,也需要獲取整個(gè)對象的鎖,這會(huì)阻塞所有其他線程的讀和寫操作。在并發(fā)度稍高的場景下,這種串行化的執(zhí)行方式會(huì)成為嚴(yán)重的性能瓶頸。
底層原理:加鎖的動(dòng)態(tài)數(shù)組
Vector 的底層實(shí)現(xiàn)與 ArrayList 高度相似,都是基于一個(gè)可動(dòng)態(tài)擴(kuò)容的數(shù)組。
核心成員變量
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
protected Object[] elementData; // 存儲(chǔ)元素的數(shù)組
protected int elementCount; // 實(shí)際元素?cái)?shù)量(相當(dāng)于 ArrayList 中的 size)
protected int capacityIncrement; // 容量增長系數(shù)
private static final long serialVersionUID = -2767605614048989439L;
// 定義一個(gè)數(shù)組理論上的最大容量上限
// 注意:這個(gè) 8 不是一個(gè)魔法數(shù)字,它是基于對主流 JVM 實(shí)現(xiàn)對象內(nèi)存布局的經(jīng)驗(yàn)值。
// 它足夠大,可以覆蓋大多數(shù)虛擬機(jī)中數(shù)組對象的額外開銷。
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//...........
}與 ArrayList 相比,Vector 多了一個(gè) capacityIncrement 字段。這個(gè)字段用于自定義擴(kuò)容時(shí)的增長量。如果設(shè)置為 0 (默認(rèn)值),則容量翻倍。
擴(kuò)容機(jī)制
Vector 的擴(kuò)容機(jī)制與 ArrayList 類似,都是在容量不足時(shí)創(chuàng)建一個(gè)新數(shù)組并復(fù)制舊數(shù)據(jù)。但關(guān)鍵區(qū)別在于擴(kuò)容的倍數(shù)。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; // 獲取當(dāng)前數(shù)組容量
// 計(jì)算新容量 newCapacity:
// 如果設(shè)置了capacityIncrement(擴(kuò)容增量),則新容量 = 舊容量 + capacityIncrement
// 如果沒有設(shè)置capacityIncrement(默認(rèn)為0),則新容量 = 舊容量的2倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
// 檢查新容量是否滿足最小需求,如果不滿足則使用minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 檢查新容量是否超過最大數(shù)組限制(MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8)
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 使用Arrays.copyOf創(chuàng)建新數(shù)組并復(fù)制原有元素
elementData = Arrays.copyOf(elementData, newCapacity);
}這是 Vector 特有的擴(kuò)容機(jī)制,與 ArrayList 不同(ArrayList 默認(rèn) 1.5 倍)。
capacityIncrement 可以在構(gòu)造 Vector 時(shí)指定,用于控制每次擴(kuò)容的增長量。
方法 hugeCapacity(minCapacity) 處理容量超過Integer.MAX_VALUE - 8的情況。
為什么 Vector 會(huì)被棄用?
Vector 被標(biāo)記為 “過時(shí)”,并非是因?yàn)橛蠦ug,而是有了更好的代替方案。綜上所述,因?yàn)?Vector 所有的公共方法都被 synchronized 關(guān)鍵字修飾,所以其帶來的性能開銷在并發(fā)的場景下是難以接受的。
更好的替代品:
- Collections.synchronizedList(new ArrayList<>()):它是一個(gè)包裝器,它和Vector一樣,也是通過synchronized為每個(gè)方法加鎖保證線程安全。它的出現(xiàn),使開發(fā)者可以用更靈活的ArrayList來獲得線程安全能力,而不必鎖在Vector這個(gè)具體的實(shí)現(xiàn)上。
- java.util.concurrent.CopyOnWriteArrayList:這是JDK 1.5 引入的并發(fā)集合。它采用 “寫時(shí)復(fù)制”策略,實(shí)現(xiàn)了讀無操作鎖,極大提升了讀多寫少場景下的并發(fā)性能。是對Vector的降維打擊。
1.3 LinkedList
LinkedList 是基于節(jié)點(diǎn)的集合,其中的每個(gè)元素都包含了對前一個(gè)元素和后一個(gè)元素的引用。這種結(jié)構(gòu)就像是一列火車,每個(gè)車廂都連接著前一節(jié)和后一節(jié)車廂。
核心特點(diǎn):
- 基于雙向鏈表實(shí)現(xiàn):與 ArrayList 相比,這是最根本的區(qū)別。
- 有序:元素按插入順序排列。
- 可重復(fù):可以存儲(chǔ)任意相同元素和 null 值。
- 非線程安全:與 ArrayList 一樣,不是線程安全的。
- 雙重身份:它不僅實(shí)現(xiàn)了 List 接口,還實(shí)現(xiàn)了 Deque 接口(雙端隊(duì)列),這意味著它可以被當(dāng)作棧或隊(duì)列來使用。
底層原理:雙向鏈表的節(jié)點(diǎn)藝術(shù)
LinkedList 的優(yōu)雅與強(qiáng)大,完全隱藏在其精巧的節(jié)點(diǎn)結(jié)構(gòu)和指針操作之中。
基石:Node<E> 內(nèi)部類
LinkedList 的一切都始于這個(gè)私有的靜態(tài)內(nèi)部類。它不僅是一個(gè)基礎(chǔ)的數(shù)據(jù)容器,更是一個(gè)具備 “雙向感知” 功能的連接節(jié)點(diǎn)。
private static class Node<E> {
E item; // 存儲(chǔ)節(jié)點(diǎn)的數(shù)據(jù)
Node<E> next; // 指向下一個(gè)節(jié)點(diǎn)的引用
Node<E> prev; // 指向上一個(gè)節(jié)點(diǎn)的引用
// 構(gòu)造函數(shù):建立車廂之間的連接
Node(Node<E> prev, E element, Node<E> next) {
this.item = element; // 設(shè)置當(dāng)前節(jié)點(diǎn)的數(shù)據(jù)
this.next = next; // 設(shè)置下一個(gè)節(jié)點(diǎn)
this.prev = prev; // 設(shè)置上一個(gè)節(jié)點(diǎn)
}
}雙向鏈表節(jié)點(diǎn)由三部分組成:存儲(chǔ)的數(shù)據(jù)(item)、指向前驅(qū)節(jié)點(diǎn)的指針(prev)以及指向后繼節(jié)點(diǎn)的指針(next)。
宏觀結(jié)構(gòu):first、last和size
LinkedList 類本身并不直接存儲(chǔ)元素,而是通過三個(gè)核心成員變量來管理整個(gè)鏈表的狀態(tài)。
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0; // 記錄LinkedList中元素的數(shù)量,初始化為0,表示空鏈表
transient Node<E> first; // 頭指針,指向鏈表的第一個(gè)節(jié)點(diǎn),若鏈表為空則為 null
transient Node<E> last; // 尾指針,指向鏈表的最后一個(gè)節(jié)點(diǎn),若鏈表為空則為 null
//........
}這種結(jié)構(gòu)讓頭部和尾部的操作變得非常高效,因?yàn)榭梢灾苯油ㄟ^指針訪問。
性能分析:快與慢的另一面
| 操作類型 | 時(shí)間復(fù)雜度 | 原因分析 |
|---|---|---|
| 訪問( get(int index) ) | O(n) | 數(shù)組可以通過索引直接計(jì)算地址,但鏈表必須從 first 或 last 開始,一個(gè)一個(gè)地向前或向后遍歷,直至查找到目標(biāo)索引。 |
| 頭部/尾部添加(addFirst/addLast) | O(1) | 只需要修改 first 或 last 的引用,并讓新節(jié)點(diǎn)指向舊的頭部/尾部即可,不涉及數(shù)據(jù)移動(dòng)。 |
| 中間插入(add(int index, E e)) | O(n) | 雖然插入本身是 O(1)(只需修改前后節(jié)點(diǎn)的指針),但找到插入位置需要 O(n) 的時(shí)間去遍歷。 |
| 頭部/尾部刪除(removeFirst/removeLast) | O(1) | 同頭部/尾部添加,只需修改 first 或 last 的引用。 |
| 中間刪除(remove(int index)) | O(n) | 與中間插入同理,主要時(shí)間消耗在找到待刪除節(jié)點(diǎn)上。 |
| 查找 (contains(Object o)) | O(n) | 必須從頭到尾遍歷整個(gè)鏈表。 |
訪問( get(int index) )
public E get(int index) {
// 檢查傳入索引是否有效
checkElementIndex(index);
// 如果索引有效,調(diào)用 node(int index) 方法找到對應(yīng)索引得節(jié)點(diǎn),并返回找到的節(jié)點(diǎn)的 item 字段
return node(index).item;
}
private void checkElementIndex(int index) {
// 檢查傳入索引是否有效(即在指定范圍內(nèi)),無效則拋出IndexOutOfBoundsException的異常
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
// 判斷索引是否在0到size-1范圍內(nèi)
return index >= 0 && index < size;
}
Node<E> node(int index) {
// 根據(jù)索引與鏈表長度的關(guān)系決定是從頭開始遍歷還是從尾開始遍歷,以提高查找效率
// 如果index小于size的一半(size >> 1 (size/2))
if (index < (size >> 1)) {
Node<E> x = first; // 從頭節(jié)點(diǎn)開始
for (int i = 0; i < index; i++) // 向后遍歷index次
x = x.next;
return x;
// 否則從last節(jié)點(diǎn)開始向前遍歷
} else {
Node<E> x = last; // 從尾節(jié)點(diǎn)開始
for (int i = size - 1; i > index; i--) // 向前遍歷(size-1-index)次
x = x.prev;
return x;
}
}鏈表的get操作時(shí)間復(fù)雜度為O(n) .因?yàn)榭赡苄枰闅v鏈表。且索引必須在 0 到 size-1 范圍內(nèi),否則會(huì)拋出異常。如若在多線程環(huán)境下使用,需要外部同步,因?yàn)樵摲椒ú皇蔷€程安全的。
頭部/尾部添加 (addFirst/addLast)
public void addFirst(E e){
linkFirst(e); // 將實(shí)際元素 e 鏈接到列表開頭
}
public void linkFirst(E e){
// 保存當(dāng)前頭節(jié)點(diǎn)的引用
final Node<E> f = first;
// 創(chuàng)建新的節(jié)點(diǎn),它的 prev 是 null,next 是舊的頭節(jié)點(diǎn) f
final Node<E> newNode = new Node<>(null, e, f);
// 讓 first 指針指向新節(jié)點(diǎn),新節(jié)點(diǎn)成為新的頭
first = newNode;
// 處理邊界情況:如果鏈表原來為空
if (f == null)
// 鏈表為空時(shí),新節(jié)點(diǎn)既是頭也是尾
last = newNode;
else
// 如果鏈表不為空,讓舊頭節(jié)點(diǎn)的 prev 指向新節(jié)點(diǎn)
f.prev = newNode;
size++; // 增加 size 大小
modCount++; // 修改計(jì)數(shù)器,用于快速失敗
}
public void addLast(E e){
linkLast(e); // 將實(shí)際元素 e 鏈接到列表尾部
}
void linkLast(E e){
// 保存當(dāng)前尾部節(jié)點(diǎn)的引用
final Node<E> f = last;
// 創(chuàng)建新的節(jié)點(diǎn),它的 prev 是 舊的尾部節(jié)點(diǎn) l,next 是 null
final Node<E> newNode = new Node<>(l, e, null);
// 讓 last 指針指向新節(jié)點(diǎn),新節(jié)點(diǎn)成為新的尾部
last = newNode;
// 處理邊界情況:如果鏈表原來為空
if (l == null)
// 鏈表為空時(shí),新節(jié)點(diǎn)既是頭也是尾
first= newNode;
else
// 如果鏈表不為空,讓舊尾部節(jié)點(diǎn)的 next 指向新節(jié)點(diǎn)
l.next= newNode;
size++; // 增加 size 大小
modCount++; // 修改計(jì)數(shù)器,用于快速失敗
}addFirst / addLast 方法本身是一個(gè)簡單的包裝器,實(shí)際操作操作是由 linkFirst / linkLast 完成。linkFirst / linkLast 方法通常會(huì)在鏈表數(shù)據(jù)結(jié)構(gòu)中實(shí)現(xiàn),它會(huì)在鏈表頭部/尾部插入一個(gè)新的節(jié)點(diǎn),并將新節(jié)點(diǎn)的 next / prev 引用指向原 頭節(jié)點(diǎn)/尾節(jié)點(diǎn),然后更新鏈表的 頭節(jié)點(diǎn)/尾節(jié)點(diǎn) 為新節(jié)點(diǎn)。
在指定位置插入元素( add(int index,E e) )
public void add(int index, E element){
// 檢查索引是否越界
checkPositionIndex(index);
// 如果插入位置是末尾
if(index == size)
linkLast(element); //調(diào)用linkLast 方法添加至尾部
else
linkBefore(element,node(index)); // 否則,在指定節(jié)點(diǎn)前插入
}
private void checkPositionIndex(int index){
// 檢查傳入索引是否有效(即在指定范圍內(nèi)),無效則拋出IndexOutOfBoundsException異常
if(!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index){
// 判斷索引是否在0到size范圍內(nèi)
return index >= 0 && index <= size;
}
void linkBefore(E e, Node<E> succ){
// 獲取目標(biāo)節(jié)點(diǎn) succ 的前驅(qū)節(jié)點(diǎn)
final Node<E> pred = succ.prev;
// 創(chuàng)建新節(jié)點(diǎn)newNode,設(shè)置其前驅(qū)為pred,后繼為succ
final Node<E> newNode = new Node<>(pred, e, succ);
// 更新succ的前驅(qū)指針指向newNode
succ.prev = newNode;
// 目標(biāo)節(jié)點(diǎn) succ 的前驅(qū)節(jié)點(diǎn) pred 是否為null,判斷是否在鏈表頭部插入
if (pred == null)
first = newNode; // 如果pred為null,說明succ是第一個(gè)節(jié)點(diǎn),需要更新first指針
else
pred.next = newNode; // 否則,更新pred的后繼指針指向newNode
size++; // 增加鏈表大小size
modCount++; // 修改計(jì)數(shù)modCount,用于快速失敗
}add( int index, E e) 的實(shí)現(xiàn)是一個(gè)典型的 “查找 + 插入” 的模式:
- 檢查索引的合法性
- 判斷位置,選擇最高效的路徑(末尾直接插入,其他位置先查找)
- 查找節(jié)點(diǎn)時(shí)采用雙向遍歷優(yōu)化,減少一半的遍歷時(shí)間
- 插入節(jié)點(diǎn)本身是高效的 O(1) 操作,只涉及指針修改
頭部/尾部刪除(removeFirst/removeLast)
public E removeFirst(){
// 獲取鏈表頭節(jié)點(diǎn),并用 final 修飾,確保引用不會(huì)被改變
final Node<E> f = first;
// 檢查鏈表是否為空
//如果為空(first為null),拋出NoSuchElementException異常
if (f == null)
throw new NoSuchElementException();
//調(diào)用unlinkFirst方法實(shí)際執(zhí)行刪除操作,返回被刪除節(jié)點(diǎn)的值
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f){
// 保存要?jiǎng)h除節(jié)點(diǎn)的元素值
final E element = f.item;
// 保存下一個(gè)節(jié)點(diǎn)的引用
final Node<E> next = f.next;
// 清空當(dāng)前節(jié)點(diǎn)的數(shù)據(jù)
// 顯式地將引用置為null可以幫助垃圾回收器更快回收內(nèi)存
f.item = null;
f.next = null; // help GC
// 將鏈表的first指針指向下一個(gè)節(jié)點(diǎn)
first = next;
// 如果下一個(gè)節(jié)點(diǎn)為null,說明鏈表現(xiàn)在為空
if (next == null)
last = null;
// 否則將下一個(gè)節(jié)點(diǎn)的前驅(qū)指針設(shè)為null
else
next.prev = null;
// 更新鏈表大小和修改次數(shù)
size--;
modCount++;
// 返回被刪除節(jié)點(diǎn)的元素值
return element;
}
public E removeLast(){
// 獲取鏈表尾節(jié)點(diǎn),并用 final 修飾,確保引用不會(huì)被改變
final Node<E> l = last;
// 檢查鏈表是否為空
//如果為空(last為null),拋出NoSuchElementException異常
if (l == null)
throw new NoSuchElementException();
//調(diào)用unlinkLast方法實(shí)際執(zhí)行刪除操作,返回被刪除節(jié)點(diǎn)的值
return unlinkLast(l);
}
private E unlinkLast(Node<E> l){
// 保存要?jiǎng)h除節(jié)點(diǎn)的值
final E element = l.item;
// 保存前一個(gè)節(jié)點(diǎn)的引用
final Node<E> prev = l.prev;
// 清空當(dāng)前節(jié)點(diǎn)的數(shù)據(jù)
// 顯式地將引用置為null可以幫助垃圾回收器更快回收內(nèi)存
l.item = null;
l.prev = null; // help GC
// 將鏈表的last指針指向上一個(gè)節(jié)點(diǎn)
last = prev;
// 如果上一個(gè)節(jié)點(diǎn)為null,說明鏈表現(xiàn)在為空
if (prev == null)
first = null;
// 否則將上一個(gè)節(jié)點(diǎn)的后繼指針設(shè)為null
else
prev.next = null;
// 更新鏈表大小和修改次數(shù)
size--;
modCount++;
// 返回被刪除節(jié)點(diǎn)的元素值
return element;
}在執(zhí)行 removeFirst 或 removeLast 方法刪除首尾元素時(shí),會(huì)先獲取目標(biāo)節(jié)點(diǎn)并進(jìn)行非空校驗(yàn)。若節(jié)點(diǎn)為空,則拋出 NoSuchElementException 異常;若節(jié)點(diǎn)存在,則調(diào)用對應(yīng)的 unlinkFirst 或 unlinkLast 方法完成刪除操作。
刪除指定位置元素( remove(int index) )
public E remove(int index){
檢查索引是否有效
checkElementIndex(index);
// 調(diào)用 unlink 方法刪除指定位置元素,并返回刪除元素的值
return unlink(node(index));
}
private void checkElementIndex(int index){
// 檢查傳入索引是否有效(即在指定范圍內(nèi)),無效則拋出IndexOutOfBoundsException的異常
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
// 判斷索引是否在0到size-1范圍內(nèi)
return index >= 0 && index < size;
}
E unlink(Node<E> x){
// assert x != null;
// 保存刪除目標(biāo)元素 x及元素本身的前驅(qū)和后繼引用,使用final修飾,確保引用不會(huì)被修改
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
// 如果前驅(qū)節(jié)點(diǎn)為null,說明要移除的是頭節(jié)點(diǎn),則將first指針指向后繼節(jié)點(diǎn)next
if (prev == null) {
first = next;
// 否則,則將前驅(qū)節(jié)點(diǎn)的next指針指向當(dāng)前節(jié)點(diǎn)的后繼節(jié)點(diǎn)next,并斷開當(dāng)前節(jié)點(diǎn)與前驅(qū)節(jié)點(diǎn)的連接
} else {
prev.next = next;
x.prev = null;
}
// 如果后繼節(jié)點(diǎn)為null,說明要移除的是尾節(jié)點(diǎn),則將last指針指向前驅(qū)節(jié)點(diǎn)prev
if (next == null) {
last = prev;
// 否則,則將后繼節(jié)點(diǎn)的prev指針指向前驅(qū)節(jié)點(diǎn)prev,并斷開當(dāng)前節(jié)點(diǎn)與后繼節(jié)點(diǎn)的連接
} else {
next.prev = prev;
x.next = null;
}
// 將被移除節(jié)點(diǎn)的item設(shè)為null,幫助垃圾回收
x.item = null;
// 減少鏈表的大小size
size--;
// 增加修改計(jì)數(shù)modCount(用于迭代時(shí)的快速失敗機(jī)制)
modCount++;
// 返回被移除節(jié)點(diǎn)的元素值
return element;
}刪除指定位置元素remove(int index)的時(shí)間復(fù)雜度為O(n),原因在于需要先通過node(index)方法定位目標(biāo)節(jié)點(diǎn),這個(gè)定位過程需要線性時(shí)間。雖然實(shí)際刪除操作僅需常數(shù)時(shí)間O(1),但整體時(shí)間復(fù)雜度仍由較耗時(shí)的定位步驟決定。
查找 ( contains(Object o) )
public boolean contains(Object o){
// 調(diào)用 indexOf 查找目標(biāo)元素 o 的索引,并判斷目標(biāo)元素索引不等于 -1
// 目標(biāo)元素索引等于 -1,等式不成立,返回 false; 不等于則返回 true
return indexOf(o) != -1;
}
public int indexOf(Object o){
// 初始化索引記數(shù)器
int index = 0;
// 分別處理查找 null 值和非 null 值的情況,避免出現(xiàn)NullPointerException
// 處理查找null值的情況
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
// 處理查找非null值的情況
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
// 未找到元素,返回-1
return -1;
}contains 方法將“查找元素是否存在”的問題,巧妙地轉(zhuǎn)換成了“查找元素的索引是否為-1”的問題。如若查找目標(biāo)元素 o 在鏈表中存在相同元素,只會(huì)返回第一個(gè)匹配項(xiàng)的索引。
LinkedList的內(nèi)存與空間局部性
- 內(nèi)存開銷:每個(gè)元素都需要一個(gè) Node 對象來包裝。除了存儲(chǔ)元素 E 本身,每個(gè) Node 還額外需要兩個(gè)引用( prev 和 next )的空間。在 64 位JVM (開啟壓縮指針)中,一個(gè) Node 對象頭占 12字節(jié),兩個(gè)引用占 8字節(jié),總共20字節(jié)的開銷,這其中還不包含元素 E 本身的大小。而 ArrayList 只是一個(gè)數(shù)組中的引用。
- 空間局部性差:ArrayList 的元素在內(nèi)存中是連續(xù)存放的,當(dāng) CPU 訪問一個(gè)元素時(shí),可以預(yù)加載其相鄰的元素到緩存中,這就極大地提高了遍歷性能。而 LinkedList 的節(jié)點(diǎn)在內(nèi)存中是散落的,CPU 無法有效的預(yù)取,緩存命中率低,遍歷性能遠(yuǎn)不如 ArrayList。
迭代器:ListIterator
如果說普通的 Iterator 只能讓你在集合中”單向、只讀(主要)“地前進(jìn),那么 ListIterator 就像是給了你一輛可以倒車、隨時(shí)變速、還能邊開邊修路的全地形車。它是 Java 集合專門為 List 接口設(shè)計(jì)的強(qiáng)大迭代器。
普通的 Iterator 由有三個(gè)明顯的局限:
- 單向遍歷:只能從前往后走,不能回頭
- 無法獲取索引:在迭代過程中,存在無法知道當(dāng)前已經(jīng)迭代到了幾個(gè)元素
- 修改能力弱:只能通過 remove() 方法刪除元素,不能添加或替換元素
ListIterator 正是為了克服這些局限而誕生,它允許你:
- 雙向遍歷:向前或向后遍歷
- 獲取索引:精確知道當(dāng)前光標(biāo)的位置
- 強(qiáng)大的修改能力:可以在遍歷過程中添加、刪除、修改元素
概念:光標(biāo)
想象列表元素之間有間隙,光標(biāo)就落在這個(gè)間隙里:
元素(0) 元素(1) 元素(2) 元素(3)
A B C D
| | | | |
^(0) ^(1) ^(2) ^(3) ^(4)
- 初始狀態(tài),光標(biāo)在位置0(即第一個(gè)元素之前)。
- next() 會(huì)跳過光標(biāo)后的元素,并將光標(biāo)向后移動(dòng)。
- previous() 會(huì)跳過光標(biāo)前的元素,并將光標(biāo)向前移動(dòng)。
核心方法
ListIterator 接口繼承 Iterator,并擴(kuò)展以下核心方法:
遍歷與移動(dòng)
| 方法 | 作用 | 光標(biāo)變化 | 返回值 |
|---|---|---|---|
| next() | 返回下一個(gè)元素,并向后移動(dòng)光標(biāo) | index -> index+1 | 下一個(gè)元素 |
| previous() | 返回上一個(gè)元素,并向前移動(dòng)光標(biāo) | index -> index-1 | 上一個(gè)元素 |
| hasNext() | 檢查后面是否還有元素 | 不變 | true / false |
| hasPrevious() | 檢查前面是否還有元素 | 不變 | true / false |
索引查詢
| 方法 | 作用 | 返回值 |
|---|---|---|
| nextIndex() | 返回下一次調(diào)用next()時(shí)返回的元素的索引 | int |
| previousIndex() | 返回下一次調(diào)用previous()時(shí)返回的元素的索引 | int |
修改操作
| 方法 | 作用 | 前置條件 |
|---|---|---|
| set(E e) | 替換最后一次通過 next() 或 previous() 返回的元素 | 必須先調(diào)用 next() 或 previous(),且在這之后沒調(diào)用過 add() 或 remove() |
| add(E e) | 插入一個(gè)元素到光標(biāo)當(dāng)前位置 | 無前置條件 |
| remova() | 刪除最后一次通過 next() 或 previous() 返回的元素 | 必須先調(diào)用 next() 或 previous(),且在這之后沒調(diào)用過 add() 或 remove() |
import java.util.LinkedList;
import java.util.ListIterator;
public class ListIteratorDemo {
public static void main(String[] args){
LinkedList<String> list = new LinkedList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
System.out.println("原始列表: " + list); // [A, B, C, D]
// 獲取 ListIterator,從索引 0 開始
ListIterator<String> iterator = list.listIterator();
// 1. 獲取第一個(gè)元素 "A"
System.out.println("next: " + iterator.next()); // 返回 A, 光標(biāo)移到 A 后
// 光標(biāo)位置: A | B C D
// 2. 在當(dāng)前光標(biāo)位置(即 A 和 B 之間)插入 "New"
iterator.add("New");
System.out.println("add后: " + list); // [A, New, B, C, D]
// 光標(biāo)位置: A New | B C D
// 3. 獲取下一個(gè)元素 "B"
System.out.println("next: " + iterator.next()); // 返回 B, 光標(biāo)移到 B 后
// 光標(biāo)位置: A New B | C D
// 4. 獲取下一個(gè)元素 "C"
System.out.println("next: " + iterator.next()); // 返回 C, 光標(biāo)移到 C 后
// 注意:最后一次返回的是 C,所以接下來的 set 會(huì)替換 C
// 光標(biāo)位置: A New B C | D
// 5. 將剛剛獲取的 "C" 替換為 "Replace_C"
iterator.set("Replace_C");
System.out.println("set后: " + list); // [A, New, B, Replace_C, D]
// 6. 向后回退:獲取前一個(gè)元素
// 此時(shí)光標(biāo)在 C(即Replace_C) 后,前一個(gè)就是 Replace_C
System.out.println("previous: " + iterator.previous()); //返回 Replace_C, 光標(biāo)回退
// 光標(biāo)位置: A New B | Replace_C D
// 7. 刪除剛剛回退獲取的元素 "Replace_C"
iterator.remove();
System.out.println("remove后: " + list); // [A, New, B, D]
}
}注意:
雖然 ListIterator 允許在迭代過程中修改列表(通過它自己的add/remove/set),但如果你在迭代過程中使用了列表對象自身的方法(如list.add())去修改結(jié)構(gòu),迭代器會(huì)立即拋出ConcurrentModificationException。
1.4 CopyOnWriteArrayList
CopyOnWriteArrayList 是 java.util.concurrent 包下的成員,它是線程安全的 ArrayList。它的名字 CopyOnWrite (寫時(shí)復(fù)制)已經(jīng)揭示了它的核心秘密:當(dāng)你需要修改列表時(shí),不要直接在原數(shù)組上改,而是先把原數(shù)組復(fù)制一份,在副本上修改,修改完成后,再讓引用指向新數(shù)組。
核心思想:讀寫分離(讀線程與寫線程互不干擾)
CopyOnWriteArrayList 解決并發(fā)問題的思路與 Vector 或 synchronizedList 截然不同:
- Vector 等悲觀鎖方案:為了保證數(shù)據(jù)一致性,無論是讀還是寫,都要搶同一把鎖。這就導(dǎo)致了讀操作被寫操作阻塞,性能低下。
- CopyOnWriteArrayList 樂觀鎖方案:讀操作采用無鎖設(shè)計(jì),因?yàn)樗粫?huì)修改數(shù)據(jù)(允許讀取到稍舊的數(shù)據(jù))。寫操作則通過加鎖機(jī)制確保原子性,防止多線程并發(fā)復(fù)制。加鎖后,所有寫操作都在新副本上進(jìn)行。
底層原理解析
CopyOnWriteArrayList是一個(gè)設(shè)計(jì)精巧的并發(fā)容器。它用寫操作的昂貴代價(jià)(復(fù)制數(shù)組)換取了讀操作的極致性能(無鎖)。
核心成員變量
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// 序列化版本號(hào),用于實(shí)現(xiàn) Serializable 接口
private static final long serialVersionUID = 8673264195747942595L;
// ReentrantLock 可重入鎖,用來保護(hù)所有修改操作
// transient 修飾,表示在序列化時(shí)不會(huì)保存
final transient ReentrantLock lock = new ReentrantLock();
// 存儲(chǔ)實(shí)際數(shù)據(jù)的數(shù)組
// volatile關(guān)鍵字確??梢娦裕匆粋€(gè)線程修改了數(shù)組,其他線程能立即看到變化
private transient volatile Object[] array;
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
//.............
}注意這里使用了 volatile,這是 CopyOnWriteArrayList 保證線程安全的基石之一,它確保了當(dāng)寫操作把 array 指針指向新數(shù)組時(shí),讀線程能立刻感知到。
讀操作:get(int index) —— 完美無鎖
public E get(int index) {
// 先調(diào)用 getArray() 獲取 array 引用,不加任何鎖
// 然后調(diào)用另一個(gè)重載的get方法,傳入數(shù)組和索引,返回?cái)?shù)組中的元素
return get(getArray(),index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}讀操作性能極佳,即使存在并發(fā)寫操作,讀線程也能無阻塞地并行讀取,且不會(huì)拋出 ConcurrentModificationException。不過需要注意其弱一致性問題:當(dāng)寫線程修改數(shù)據(jù)但尚未更新數(shù)組引用時(shí),讀線程可能讀取到舊數(shù)據(jù)。雖然這不適用于對實(shí)時(shí)性要求極高的系統(tǒng),但對于大多數(shù)業(yè)務(wù)場景(如配置列表、白名單等)來說完全可接受。
寫操作:add(E e) —— 寫時(shí)復(fù)制
public boolean add(E e) {
// 獲取鎖對象
final ReentrantLock lock = this.lock;
// 加鎖,確保線程安全
lock.lock();
try {
// 獲取當(dāng)前數(shù)組
Object[] elements = getArray();
// 獲取當(dāng)前數(shù)組長度
int len = elements.length;
// 創(chuàng)建一個(gè)新數(shù)組,長度為原數(shù)組+1,并拷貝舊數(shù)組
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 在新數(shù)組末尾添加新元素
newElements[len] = e;
// 將新數(shù)組設(shè)置為底層數(shù)組
setArray(newElements);
return true;
} finally {
// 釋放鎖
lock.unlock();
}
}寫操作存在著明顯的性能瓶頸和延遲問題:
- 內(nèi)存消耗:每次寫入都需要完整復(fù)制底層數(shù)組。當(dāng)處理大型數(shù)組(例如存儲(chǔ)10萬個(gè)對象)且頻繁修改時(shí),會(huì)產(chǎn)生巨大的內(nèi)存壓力,容易觸發(fā)Young GC甚至Full GC。
- 性能損耗:盡管僅涉及加鎖和復(fù)制操作,但對于大規(guī)模數(shù)據(jù)(N值較大),Arrays.copyof的時(shí)間復(fù)雜度達(dá)到O(n),導(dǎo)致寫入效率較低。
- 弱一致性:寫入過程中其他線程可能仍在讀取舊數(shù)組,存在短暫的數(shù)據(jù)不一致。CopyOnWriteList返回的迭代器基于"數(shù)組快照"機(jī)制,無法反映后續(xù)的修改。
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListDemo {
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
// 迭代器創(chuàng)建時(shí),拿到的是 [A, B] 的快照
Iterator<String> it = list.iterator();
// 主線程修改了列表
list.add("C");
while(it.hasNext()) {
// 只會(huì)打印 A 和 B,不會(huì)拋出異常,也看不到 C
System.out.println(it.next());
}
}
}2. Set
Set 是一個(gè)無序且唯一的集合。它不允許包含重復(fù)的元素,就像學(xué)校班級(jí)的一個(gè)花名冊,每個(gè)學(xué)生都是獨(dú)一無二的。
核心特點(diǎn):
- 元素唯一:不允許出現(xiàn)重復(fù)元素(最多只能包含一個(gè) null 元素)。
- 無序:無法確保元素的存儲(chǔ)與遍歷順序 。
2.1 HashSet
HashSet 雖然它的名字帶個(gè) "Set",但它實(shí)際上是披著 "Set" 外衣的 HashMap。它是一個(gè)偽裝者,它所有的核心特性——唯一性、快速查找、無序性——全部來自 HashMap。
底層原理解析
理解 HashSet 的核心,就在于理解它是如何利用 HashMap 來實(shí)現(xiàn)唯一性的。
揭秘 HashSet 的本質(zhì)
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
// 序列化版本號(hào)
static final long serialVersionUID = -5024744406713321676L;
// HashSet 的核心,實(shí)際上使用 HashMap 來存儲(chǔ)數(shù)據(jù)
// transient 關(guān)鍵字表示 map 不參與序列化
private transient HashMap<E,Object> map;
// 一個(gè)固定的 Object 對象,作為 HashMap 中所有 value 的占位值
private static final Object PRESENT = new Object();
// 創(chuàng)建 HashSet,底層 HashMap 使用默認(rèn)初始容量(16)和加載因子(0.75)
public HashSet() {
map = new HashMap<>();
}
// 創(chuàng)建一個(gè)包含指定集合元素的 HashSet,根據(jù)集合大小計(jì)算合適的初始容量,確保不會(huì)頻繁擴(kuò)容
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
// 創(chuàng)建 HashSet,允許自定義初始容量和加載因子
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
// 創(chuàng)建 HashSet, 允許自定義初始容量
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
}HashSet 本質(zhì)上是通過 HashMap 實(shí)現(xiàn)的特殊結(jié)構(gòu)。它將所有元素作為 HashMap 的鍵存儲(chǔ),利用 HashMap 鍵不可重復(fù)的特性來實(shí)現(xiàn)去重功能。為了維持 HashMap 的鍵值對結(jié)構(gòu),HashSet 使用一個(gè)名為 PRESENT 的常量作為占位符填充值字段。
核心操作源碼解析
public boolean add(E e) {
// 直接用 map 的 put 方法存儲(chǔ)元素 e
// e 是 key, PRESENT 是固定的 value
return map.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
// 直接調(diào)用 map 的 remove 方法刪除元素 o
return map.remove(o)==PRESENT;
}
public boolean contains(Object o) {
// 直接調(diào)用 map 的 containsKey 方法查找元素 o
return map.containsKey(o);
}HashSet 的所有核心操作(包括 add、remove 和 contains)實(shí)際上都是通過直接調(diào)用底層 HashMap 的對應(yīng)方法來實(shí)現(xiàn)的,這體現(xiàn)了典型的委托模式設(shè)計(jì)。具體實(shí)現(xiàn)細(xì)節(jié)如下:
- 在 HashSet 內(nèi)部維護(hù)了一個(gè) HashMap 實(shí)例作為存儲(chǔ)容器
- 當(dāng)調(diào)用 add(E e) 方法時(shí),實(shí)際上是執(zhí)行 map.put(e, PRESENT) 操作:
- PRESENT 是一個(gè)固定的 Object 對象作為占位值
- 這樣設(shè)計(jì)可以復(fù)用 HashMap 的鍵唯一性特性
- remove(Object o) 方法對應(yīng) map.remove(o) 調(diào)用
- 返回的是是否成功移除,而非被移除的值
- contains(Object o) 方法直接委托給 map.containsKey(o)
這種委托模式的優(yōu)勢在于:
- 代碼復(fù)用:直接利用 HashMap 已經(jīng)實(shí)現(xiàn)的哈希表功能
- 維護(hù)簡單:HashSet 只需要關(guān)注集合接口的實(shí)現(xiàn)
- 性能保證:所有操作的時(shí)間復(fù)雜度與 HashMap 一致
這種設(shè)計(jì)模式在 Java 集合框架中很常見,類似的還有:
- TreeSet 委托給 TreeMap
- LinkedHashSet 委托給 LinkedHashMap
HashSet 的去重機(jī)制:基于 HashMap 的委托實(shí)現(xiàn)
HashSet 的核心特性在于其能夠自動(dòng)過濾重復(fù)元素,而這一機(jī)制并非由其自身復(fù)雜的邏輯實(shí)現(xiàn),而是全權(quán)委托給了底層的 HashMap。正如 JDK 官方源碼注釋所明確指出的:
* This class implements the <tt>Set</tt> interface, backed by a hash table
* (actually a <tt>HashMap</tt> instance). It makes no guarantees as to the
* iteration order of the set; in particular, it does not guarantee that the
* order will remain constant over time. This class permits the <tt>null</tt>
* element.
中文釋義:此類實(shí)現(xiàn)了<tt>Set</tt>接口,以哈希表支持(實(shí)際上是<tt>HashMap</tt>實(shí)例)。它不保證集合的迭代順序;特別是,它不保證順序會(huì)隨時(shí)間保持不變。此類允許<tt>null</tt>元素。
這種“委托模式”在 HashSet 的 add 方法中體現(xiàn)得淋漓盡致。
// Adds the specified element to this set if it is not already present.
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}HashSet 將待存儲(chǔ)的元素 e 作為 Key 傳入 HashMap,并使用靜態(tài)常量對象 PRESENT 作為占位 Value。
去重的具體判定完全依賴于 map.put() 的返回值:
- 當(dāng)方法返回
null時(shí),表示HashMap中原本不存在該 Key,判定為不重復(fù),插入成功。 - 當(dāng)方法返回非
null值(即舊的PRESENT)時(shí),表示HashMap中已存在相同的 Key,判定為重復(fù),插入失敗。
深入分析 map.put() 方法后,我們發(fā)現(xiàn) HashSet 的去重功能實(shí)際上是通過 putVal() 方法實(shí)現(xiàn)的。
public V put(K key, V value) {
// 調(diào)用hash(key)方法計(jì)算鍵的哈希值
// 將哈希值、鍵、值以及其他參數(shù)傳遞給putVal方法
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
// 如果 key 是 null,返回 0;否則將 h 右移 16 位并與自身異或(高位運(yùn)算,減少?zèng)_突)
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// hash: key的哈希值
// key: 要存儲(chǔ)的鍵
// value: 要存儲(chǔ)的值
// onlyIfAbsent: 如果為true,則僅在鍵不存在時(shí)才插入
// evict: 是否在插入后執(zhí)行可能的移除操作(用于LinkedHashMap)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 檢查內(nèi)部數(shù)組table是否為空,如果是則通過resize()初始化
// resize()會(huì)創(chuàng)建一個(gè)新的數(shù)組并返回
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 使用(n-1) & hash計(jì)算key在數(shù)組中的索引位置
// 如果該位置為空,直接創(chuàng)建新節(jié)點(diǎn)放入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 處理哈希沖突
else {
Node<K,V> e; K k;
// 直接匹配:檢查第一個(gè)節(jié)點(diǎn)是否與要插入的key相同
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 記錄當(dāng)前節(jié)點(diǎn)
// 紅黑樹節(jié)點(diǎn)
else if (p instanceof TreeNode)
// 調(diào)用putTreeVal方法處理
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 鏈表節(jié)點(diǎn):遍歷鏈表,查找是否已存在相同的key
else {
for (int binCount = 0; ; ++binCount) {
// 如果遍歷到鏈表尾部還沒找到重復(fù)的,說明元素不重復(fù),掛在尾部
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 當(dāng)鏈表長度達(dá)到TREEIFY_THRESHOLD(默認(rèn)8)時(shí),轉(zhuǎn)換為紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 在鏈表中判斷:Hash值相同 且 (引用相同 或 equals相同)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break; // 找到重復(fù)了,跳出循環(huán),e 指向重復(fù)節(jié)點(diǎn)
p = e;
}
}
// e 不為 null,說明在 Map 中找到了現(xiàn)有的 Key(即元素重復(fù))
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 根據(jù) onlyIfAbsent 決定是否覆蓋(HashSet 永遠(yuǎn)是覆蓋)
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
// 返回舊值,這對 HashSet 意味著 add 失敗
return oldValue;
}
}
// 如果程序能走到這里,說明 e 為 null,是新增節(jié)點(diǎn)
++modCount; //增加修改計(jì)數(shù)
// 檢查是否需要擴(kuò)容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null; // 返回 null,這對 HashSet 意味著 add 成功
}根據(jù)源碼分析,HashSet 判斷重復(fù)元素的機(jī)制可分為兩個(gè)關(guān)鍵步驟:
1. 哈希值比對(hashCode):
- 首先計(jì)算新元素的 hashCode()
- 若該哈希值與集合中現(xiàn)有元素均不相同,則判定為不重復(fù)元素,直接存入
- 若出現(xiàn)哈希值相同的情況(哈希沖突),則進(jìn)入下一步判斷
2. 內(nèi)容比對(equals):
- 當(dāng)哈希值相同時(shí),調(diào)用 equals() 方法進(jìn)行內(nèi)容比較
- 若 equals() 返回 true,則判定為重復(fù)對象,拒絕存入
- 若 equals() 返回 false,則視為不同對象,將其添加到該哈希值對應(yīng)的鏈表(或紅黑樹)結(jié)構(gòu)中
hashCode 與 equals 的“黃金契約”
在 Java 集合框架中,hashCode 與 equals 并非兩個(gè)單獨(dú)的方法,它們之間存在著一種隱含的、必須嚴(yán)格遵守的 “契約”。這種契約不僅是設(shè)計(jì)原則,更是保證對象能被正確存儲(chǔ)(特別是存入 HashSet 或作為 HashMap 的 Key)的法律底線。
簡單來說,這個(gè)契約可以概括為三個(gè)核心規(guī)則:
1、相等的對象必須擁有相等的 Hash 碼。這是契約的基石。如果兩個(gè)對象通過 equals 判定為邏輯上的 “同一者”,那么它們調(diào)用 hashCode 返回的整數(shù)值必須完全一致。
- 反例:如果兩個(gè)對象 equals 為 true,但 hash 值不同,它們在 HashMap 中會(huì)被分發(fā)給不同的“房間"(數(shù)組下標(biāo))。這不僅違反了唯一性邏輯,更會(huì)導(dǎo)致去重機(jī)制徹底失效——集合會(huì)將它們認(rèn)為是兩個(gè)完全不同的陌生人。
2、Hash 碼相同的對象,不一定相等。這是 Hash 碰撞的必然結(jié)果。由于 Hash 算法是將任意長度的輸入映射到固定長度的輸出,沖突是不可避免的。
- 解讀:僅僅因?yàn)閮蓚€(gè)對象落在了同一個(gè)“房間”(Hash 值相同),并不能說明它們是同一個(gè)對象。此時(shí),必須由
equals方法出馬進(jìn)行最后的“人臉識(shí)別”,以決定是覆蓋(相同)還是掛載(不同)到鏈表上。
3、重寫 equals 時(shí),必須重寫 hashCode。這是 Java 開發(fā)的鐵律。如果你為了自定義對象的比較邏輯而重寫了 equals,卻保留了繼承自 Object 的默認(rèn) hashCode(基于內(nèi)存地址計(jì)算),你就親手撕毀了這個(gè)契約。
- 重寫 equals,不重寫 hashCode 造成后果:你將得到一個(gè)看似正常、實(shí)則內(nèi)部混亂的集合。對象可能會(huì)被錯(cuò)誤地覆蓋,或者重復(fù)數(shù)據(jù)無法被剔除。
在 Java.util.HashSet 的源碼注釋明確指出,該類的底層實(shí)現(xiàn)基于哈希表(實(shí)際是一個(gè) HashMap 實(shí)例)。其去重機(jī)制完全依賴于 HashMap 對 Key 的唯一性約束。在 add(E e) 方法實(shí)現(xiàn)中(源碼:return map.put(e, PRESENT) == null),通過判斷 map.put 的返回值來確定元素是否已添加。這就要求存儲(chǔ)對象必須嚴(yán)格遵循 Object 類的契約:當(dāng)兩個(gè)對象相等時(shí),調(diào)用它們的 hashCode 方法必須返回相同的整數(shù)值,這樣才能確保先計(jì)算哈希值再比較對象的去重流程正常運(yùn)作。
總結(jié):hashCode 負(fù)責(zé)宏觀定位(將對象快速分流到對應(yīng)的存儲(chǔ)區(qū)域),而 equals 負(fù)責(zé)微觀甄別(在區(qū)域內(nèi)精確比對對象內(nèi)容)。只有當(dāng)“定位”與“甄別”的標(biāo)準(zhǔn)保持一致時(shí),Java 的哈希集合才能高效、準(zhǔn)確地運(yùn)行。
HashSet 的擴(kuò)容機(jī)制
HashSet 的擴(kuò)容機(jī)制是其保證查詢效率和存儲(chǔ)容量的核心手段。本質(zhì)上,它是底層 HashMap 擴(kuò)容機(jī)制的直接映射。當(dāng)元素?cái)?shù)量增加到一定程度,數(shù)組的長度會(huì)翻倍,并且所有元素的位置會(huì)重新計(jì)算。
擴(kuò)容的“扳機(jī)”(核心參數(shù))
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
{
// 默認(rèn)初始容量(初始容量為2的4次方)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
// 最大容量(最大容量為2的30次方)
static final int MAXIMUM_CAPACITY = 1 << 30; // 1073741824
// 默認(rèn)負(fù)載因子(默認(rèn)的負(fù)載因子為0.75,加載因子 = 哈希表中元素?cái)?shù)量 / 容量)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// .........
}注意:
1、容量必須是2的冪次方,這是 HashMap 設(shè)計(jì)的重要特點(diǎn),其目的:
- 使用位運(yùn)算代替模運(yùn)算,提供性能
- 使哈希分布更均勻
2、這些常量的選擇都是經(jīng)過性能測試和優(yōu)化的結(jié)果:
- 初始容量 16:足夠小以節(jié)省初始內(nèi)存,又足夠大以減少早期擴(kuò)容
- 最大容量 1073741824:這個(gè)限制主要是因?yàn)閿?shù)組在 Java 中最大長度限制,避免內(nèi)存溢出??紤]到實(shí)際使用場景,這個(gè)容量已經(jīng)足夠大了
- 負(fù)載因子 0.75:0.75 是一個(gè)權(quán)衡值,在時(shí)間和空間成本之間尋求平衡,太大會(huì)導(dǎo)致空間浪費(fèi),太小又會(huì)導(dǎo)致哈希沖突增多,影響性能
3、在實(shí)際使用中:
- 如果知道大概要存儲(chǔ)多少元素,可以指定初始容量以減少擴(kuò)容次數(shù)
- 如果內(nèi)存充足且追求查詢性能,可以適當(dāng)降低加載因子
- 如果內(nèi)存緊張,可以適當(dāng)提高加載因子
觸發(fā)判斷:HashMap 的 putVal() 方法
在 HashMap.putVal 中,當(dāng)元素插入成功后,會(huì)檢查是否需要擴(kuò)容。
// java.util.HashMap
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
{
// ......(省略前面的計(jì)算索引和處理鏈表邏輯)
++modCount;
// 插入成功后,size 自增
// 【核心擴(kuò)容判斷】
// size:當(dāng)前元素?cái)?shù)量
// threshold:擴(kuò)容閾值(容量*負(fù)載因子)
if(++size > threshold)
resize(); // 觸發(fā)擴(kuò)容
afterNodeInsertion(evict);
return null;
}當(dāng) HashSet 中的實(shí)際元素個(gè)數(shù)(size) 大于 擴(kuò)容閾值(默認(rèn):16 * 0.75 = 12) 時(shí),就會(huì)觸發(fā)擴(kuò)容。
注意:
判斷標(biāo)準(zhǔn)基于元素?cái)?shù)量而非數(shù)組占用情況。即使數(shù)組某個(gè)位置存在長鏈表,只要元素總數(shù)未超過擴(kuò)容閾值(12),通常不會(huì)觸發(fā)擴(kuò)容。
除非鏈表長度達(dá)到 8 且數(shù)組長度未達(dá)到 64(size < 64 & 鏈表長度 >= 8)時(shí),這通常是因?yàn)?nbsp;數(shù)組容量太小,導(dǎo)致大量元素?cái)D在同一個(gè)桶位(Hash 碰撞嚴(yán)重),而不是因?yàn)檫@些元素本身的 Hash 代碼沖突。
這種情況下,會(huì)先進(jìn)行擴(kuò)容(嘗試打散:長鏈表打散成短鏈表),把 Hash 取模的范圍變大,將原本擠在一起的元素會(huì)被重新分配到不同的索引下。如果擴(kuò)容后,數(shù)組大了,鏈表還是很長,再轉(zhuǎn)紅黑樹進(jìn)行處理。這屬于另一種特殊情況優(yōu)化(預(yù)防性擴(kuò)容優(yōu)化/延遲樹化)。
這實(shí)際上是一個(gè)兩段式的防御策略:
- 低水位防御(數(shù)組 < 64):用擴(kuò)容解決沖突
- 高水位防御(數(shù)組 >= 64):用紅黑樹解決沖突
擴(kuò)容的核心:HashMap 的 resize() 方法
這是擴(kuò)容的靈魂所在。
// java.util.HashMap
final Node<K,V>[] resize(){
// 獲取舊的 table
Node<K,V>[] oldTab = table;
// 獲取舊表格(oldTab)的容量
// 使用三元運(yùn)算符判斷:如果oldTab為null,則oldCap為0;否則為oldTab的長度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 獲取舊的擴(kuò)容閾值(threshold)
// HashMap需要擴(kuò)容的臨界值,通常為容量*加載因子
int oldThr = threshold;
// 聲明新的容量(newCap)和新的閾值(newThr)
int newCap, newThr = 0;
// 如果舊數(shù)組不為空
if (oldCap > 0) {
// 如果舊容量已經(jīng)達(dá)到最大值,不再擴(kuò)容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab; // 返回舊表格
}
// 【核心邏輯】:新容量 = 舊容量左移1位 (即乘以2)
// (newCap = oldCap << 1) 例如 16 -> 32
// 如果計(jì)算的新容量沒達(dá)到最大值且舊容量大于默認(rèn)初始容量,計(jì)算新閾值
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 新閾值也乘以2
newThr = oldThr << 1;
}
// 當(dāng)oldThr > 0時(shí),說明HashMap已經(jīng)被初始化過
// 這種情況下,新的容量(newCap)直接設(shè)置為舊的閾值(oldThr)
// 注釋"initial capacity was placed in threshold"表明初始容量被存儲(chǔ)在了閾值中
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 初始化時(shí),如果用戶沒有指定初始容量(即 threshold 為 0)的情況下
else {
// 設(shè)置默認(rèn)容量和擴(kuò)容閾值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新閾值是 0 (比如使用默認(rèn)構(gòu)造函數(shù)時(shí)),重新計(jì)算
if (newThr == 0) {
// 新的容量:(newCap)乘以加載因子(loadFactor)
float ft = (float)newCap * loadFactor;
// 使用浮點(diǎn)數(shù)ft進(jìn)行中間計(jì)算,避免精度丟失,將計(jì)算結(jié)果轉(zhuǎn)換為整數(shù)賦值給threshold
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; // 更新全局閾值
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 創(chuàng)建新的數(shù)組
table = newTab; // 將新數(shù)組賦值給 table
// 如果舊數(shù)組有數(shù)據(jù),開始遍歷遷移
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 如果當(dāng)前位置有元素
if ((e = oldTab[j]) != null) {
oldTab[j] = null; // 顯式地將引用置為null幫助垃圾回收器更快回收內(nèi)存
// 單個(gè)節(jié)點(diǎn)處理
if (e.next == null)
// 直接計(jì)算新位置并放入
newTab[e.hash & (newCap - 1)] = e;
// 紅黑樹處理
else if (e instanceof TreeNode)
// 調(diào)用split方法進(jìn)行拆分
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 鏈表處理
// 通過維護(hù)兩個(gè)鏈表(lo和hi),將元素分別加入對應(yīng)的鏈表
else {
// loHead/loTail:保持原索引位置的鏈表
// hiHead/hiTail:需要移動(dòng)到"原索引+舊容量"位置的鏈表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 判斷元素的位置是否需要移動(dòng)
// 如果(e.hash & oldCap) == 0,說明元素在新數(shù)組中的位置不變
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 如果(e.hash & oldCap) != 0
// 元素需要移動(dòng)到新位置,新位置 = 原位置 + oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 處理低位鏈表
if (loTail != null) {
loTail.next = null; // 斷開低位鏈表的最后一個(gè)節(jié)點(diǎn)的next指針
newTab[j] = loHead; // 將低位鏈表放在新數(shù)組的原索引位置
}
// 處理高位鏈表
if (hiTail != null) {
hiTail.next = null; // 斷開高位鏈表的最后一個(gè)節(jié)點(diǎn)的next指針
newTab[j + oldCap] = hiHead; // 將高位鏈表放在新數(shù)組的原索引+oldCap位置
}
}
}
}
}
return newTab;
}HashSet 的擴(kuò)容實(shí)際上是執(zhí)行 HashMap 的 resize() 方法。這一過程可細(xì)分為四個(gè)關(guān)鍵步驟:
第一階段:計(jì)算新容量與閾值 (計(jì)算階段)
1、檢查舊容量:
- 獲取當(dāng)前數(shù)組的長度
oldCap - 如果當(dāng)前數(shù)組長度已經(jīng)達(dá)到最大限制(
2^30),則將閾值設(shè)為Integer.MAX_VALUE,不再擴(kuò)容,直接返回
2、確定新容量:
- 如果舊容量大于 0 且未達(dá)上限,新容量 = 舊容量 << 1(即 乘以 2)
- 例如:16 變?yōu)?32,32 變?yōu)?64
3、確定新閾值:
- 新的擴(kuò)容閾值 = 新容量 × 負(fù)載因子(默認(rèn) 0.75)
- 例如:新容量 16 × 0.75 = 12。這意味著當(dāng)存入第 13 個(gè)元素時(shí),會(huì)再次觸發(fā)擴(kuò)容
第二階段:申請新數(shù)組 (內(nèi)存階段)
1、創(chuàng)建新桶數(shù)組:
- 在內(nèi)存中創(chuàng)建一個(gè)新的
Node數(shù)組,大小為第一步計(jì)算出的newCap
2、暫存:
- 此時(shí),新數(shù)組是空的,舊數(shù)組依然存在,里面存著所有數(shù)據(jù)
第三階段:數(shù)據(jù)遷移 (核心遷移階段)
這是擴(kuò)容最耗時(shí)的一步,需要遍歷舊數(shù)組中的每個(gè)元素,并將其放入新數(shù)組。
1、遍歷舊數(shù)組:
- 依次取出每個(gè)位置上的數(shù)據(jù)(可能是 null,可能是單個(gè)節(jié)點(diǎn),可能是鏈表,也可能是紅黑樹)
2、分類處理:
- 單個(gè)元素:直接計(jì)算新索引,放入新數(shù)組
- 紅黑樹:將樹拆分為低位樹和高位樹,如果節(jié)點(diǎn)數(shù)太少(<=6),會(huì)退化回鏈表
- 鏈表:利用
(e.hash & oldCap)判斷,如果(e.hash & oldCap) == 0,元素在擴(kuò)容后,索引不變(仍在j位置);如果(e.hash & oldCap) != 0,元素在擴(kuò)容后,移動(dòng)到j + oldCap位置。這樣就將一個(gè)長鏈表拆分成了兩個(gè)短鏈表,并保持了原有順序(尾插法)
第四階段:引用切換與清理 (收尾階段)
1、切換引用:
- 將
HashMap內(nèi)部維護(hù)的table屬性指向新的數(shù)組
2、置空舊數(shù)組:
- 原數(shù)組的引用被丟棄,等待 JVM 垃圾回收器(GC)進(jìn)行回收
簡易流程圖:
開始
↓
檢查元素個(gè)數(shù) > 閾值 ?
↓ (是)
計(jì)算新容量 (x2) 和新閾值
↓
創(chuàng)建一個(gè)更大的新數(shù)組 (2倍大小)
↓
遍歷舊數(shù)組的每個(gè)桶位
↓
判斷數(shù)據(jù)類型?
├─ 單個(gè)節(jié)點(diǎn) → 直接移到新位置
├─ 紅黑樹 → 拆樹/移樹
└─ 鏈表 → (hash & oldCap) 判斷
├─ == 0 → 留在原位
└─ != 0 → 移到 (原位 + 舊容量)
↓
將 table 指向新數(shù)組
↓
擴(kuò)容結(jié)束
在 JDK 1.7 版本中,擴(kuò)容時(shí)需要重新計(jì)算每個(gè)元素的索引位置(index = hash & (newCap - 1)),并且采用頭插法會(huì)導(dǎo)致鏈表順序反轉(zhuǎn)。而在 JDK 1.8 版本中,它不需要重新計(jì)算 Hash,也不需要通過 & (newCap - 1) 重新計(jì)算索引,只需要判斷一個(gè)位( if ((e.hash & oldCap) == 0) ),就能決定元素是留在原地,還是移動(dòng)到 原位置 + 原容量 的地方。這極大地提升了擴(kuò)容效率。
為了防止 Hash 沖突嚴(yán)重導(dǎo)致鏈表變成“長龍”(查詢退化成 O(n)),JDK 1.8 引入了紅黑樹優(yōu)化方案:
- 觸發(fā)條件:鏈表長度>8且數(shù)組容量>64時(shí)自動(dòng)轉(zhuǎn)為紅黑樹
- 退化機(jī)制:元素?cái)?shù)量≤6時(shí)自動(dòng)恢復(fù)為鏈表結(jié)構(gòu),節(jié)省存儲(chǔ)空間
注意:如果數(shù)組容量沒到 64,只是鏈表長了(Hash 沖突極多),HashSet 會(huì)選擇優(yōu)先擴(kuò)容數(shù)組,而不是轉(zhuǎn)樹。因?yàn)閿U(kuò)容能打散鏈表。
2.2 LinkedHashSet
LinkedHashSet 是 Java 集合中 Set 接口的一個(gè)重要實(shí)現(xiàn)類,位于 java.util 包中。它是 HashSet 的子類,在具有哈希表查找效率的同時(shí),結(jié)合了鏈表的結(jié)構(gòu)來維護(hù)插入順序。
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
// ..........
}一句話概括:LinkedHashSet 是一種有序集合(這是它與 HashSet 的主要區(qū)別)。雖然需要維護(hù)額外鏈表來保證順序?qū)е滦阅苈缘陀?HashSet,但它在增刪操作上依然保持高效。與所有 Set 實(shí)現(xiàn)一樣,LinkedHashSet 不允許存儲(chǔ)重復(fù)元素。
- 繼承關(guān)系:LinkedHashSet 繼承 HashSet
- 數(shù)據(jù)結(jié)構(gòu):其底層實(shí)現(xiàn)是一個(gè) LinkedHashMap(委托給 LinkedHashMap)
- 哈希表:負(fù)責(zé)根據(jù)元素的 hashCode 存儲(chǔ)數(shù)據(jù),保證元素的唯一性和快速查詢(O(1)時(shí)間復(fù)雜度)
- 雙向鏈表:負(fù)責(zé)維護(hù)元素的插入順序(或訪問順序),保證遍歷時(shí)的有序性
底層原理解析:
在 Java 集合框架江湖中,LinkedHashSet 是一個(gè)非常奇特的存在。它是 Java 源碼設(shè)計(jì)中 “組合優(yōu)于繼承” 和 “代碼復(fù)用” 的一個(gè)精彩案例。
源碼初印象:極簡主義
如果你去翻閱 JDK 源碼,你會(huì)發(fā)現(xiàn)一個(gè)驚人的事實(shí):LinkedHashSet 類的源碼極其簡短,所有的核心邏輯竟然都在它的父類 HashSet 里。
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
// 序列化/反序列化的版本控制ID
private static final long serialVersionUID = -2851667679971038690L;
// 創(chuàng)建一個(gè)指定初始容量和加載因子的空LinkedHashSet
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
// 創(chuàng)建一個(gè)指定初始容量,默認(rèn)加載因子(0.75)的空LinkedHashSet
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
// 創(chuàng)建一個(gè)默認(rèn)初始容量(16)和默認(rèn)加載因子(0.75)的空LinkedHashSet
public LinkedHashSet() {
super(16, .75f, true);
}
// 創(chuàng)建一個(gè)包含指定集合所有元素的LinkedHashSet,初始容量設(shè)為max(2*c.size(), 11)
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
// 可分割迭代器
@Override
public Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
}
}整個(gè)類只有四個(gè)構(gòu)造函數(shù),所有的構(gòu)造函數(shù)都調(diào)用了 super(.... , true),沒有任何 add、remove 或 iterator 方法的實(shí)現(xiàn)。這意味著它完全復(fù)用了父類 HashSet 的邏輯。
源碼核心:HashSet 中的 “后門”
為了支持 LinkedHashSet,HashSet 在源碼中預(yù)留了一個(gè)特殊的構(gòu)造方法。這個(gè)方法通常是 protected 或者 default(包級(jí)私有)的,專門供子類或同包下的類使用。
// java.util.HashSet
// 底層存儲(chǔ) Map
private transient HashMap<E,Object> map;
/**
* 構(gòu)造一個(gè)新的空的鏈接哈希集合實(shí)例。
* (這個(gè)構(gòu)造函數(shù)只給 LinkedHashSet 訪問,dummy 沒有實(shí)際意義)
*/
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
// 注意這里:new 的不是 HashMap,而是 LinkedHashMap!
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}LinkedHashSet 繼承自 HashSet。在構(gòu)造過程中,它會(huì)調(diào)用 HashSet 的特殊構(gòu)造函數(shù) HashSet(int initialCapacity, float loadFactor, boolean dummy)。這個(gè)構(gòu)造函數(shù)初始化了父類 HashSet 的 map 成員變量,但并非創(chuàng)建普通的 HashMap,而是實(shí)例化了一個(gè) LinkedHashMap。因此,LinkedHashSet 本質(zhì)上仍是 HashSet,只是其內(nèi)部 map 變量實(shí)際指向的是 LinkedHashMap 實(shí)例。
LinkedHashSet 如何保證其有序性
既然 LinkedHashSet 底層是 LinkedHashMap,那么它的有序性就完全由 LinkedHashMap 決定。
在 HashMap 中,每個(gè)節(jié)點(diǎn)都是 Node<K,V> (數(shù)組+鏈表/紅黑樹)。而在 LinkedHashMap 中,節(jié)點(diǎn)被替換為了 Entry<K,V>,它繼承自 HashMap.Node,并增加了兩個(gè)指針。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
{
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 存儲(chǔ)鍵的哈希值,用于快速定位
final K key; // 存儲(chǔ)鍵,用final修飾表示鍵不可變
V value; // 存儲(chǔ)值,可以被修改
Node<K,V> next; // 指向下一個(gè)節(jié)點(diǎn)的引用,用于處理哈希沖突
//........
}
}public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
{
// 靜態(tài)內(nèi)部類
// 繼承自 HashMap.Node,說明其具備了 HashMap 中節(jié)點(diǎn)的所有基本特性
static class Entry<K,V> extends HashMap.Node<K,V>{
// 新增的兩個(gè)指針,用于維護(hù)雙向鏈表
// before(前驅(qū)指針):指向前一個(gè)節(jié)點(diǎn)
// after(后繼指針):指向后一個(gè)節(jié)點(diǎn)
Entry<K,V> before, after;
// 構(gòu)造函數(shù)
// 接收四個(gè)參數(shù):hash值、鍵、值和下一個(gè)節(jié)點(diǎn)
// 通過 super() 調(diào)用父類 HashMap.Node 的構(gòu)造函數(shù),確保新建節(jié)點(diǎn)具備 HashMap 節(jié)點(diǎn)的所有基本屬性
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
}這意味著,LinkedHashMap 中的每個(gè)節(jié)點(diǎn)都同時(shí)處在 “兩個(gè)鏈表” 中:
- 哈希桶鏈表(單向):用于解決 hash 沖突,保證快速查找(復(fù)用了 HashMap 中的 next 指針)
- 雙向鏈表(雙向):用于維護(hù)順序(新增了前驅(qū)指針 before 和 后繼指針 after)
哈希表數(shù)組 雙向鏈表 (維護(hù)順序)
+----------+ +--------<--------+--------->----------+
| Index 0 | -----> | Entry A | Entry B |
+----------+ | (before:null) | (before:A) |
| (after:B) | (after:null) |
+----------+ +----------------+-------------------+
| Index 1 | -----> | Entry C
+----------+ | (before:null) <-- 注意:如果C是最后插入的
| (after:null) 鏈表指針會(huì)重新連接
核心成員變量:定義 “有序” 的規(guī)則
LinkedHashMap 提供了兩種有序模式:
- 插入順序:元素按照添加的先后順序排列,先插入的元素在前
- 訪問順序(LRU 機(jī)制):最近被訪問(get/put)的元素會(huì)被移至末尾,實(shí)現(xiàn)最近最少使用算法
這兩種模式通過一個(gè) boolean 類型的標(biāo)志位進(jìn)行切換控制。
// java.util.linkedHashMap // 雙向鏈表的頭節(jié)點(diǎn) // transient關(guān)鍵字表示這個(gè)字段不會(huì)被序列化 transient LinkedHashMap.Entry<K,V> head; // 雙向鏈表的尾節(jié)點(diǎn) // transient關(guān)鍵字表示這個(gè)字段不會(huì)被序列化 transient LinkedHashMap.Entry<K,V> tail; // 這是一個(gè) final 字段,決定了 LinkedHashMap 的迭代順序 // 當(dāng)為 true 時(shí),按照訪問順序迭代(最近訪問的放在最后) // 當(dāng)為 false 時(shí),按照插入順序迭代 (默認(rèn)為 false) final boolean accessOrder;
節(jié)點(diǎn)上鏈
既然數(shù)據(jù)結(jié)構(gòu)變了,那么在插入新節(jié)點(diǎn)時(shí),LinkedHashMap 必然要維護(hù)這個(gè)雙向鏈表。
當(dāng) HashMap 調(diào)用 put 添加元素時(shí),發(fā)現(xiàn)計(jì)算出的哈希位置當(dāng)前沒有數(shù)據(jù),需要?jiǎng)?chuàng)建一個(gè)新節(jié)點(diǎn)。此時(shí)會(huì)調(diào)用 LinkedHashMap 重寫的 newNode 方法。
// java.util.LinkedHashMap
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
// 創(chuàng)建新節(jié)點(diǎn) p
LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e);
// 調(diào)用鏈接方法,把新增節(jié)點(diǎn) p 掛在鏈表尾部
linkNodeLast(p);
return p;
}
private void linkNodeLast(LinkedHashMap。Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail; // 保存當(dāng)前尾節(jié)點(diǎn)
tail = p; // 將新節(jié)點(diǎn)p設(shè)置為新的尾節(jié)點(diǎn)
if (last == null) // 如果鏈表為空
head = p; // 將新節(jié)點(diǎn)同時(shí)設(shè)為頭節(jié)點(diǎn)
else { // 如果鏈表不為空
p.before = last; // 將新節(jié)點(diǎn)的前驅(qū)指針指向原尾節(jié)點(diǎn)
last.after = p; // 將原尾節(jié)點(diǎn)的后繼指針指向新節(jié)點(diǎn)
}
}HashMap 在執(zhí)行 map.put 操作時(shí),首先計(jì)算鍵的哈希值來確定數(shù)組下標(biāo)。LinkedHashMap 則通過重寫 newNode 方法,在節(jié)點(diǎn)創(chuàng)建過程中生成特殊的 Entry 對象,并調(diào)用 linkNodeLast 方法將該 Entry 通過 before 和 after 指針鏈接到雙向鏈表末尾。無論哈希值如何分布,雙向鏈表始終按照插入順序進(jìn)行擴(kuò)展。
訪問順序與 LRU 實(shí)現(xiàn)
LinkedHashMap 內(nèi)部維護(hù)了一個(gè)雙向鏈表,這個(gè)鏈表的順序由 accessOrder 參數(shù)控制:
- accessOrder = false (默認(rèn)):按照插入順序排序(FIFO)
- accessOrder = true:按照訪問順序排序(LRU)。這就是 LRU 的開關(guān)
// java.util.LinkedHashMap
public V get(Object key) {
// 聲明一個(gè) Node 類型的變量 e,用于存儲(chǔ)找到的節(jié)點(diǎn)
Node<K,V> e;
// 調(diào)用 getNode 方法查找節(jié)點(diǎn)
// 如果找不到對應(yīng)節(jié)點(diǎn)(e為null),直接返回null
if ((e = getNode(hash(key), key)) == null)
return null;
// 如果accessOrder為true,表示按照訪問順序排序
if (accessOrder)
// 調(diào)用 afterNodeAccess(e) 方法將最近訪問的節(jié)點(diǎn)移到鏈表末尾
afterNodeAccess(e);
// 返回找到的節(jié)點(diǎn)的value值
return e.value;
}
void afterNodeAccess(Node<K,V> e) {
LinkedHashMap.Entry<K,V> last;
// 檢查是否需要重新排序(accessOrder為true)且節(jié)點(diǎn)不在尾部
if (accessOrder && (last = tail) != e) {
// 保存當(dāng)前節(jié)點(diǎn)的前后節(jié)點(diǎn)引用
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// 將p的after置為null,因?yàn)樗鼘⒊蔀樾碌奈补?jié)點(diǎn)
p.after = null;
// 如果b為null,說明p是頭節(jié)點(diǎn),更新頭節(jié)點(diǎn)為a;否則,將b的after指向a
if (b == null)
head = a;
else
b.after = a;
// 如果a不為null,將a的before指向b;否則,更新last為b
if (a != null)
a.before = b;
else
last = b;
// 如果last為null,說明鏈表為空,將頭節(jié)點(diǎn)head設(shè)置為p;否則,將p連接到last之后
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
// 更新尾節(jié)點(diǎn)tail為p
tail = p;
// 增加修改計(jì)數(shù)器
++modCount;
}
}當(dāng) accessOrder 設(shè)置為 true 時(shí),每次執(zhí)行 get(key) 操作(即訪問數(shù)據(jù)),LinkedHashMap 會(huì)在內(nèi)部自動(dòng)調(diào)用 afterNodeAccess 方法,將被訪問的節(jié)點(diǎn)移至雙向鏈表尾部。
這就好比排隊(duì)。
- 插入順序:來了一個(gè)人,站到隊(duì)尾,以后不管他怎么說話,位置不動(dòng)
- 訪問順序:來了一個(gè)人,站到隊(duì)尾。一旦這個(gè)人和你說了句話(訪問了
get),他立馬插隊(duì)跑到隊(duì)尾去
隊(duì)首的永遠(yuǎn)是最久沒有被訪問的元素。這就是 LRU(Least Recently Used)緩存淘汰策略的基礎(chǔ)!
工作流程圖如下:

數(shù)據(jù)結(jié)構(gòu)示意:
[ 最近最少使用 ] <-----> [ ...中間數(shù)據(jù)... ] <-----> [ 最近剛使用 ]
(Head) (Tail)
↑ ↑
|--- 當(dāng)緩存滿時(shí),刪除 Head(最久未用) |--- 新訪問或插入的數(shù)據(jù)移到這
- 鏈表頭部:存放的是最久未被訪問的數(shù)據(jù)
- 鏈表尾部:存放的是最近剛被訪問的數(shù)據(jù)
- 淘汰策略:當(dāng)緩存數(shù)量達(dá)到預(yù)設(shè)上限時(shí),刪除鏈表頭部的節(jié)點(diǎn)
注意:
- 線程安全:LinkedHashMap 并非線程安全類,在多線程環(huán)境下使用時(shí)需進(jìn)行外部同步控制
- 性能考慮:每次訪問節(jié)點(diǎn)都可能觸發(fā)鏈表的調(diào)整操作,這在頻繁訪問時(shí)會(huì)有一定的性能開銷。如果不需要按訪問順序排序,建議將
accessOrder設(shè)為false,以避免不必要的開銷 - 內(nèi)存占用:LinkedHashMap 在 HashMap 的基礎(chǔ)上額外維護(hù)了一個(gè)雙向鏈表結(jié)構(gòu),因此會(huì)消耗更多的內(nèi)存空間。??????
迭代器:有序
HashMap 的迭代器通過按數(shù)組索引順序(0 到 15)遍歷來訪問元素,因此其遍歷順序是不確定的。相比之下,LinkedHashMap 通過重寫迭代器邏輯,實(shí)現(xiàn)了有序遍歷。
// java.util.LinkedHashMap
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next; // 指向下一個(gè)要遍歷的節(jié)點(diǎn)
LinkedHashMap.Entry<K,V> current; // 當(dāng)前正在處理的節(jié)點(diǎn)
int expectedModCount; // 預(yù)期的修改次數(shù),用于快速失敗(fail-fast)機(jī)制
LinkedHashIterator() {
next = head; // 從雙向鏈表的頭節(jié)點(diǎn)開始
expectedModCount = modCount; // 記錄創(chuàng)建迭代器時(shí)的修改次數(shù)
current = null; // 當(dāng)前節(jié)點(diǎn)初始化為null
}
// 判斷是否還有下一個(gè)元素
public final boolean hasNext() {
return next != null;
}
final LinkedHashMap.Entry<K,V> nextNode(){
LinkedHashMap.Entry<K,V> e = next;
// 檢查是否有并發(fā)修改
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
// 更新當(dāng)前節(jié)點(diǎn)
current = e;
// 移動(dòng)到下一個(gè)節(jié)點(diǎn)(通過after指針)
next = e.after;
return e;
}
}迭代器無需關(guān)注數(shù)組的存儲(chǔ)位置,只需沿著 after 指針依次訪問即可。由于 after 指針已按順序維護(hù),遍歷結(jié)果自然保持有序。
LinkedHashMap 通過"雙鏈路"機(jī)制,巧妙實(shí)現(xiàn)了 O(1) 查詢性能與可預(yù)測遍歷順序的完美結(jié)合。
2.3、TreeSet
TreeSet 是 Java 集合框架中 Set 接口的一個(gè)實(shí)現(xiàn)類,它位于 java.util 包中。與 HashSet 基于哈希表實(shí)現(xiàn)不同,TreeSet 是基于紅黑樹(Red-Black Tree)數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)的。
核心特點(diǎn):
- 有序性:TreeSet 中的元素會(huì)按照自然順序或者自定義比較器進(jìn)行排序
- 唯一性:作為 Set 的實(shí)現(xiàn),它不包含重復(fù)元素
- 非線程安全:與大多數(shù)集合一樣,TreeSet 不是線程安全的。如果需要在多線程環(huán)境下使用,必須進(jìn)行外部同步
注意:LinkedHashSet 和 TreeSet 雖然都是有序集合,但它們的排序機(jī)制存在本質(zhì)區(qū)別。LinkedHashSet 嚴(yán)格維護(hù)元素的插入順序,遍歷時(shí)會(huì)按照 A→B→C 的順序輸出,與元素插入時(shí)間完全一致。而 TreeSet 則依據(jù)元素的自然排序規(guī)則(如字母順序或數(shù)值大?。┳詣?dòng)排序(使用自然排序,元素不能類型不能比較 null,否則會(huì)拋出 NullPointerException),無論插入順序如何,遍歷結(jié)果始終是 A→B→C 這樣的有序序列。
代碼示例如下:
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;
// 插入順序 vs 排序順序
public class OrderDemo {
public static void main(String[] args) {
String[] letters = {"C", "A", "B", "A"};
// LinkedHashSet: 維持插入順序
Set<String> linkedSet = new LinkedHashSet<>(Arrays.asList(letters));
System.out.println("LinkedHashSet: " + linkedSet);
// 輸出: [C, A, B] -> 去重了,且 C 在最前面,因?yàn)樗堑谝粋€(gè)插入的
// TreeSet: 按字典序排序
Set<String> treeSet = new TreeSet<>(Arrays.asList(letters));
System.out.println("TreeSet: " + treeSet);
// 輸出: [A, B, C] -> 去重了,且按 A, B, C 排列,完全忽略了插入順序
}
}TreeSet 的繼承體系
類圖結(jié)構(gòu):
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
// ..........
}- AbstractSet:提供了 Set 接口的骨干實(shí)現(xiàn),減少了實(shí)現(xiàn)此接口所需的工作量
- NavigableSet:擴(kuò)展了 SortedSet,提供導(dǎo)航方法(如 lower、floor、ceiling、higher 等),允許對集合進(jìn)行最接近匹配的搜索
- Cloneable:支持對象克隆
- Serializable:支持序列化
底層原理解析
TreeSet 是 Java 集合框架中著名的 “偽裝者”。打開 JDK 源碼,你會(huì)發(fā)現(xiàn) TreeSet.java 只有 300 多行代碼,且沒有任何核心算法。它的一切行為,都直接委托給了一個(gè)內(nèi)部私有的 NavigableMap 對象,而這個(gè)對象運(yùn)行時(shí)通常是 TreeMap 的實(shí)例。
核心成員變量
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
// TreeSet 內(nèi)部使用一個(gè) NavigableMap (實(shí)際是 TreeMap) 來存儲(chǔ)元素
private transient NavigableMap<E,Object> m;
// 這個(gè)是一個(gè)虛擬值,用于在 TreeMap 中關(guān)聯(lián)集合的元素
// 因?yàn)?TreeMap 存儲(chǔ)的是鍵值對,而 TreeSet 只需要鍵
private static final Object PRESENT = new Object();
//.............
}- m:這是 TreeSet 的核心。NavigableMap 是一個(gè)接口,TreeSet 默認(rèn)使用其實(shí)現(xiàn)類 TreeMap。TreeSet 的元素實(shí)際是作為這個(gè) NavigableMap 的 Key 存儲(chǔ)
- PRESENT:這是一個(gè)靜態(tài)的 Object 對象,作為所有的 TreeMap 條目的 value。因?yàn)?TreeSet 只關(guān)心元素本身(即 Key),所以所有的 value 都指向同一個(gè) PRESENT 對象
底層數(shù)據(jù)結(jié)構(gòu)(TreeMap.Entry<K,V>)
// java.util.TreeMap
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left; // 左子節(jié)點(diǎn)
Entry<K,V> right; // 右子節(jié)點(diǎn)
Entry<K,V> parent; // 父節(jié)點(diǎn)
boolean color = BLACK; // 節(jié)點(diǎn)顏色,默認(rèn)為黑
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
// .........
} - Parent 指針:這是一個(gè)雙向鏈表結(jié)構(gòu)的體現(xiàn)。不僅知道子節(jié)點(diǎn),還能回溯父節(jié)點(diǎn)。這使得 TreeSet 能夠輕松實(shí)現(xiàn) lower()(前驅(qū))、higher()(后繼)等導(dǎo)航操作。
- Color 屬性:紅黑樹通過顏色約束(根黑、葉黑、紅不相連、黑高相同)來保證平衡,從而將查詢的復(fù)雜度控制在 O(log n)。
構(gòu)造方法
TreeSet 提供了多個(gè)構(gòu)造方法,它們主要區(qū)別在于如何初始化內(nèi)部的 m(TreeMap)以及是否提供自定義比較器。
// java.util.TreeSet
// 無參構(gòu)造器,使用自然排序
public TreeSet() {
this(new TreeMap<E,Object>());
}
// 帶比較器的構(gòu)造器,使用自定義排序
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
// 構(gòu)造一個(gè)包含指定集合元素的新 TreeSet,使用自然排序
public TreeSet(Collection<? extends E> c) {
this();
addAll(c);
}
// 構(gòu)造一個(gè)包含指定有序集合元素的新 TreeSet,按照相同順序
public TreeSet(SortedSet<E> s) {
this(s.comparator());
addAll(s);
}
// 包訪問權(quán)限的構(gòu)造器,直接傳入一個(gè) NavigableMap
// 主要用于子類或者特定場景,比如 headSet, subSet, tailSet 等返回的子集
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}所有公共構(gòu)造方法最終都會(huì)通過 this(new TreeMap<>(...)) 或 this(m) 來初始化內(nèi)部的 NavigableMap 對象 m。若未指定 Comparator,TreeMap 將默認(rèn)采用元素的自然排序(此時(shí)元素需實(shí)現(xiàn) Comparable 接口,如果元素類未實(shí)現(xiàn)該接口,會(huì)拋出 ClassCastException)。
核心流程:add 操作的源碼級(jí)復(fù)現(xiàn)
要理解add方法,首先需要掌握這些規(guī)則——源碼中的fixAfterInsertion(插入修復(fù))機(jī)制正是為了在規(guī)則被破壞后重新修復(fù)它們。
紅黑樹的五大鐵律:
- 節(jié)點(diǎn)非黑即紅
- 根節(jié)點(diǎn)是黑色
- 所有葉子節(jié)點(diǎn)(NIL 空節(jié)點(diǎn))是黑色
- 紅色節(jié)點(diǎn)的兩個(gè)子節(jié)點(diǎn)必須是黑色(即不能有連續(xù)的兩個(gè)紅色節(jié)點(diǎn))
- 從任一節(jié)點(diǎn)到其每個(gè)葉子節(jié)點(diǎn)的所有路徑都包含相同數(shù)目的黑色節(jié)點(diǎn)
add() 方法:排序規(guī)則
// java.util.TreeSet
public boolean add(E e) {
// 直接調(diào)用底層 TreeMap 的 put 方法
// 如果之前不存在該 key,則 put 返回 null,add 返回 true (添加成功)
// 如果之前已存在該 key (根據(jù)排序規(guī)則),則 put 返回舊 value,add 返回 false (添加失敗,元素已存在)
return m.put(e,PRESENT) == null;
}這是最常用的操作。表面上看是往集合中添加?xùn)|西,底層實(shí)際上是在操作紅黑樹。
// java.util.TreeMap
public V put(K key, V value) {
Entry<K,V> t = root; // 從根節(jié)點(diǎn)開始
// 特殊情況:樹是空的
// 此時(shí) new Entry 的 parent 是 null,默認(rèn) color 是 BLACK
// (Entry構(gòu)造器中 color 默認(rèn) BLACK,但在 insert 修正里可能會(huì)變)
if (t == null) {
compare(key, key); // 類型檢查,確保 key 是可比較的
root = new Entry<>(key, value, null); // 創(chuàng)建根節(jié)點(diǎn)
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
Comparator<? super K> cpr = comparator;
// 如果有自定義比較器,使用比較器進(jìn)行比較
if (cpr != null) {
// 遍歷樹找到合適的插入位置
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0) // 小,往左走
t = t.left;
else if (cmp > 0) // 大,往右走
t = t.right;
else // 相等,替換值, TreeSet 這里返回 PRESENT
return t.setValue(value);
} while (t != null);
}
// 如果沒有自定義比較器,使用鍵的自然排序
else {
// 鍵不能為null 且 鍵必須實(shí)現(xiàn)Comparable接口
if (key == null)
throw new NullPointerException();
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
// 創(chuàng)建新節(jié)點(diǎn)并插入到合適位置
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
// 調(diào)用fixAfterInsertion維護(hù)紅黑樹的平衡
fixAfterInsertion(e);
size++;
modCount++;
return null;
}插入新節(jié)點(diǎn)(默認(rèn)紅色)后,可能違反了“紅色節(jié)點(diǎn)不能相連”的規(guī)則。JDK 源碼通過變色和旋轉(zhuǎn)來修復(fù)。
// java.util.TreeMap
private void fixAfterInsertion(Entry<K,V> x) {
// 將新插入的節(jié)點(diǎn)設(shè)為紅色
x.color = RED;
// 當(dāng) x 不是根節(jié)點(diǎn),且 x 的父節(jié)點(diǎn)是紅色時(shí)(違反規(guī)則4),需要循環(huán)修復(fù)
while (x != null && x != root && x.parent.color == RED) {
// 判斷父節(jié)點(diǎn)是祖父節(jié)點(diǎn)的左孩子還是右孩子
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
Entry<K,V> y = rightOf(parentOf(parentOf(x))); // y 是叔節(jié)點(diǎn)
// 如果叔節(jié)點(diǎn)是紅色
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK); // 父節(jié)點(diǎn)變黑
setColor(y, BLACK); // 叔節(jié)點(diǎn)變黑
setColor(parentOf(parentOf(x)), RED); // 祖父節(jié)點(diǎn)變紅
x = parentOf(parentOf(x)); // 指針上移,以祖父為當(dāng)前節(jié)點(diǎn)繼續(xù)循環(huán)
} else {
// 如果叔節(jié)點(diǎn)是黑色,且當(dāng)前節(jié)點(diǎn)是右孩子
if (x == rightOf(parentOf(x))) {
x = parentOf(x); // 左旋(以父節(jié)點(diǎn)為中心)
rotateLeft(x);
}
// 叔節(jié)點(diǎn)是黑色,且當(dāng)前節(jié)點(diǎn)是左孩子
setColor(parentOf(x), BLACK); // 父節(jié)點(diǎn)變黑
setColor(parentOf(parentOf(x)), RED); // 祖父節(jié)點(diǎn)變紅
rotateRight(parentOf(parentOf(x))); // 右旋(以祖父節(jié)點(diǎn)為中心)
}
} else {
// 父節(jié)點(diǎn)是祖父的右孩子,邏輯同上,只是左右互換(鏡像情況)
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
// 確保根節(jié)點(diǎn)始終是黑色(符合規(guī)則2:根節(jié)點(diǎn)是黑色)
root.color = BLACK;
}當(dāng)插入新節(jié)點(diǎn)導(dǎo)致“雙紅沖突”(父節(jié)點(diǎn)與子節(jié)點(diǎn)同為紅色)時(shí),遵循以下處理邏輯:
1、叔節(jié)點(diǎn)為紅——染色上溯
- 操作:將父節(jié)點(diǎn)與叔節(jié)點(diǎn)染為黑色,祖父節(jié)點(diǎn)染為紅色
- 邏輯:相當(dāng)于將紅色沖突“上傳”給祖父,將祖父視為新節(jié)點(diǎn)繼續(xù)向上循環(huán)判斷
2、叔節(jié)點(diǎn)為黑 + 當(dāng)前為右子——左旋重定向
- 操作:以父節(jié)點(diǎn)為中心進(jìn)行左旋,將當(dāng)前節(jié)點(diǎn)指向原父節(jié)點(diǎn)
- 邏輯:將“內(nèi)側(cè)”沖突轉(zhuǎn)換為“外側(cè)”,以便統(tǒng)一處理。
3、叔節(jié)點(diǎn)為黑 + 當(dāng)前為左子——變色換位
- 操作:將父節(jié)點(diǎn)染黑,祖父節(jié)點(diǎn)染紅,以祖父節(jié)點(diǎn)為中心進(jìn)行右旋
- 邏輯:通過變色和旋轉(zhuǎn),將父節(jié)點(diǎn)提升為新的局部根節(jié)點(diǎn),從而徹底解決沖突,終止循環(huán)
注意:若父節(jié)點(diǎn)是祖父的右孩子,上述邏輯中的“左/右”和“內(nèi)/外”需鏡像互換。
具體實(shí)現(xiàn)步驟:
1、初始化檢查(檢查樹是否為空,若為空):
- 調(diào)用 compare 進(jìn)行類型檢查
- 創(chuàng)建新的根節(jié)點(diǎn)
- 更新 size 和 modCount
- 返回 null (因?yàn)槭切虏迦耄?/li>
2、查找插入位置:
- 準(zhǔn)備變量:比較結(jié)果 cmp、父節(jié)點(diǎn) parent 、比較器 cpr
- 使用比較器情況:
- 如果有自定義比較器:
- 保存當(dāng)前節(jié)點(diǎn)為父節(jié)點(diǎn)
- 比較key和當(dāng)前節(jié)點(diǎn)key
- 根據(jù)比較結(jié)果向左或向右移動(dòng)(小,往左走;大,往右走)
- 如果相等,更新值并返回舊值
- 如果沒有自定義比較器:
- 檢查key是否為null
- 將key強(qiáng)制轉(zhuǎn)換為Comparable
- 使用自然排序進(jìn)行比較
- 同樣根據(jù)比較結(jié)果移動(dòng)或更新值
- 如果有自定義比較器:
3、插入新節(jié)點(diǎn):
- 創(chuàng)建新的Entry節(jié)點(diǎn)
- 根據(jù)最后的比較結(jié)果決定插入到父節(jié)點(diǎn)的左子樹還是右子樹
4、維護(hù)紅黑樹性質(zhì):
- 調(diào)用fixAfterInsertion調(diào)整樹的結(jié)構(gòu),保持紅黑樹的平衡
- 更新size和modCount
- 返回null(表示新插入)
流程圖如下:

遍歷機(jī)制:迭代器的實(shí)現(xiàn)
TreeSet 的 iterator() 方法返回了一個(gè)按順序排列的迭代器。它是如何做到 “從小到大” 遍歷的?
// java.util.TreeSet
public Iterator<E> iterator() {
return m.navigableKeySet().iterator();
}進(jìn)入 TreeMap 的內(nèi)部類 KeyIterator 或 NavigableSubMap 的迭代器實(shí)現(xiàn):
// java.util.TreeMap
final class KeyIterator extends PrivateEntryIterator<K> {
KeyIterator(Entry<K,V> first) {
super(first);
}
public K next() {
return nextEntry().key;
}
}核心在于 nextEntry() 方法。它利用了紅黑樹的性質(zhì):中序遍歷。
final Entry<K,V> nextEntry() {
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
// 預(yù)先計(jì)算下一個(gè)節(jié)點(diǎn),保存在 next 變量中
// 這里的算法是:如果當(dāng)前節(jié)點(diǎn)有右子樹,下一個(gè)節(jié)點(diǎn)是右子樹的最左節(jié)點(diǎn);
// 如果沒有右子樹,下一個(gè)節(jié)點(diǎn)是第一個(gè)“祖先”節(jié)點(diǎn),該節(jié)點(diǎn)的左子樹包含當(dāng)前節(jié)點(diǎn)。
if ((next = successor(e)) == null)
next = null;
lastReturned = e;
return e;
}
// 尋找后繼節(jié)點(diǎn)的算法
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
if (t == null)
return null;
else if (t.right != null) {
// 有右子樹:找右子樹中最左邊的節(jié)點(diǎn)(最小的)
Entry<K,V> p = t.right;
while (p.left != null)
p = p.left;
return p;
} else {
// 無右子樹:向上找第一個(gè)左拐的父節(jié)點(diǎn)
Entry<K,V> p = t.parent;
Entry<K,V> ch = t;
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}這種迭代方式保證了 TreeSet 的遍歷是嚴(yán)格按照元素大小順序進(jìn)行的,時(shí)間復(fù)雜度為 O(1)(均攤,因?yàn)槊總€(gè)節(jié)點(diǎn)被訪問兩次,一次是向下查找,一次是向上回溯父節(jié)點(diǎn))。
高級(jí)特性:NavigableSet 的實(shí)現(xiàn)
TreeSet 實(shí)現(xiàn)了 NavigableSet,這意味著它不僅能排序,還能在有序集合中進(jìn)行 “搜索” 。
floor(E e) 與 ceiling(E e)
這些方法也是基于 TreeMap 的二叉查找特性實(shí)現(xiàn)的。
// java.util.TreeSet
public E floor(E e) {
return m.floorKey(e);
}
public K floorKey(K key) {
Entry<K,V> p = getFloorEntry(key);
return (p == null) ? null : p.key;
}
// 核心:尋找小于等于 key 的最大節(jié)點(diǎn)
final Entry<K,V> getFloorEntry(K key) {
Entry<K,V> p = root;
while (p != null) {
int cmp = compare(key, p.key);
if (cmp > 0) { // key > p.key
if (p.right != null)
p = p.right; // 往右找更大的
else
return p; // 沒右了,當(dāng)前 p 就是小于 key 的最大值
} else if (cmp < 0) { // key < p.key
if (p.left != null)
p = p.left; // 往左找更小的
else {
// 沒左了,說明當(dāng)前 p 太大了,得往回找父節(jié)點(diǎn)
Entry<K,V> parent = p.parent;
Entry<K,V> ch = p;
while (parent != null && ch == parent.left) {
ch = parent;
parent = parent.parent;
}
return parent;
}
} else
return p; // 相等,直接返回
}
return null;
}子視圖 subMap 的驚人效率
public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
E toElement, boolean toInclusive) {
return new TreeSet<>(m.subMap(fromElement, fromInclusive,
toElement, toInclusive));
}注意: 返回的 TreeSet 并不是復(fù)制數(shù)據(jù)!它持有了一個(gè)指向原 TreeMap 的視圖對象(通常是 TreeMap.AscendingSubMap)。這個(gè)視圖對象僅僅記錄了 fromElement 和 toElement 的邊界。
當(dāng)你對這個(gè)子集進(jìn)行遍歷時(shí),迭代器會(huì)在每次 next() 時(shí)檢查是否越界。如果你向子集中添加元素,add 方法內(nèi)部會(huì)攔截并檢查新元素是否在范圍內(nèi),如果不在范圍直接拋出 IllegalArgumentException。
這意味著:
subSet操作是 O(1) 的,極其輕量- 無論原集合多大,獲取子集幾乎沒有內(nèi)存開銷
3、Queue
在 Java 集合框架的龐大體系中,Queue(隊(duì)列)是一個(gè)非常特殊的且重要的接口。如果說 List 是為了存儲(chǔ)和索引,Set 是為了去重,那么 Queue 的存在就是為了處理數(shù)據(jù)。
Queue 通常用于模擬 “先進(jìn)先出(FIFO)” 的數(shù)據(jù)結(jié)構(gòu),但在 Java 語境下,它的含義遠(yuǎn)不止如此。它涵蓋了一系列以有序處理為核心的集合。
Queue 繼承自 Collection 接口,主要用于在處理之前保存元素。除了標(biāo)準(zhǔn)的集合操作外,Queue 提供了三組特定的操作方式,這是其最顯著的特征:
- 插入:向隊(duì)列尾部添加元素
- 移除:移除并返回隊(duì)列頭部元素
- 檢查:返回隊(duì)列頭部元素但不移除
為了應(yīng)對不同的業(yè)務(wù)場景(特別是容量受限的場景),Queue 為每種操作都定義了兩種方法:
| 操作 | 拋出異常 | 返回特殊值 | 說明 |
|---|---|---|---|
| 插入 | add(E e) | offer(E e) | 隊(duì)列滿時(shí),add 拋出IllegalStateException異常,offer 返回 false |
| 移除 | remove() | poll() | 隊(duì)列空時(shí),remove 拋出 NoSuchElementException,poll 返回 null |
| 檢查 | element() | peek() | 隊(duì)列空時(shí),element 拋出 NoSuchElementException,peek 返回 null |
3.1 Deque 雙端隊(duì)列(Java 官方推薦的 棧 的替代者)
Deque 是 “ Double Ended Queue” 的縮寫,讀音通常為 “Deck”。它繼承自 Queue 接口,定義了一個(gè)支持在兩端進(jìn)行元素插入和移除的線性集合。
這意味著:
- 它可以作為 FIFO 隊(duì)列 使用(一端進(jìn),另一端出)
- 它可以作為 LIFO 棧 使用(同一端進(jìn),同一端出)
- 它可以作為 雙端隊(duì)列 使用(兩端都可以進(jìn)出)
Deque 的核心在于 “雙端”,打破了傳統(tǒng) Queue 只能隊(duì)尾進(jìn)、對頭出的限制。
3.2 ArrayDeque (Java 中實(shí)現(xiàn)棧和隊(duì)列的首選類)
在 Java 集合框架的工具箱中,ArrayDeque 是一把鋒利、高效且專精的單刃刀。它不是最老資格的(Vector 和 Stack 比它老),也不是功能最全的(LinkedList 實(shí)現(xiàn)了 List 接口),但它是在線性數(shù)據(jù)操作(棧、隊(duì)列、雙端隊(duì)列)場景下的性能王者。
ArrayDeque (全稱 "Array Double Ended Queue(數(shù)組雙端隊(duì)列)")是 Deque 接口的一種可變數(shù)組實(shí)現(xiàn)。
核心特性:
- 雙端:可以在頭部或尾部高效地插入或刪除元素
- 非線程安全:沒有 synchronized 修飾,適用于單線程環(huán)境
- 禁止 null:不允許插入 null 元素(為了區(qū)分空隊(duì)列和 null 值)
為什么 ArrayDeque 這么快?
ArrayDeque 之所以被稱為 Java 集合框架中的 “性能王者”,并非由單一因素決定,而是數(shù)據(jù)結(jié)構(gòu)、算法設(shè)計(jì)、內(nèi)存管理和硬件優(yōu)化四個(gè)方面共同作用的結(jié)果。
數(shù)據(jù)結(jié)構(gòu)層面:循環(huán)數(shù)組 vs 鏈表節(jié)點(diǎn)
這是 ArrayDeque 和 LinkedList (常見的雙端隊(duì)列替代品) 最根本的區(qū)別
- 指針開銷的消除:
- LinkedList:每存儲(chǔ)一個(gè)元素,都需要?jiǎng)?chuàng)建一個(gè) Node 對象,內(nèi)部包含 prev(前驅(qū)指針)、next(后繼指針)、item(節(jié)點(diǎn)數(shù)據(jù))三個(gè)引用。在 64 位 JVM 中,僅對線頭和引用就要占用幾十個(gè)字節(jié),內(nèi)存開銷是數(shù)據(jù)本身的好幾倍
- ArrayDeque:底層是純粹的對象數(shù)組 Object[]。數(shù)據(jù)直接存儲(chǔ),沒有額外的包裝對象。這意味著同樣的內(nèi)存空間,可以存儲(chǔ)更多的數(shù)據(jù),減少 GC(垃圾回收)掃描和回收的壓力
- 物理內(nèi)存連續(xù)性(CPU 緩存友好):
- LinkedList:節(jié)點(diǎn)在堆內(nèi)存中是離散分布的。CPU 讀完節(jié)點(diǎn) A,去讀節(jié)點(diǎn) B 時(shí),很可能發(fā)生緩存未命中,必須重新從較慢的主存中讀取數(shù)據(jù)
- ArrayDeque:數(shù)組在內(nèi)存中是連續(xù)分配的。CPU 在讀取 element[0] 時(shí),會(huì)智能地將 element[1] 至 element[n] 一并加載到 L1/L2 緩存行中。當(dāng)遍歷或連續(xù)操作時(shí),CPU 命中緩存的概率極高,幾乎不需要等待主存
算法設(shè)計(jì)層面:位運(yùn)算 vs 取模
這是 ArrayDeque 比普通數(shù)組隊(duì)列快的原因
- 位運(yùn)算替代取模:
- 普通隊(duì)列:實(shí)現(xiàn)循環(huán)邏輯通常使用取模運(yùn)算 index = (index + 1) % length。取模運(yùn)算在 CPU層面涉及除法指令,這是一條昂貴的指令,周期很長。
- ArrayDeque:強(qiáng)制要求數(shù)組長度為 2 的冪次方。它使用位運(yùn)算 index = (index + 1) & (length - 1)。& 運(yùn)算是 CPU 最基礎(chǔ)的原位操作,只需要一個(gè)時(shí)鐘周期,速度比取??鞄资?/li>
- O(1) 的雙端操作:
- 不同于 ArrayList 在頭部插入需要移動(dòng) System.arraycopy 所有元素,ArrayDeque 的 addFirst 和 addLast 僅僅是修改 head 或 tail 指針的值。沒有拷貝數(shù)據(jù),時(shí)間復(fù)雜度穩(wěn)定為 O(1)
并發(fā)策略層面:無鎖設(shè)計(jì) vs 同步鎖
這是 ArrayDeque 比古老的 Stack 或 Vector 快的原因
- 去除 synchronized:
- Stack / Vector:為了保證線程安全,所有公共方法都添加了 synchronized 。這意味著即使是單線程操作,也需要獲取鎖。加鎖、釋放鎖、掛起線程(在競爭時(shí))都會(huì)帶來巨大的性能消耗
- ArrayDeque:完全非線程安全。它假設(shè)你在單線程環(huán)境下使用,或由外部自己控制并發(fā)。因此,它甩掉了所有同步鎖的包袱,飛快運(yùn)行
內(nèi)存操作層面:預(yù)分配 vs 動(dòng)態(tài)分配
- 擴(kuò)容策略優(yōu)化:
- ArrayDeque 默認(rèn)初始容量為 16,每次擴(kuò)容為原來的 2 倍。這種指數(shù)級(jí)擴(kuò)容策略分?jǐn)偭藬U(kuò)容的時(shí)間復(fù)雜度,使得 add 操作的平均時(shí)間復(fù)雜度依然為 O(1)
- 雖然擴(kuò)容時(shí)也需要 Arrays.copyOf,但由于它是基于數(shù)組的,批量內(nèi)存復(fù)制(利用 CPU 的 SIMD 指令集)效率非常高,遠(yuǎn)高于鏈表逐個(gè)節(jié)點(diǎn)的分配和鏈接
底層原理解析
ArrayDeque 是 Java 集合框架中務(wù)實(shí)的代表。它沒有花哨的功能,專注于把 “雙端操作” 做到極致。
核心源碼結(jié)構(gòu):精簡的基石
public class ArrayDeque<E> extends AbstractCollection<E>
implements Deque<E>, Cloneable, Serializable
{
// 用于存儲(chǔ)元素的
transient Object[] elements;
// 對頭指針:指向隊(duì)首元素
transient int head;
// 隊(duì)尾指針:指向下一個(gè)待插入元素的位置(隊(duì)尾元素的下一個(gè)位置)
transient int tail;
// 最小初始容量
private static final int MIN_INITIAL_CAPACITY = 8;
}- elements.length:數(shù)組長度始終保持為 2 的冪次方(8,16,32........)。這是 ArrayDeque 高效的基石。
- Head 與 Tail:這是兩個(gè)游標(biāo)
- head:指向雙端隊(duì)列頭部元素的索引
- tail:指向雙端隊(duì)列尾部下一個(gè)可以插入元素的索引位置
- 如果隊(duì)列為空,head 的值等于 tail
核心操作源碼解析
1、添加元素
// java.util.ArrayDeque
// 對頭入隊(duì)
public void addFirst(E e){
// 安全檢查: ArrayDeque 不允許 null
if (e == null)
throw new NullPointerException();
// 元素入隊(duì),head 指針向前移動(dòng)一位
elements[head = (head - 1) & (elements.length - 1)] = e;
// 在插入元素后,檢查 head 是否追上了 tail
// 在 ArrayDeque 中,數(shù)組中始終至少留一個(gè)空位
// 當(dāng) head == tail 時(shí),說明數(shù)組已經(jīng)滿了,沒有空位了
if (head == tail)
doubleCapacity();
}
// 隊(duì)尾入隊(duì)
public void addLast(E e){
// 安全檢查: ArrayDeque 不允許 null
if (e == null)
throw new NullPointerException();
// 元素入隊(duì),放在 tail 指向的位置
elements[tail] = e;
// tail 指針后移并判斷是否需要擴(kuò)容
// (tail + 1) & (elements.length - 1) 等同于 (tail + 1) % length
// 如果移動(dòng)后的 tail 撞上了 head,說明數(shù)組已經(jīng)滿了
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}2、刪除元素
// java.util.ArrayDeque
// 刪除隊(duì)列頭部元素
public E removeFirst() {
// 調(diào)用 pollFirst() 方法嘗試獲取并移除隊(duì)列頭部的元素,并將結(jié)果賦值給變量 x
E x = pollFirst();
// 如果 pollFirst() 返回 null(意味著隊(duì)列為空),則拋出一個(gè) NoSuchElementException 異常
if(x == null)
throw new NoSuchElementException();
// (隊(duì)列不為空)返回獲取到的元素 x
return x;
}
public E pollFirst() {
// 將當(dāng)前隊(duì)列頭部的索引 head 賦值給局部變量 h
int h = head;
// @SuppressWarnings("unchecked") 用于抑制編譯器的“未檢查類型轉(zhuǎn)換”警告
// elements 數(shù)組是 Object[] 類型,將其中的元素強(qiáng)制轉(zhuǎn)換為泛型 E 類型時(shí),編譯器會(huì)發(fā)出警告
// 這里開發(fā)人員確認(rèn)這種轉(zhuǎn)換是安全的,因此忽略警告
@SuppressWarnings("unchecked")
// 取出數(shù)組中索引為 h 的元素,并強(qiáng)制轉(zhuǎn)換為泛型 E 類型,賦值給 result
E result = (E) elements[h];
// 判斷取出的元素是否為 null
if (result == null)
return null;
// 將原頭部位置的數(shù)組元素顯式設(shè)為 null
// 手動(dòng)置為 null,防止內(nèi)存泄漏
// 在 Java 中,只要一個(gè)對象被引用,垃圾回收器(GC)就不會(huì)回收它
// 雖然我們即將把 head 指針移走,但數(shù)組中該位置仍然引用著舊的對象
// 如不手動(dòng)置為 null,這個(gè)對象可能一直占用內(nèi)存直到數(shù)組被覆蓋或回收
elements[h] = null;
// 計(jì)算新的頭部索引
head = (h + 1) & (elements.length - 1);
// 返回之前取出的元素
return result;
}
// 刪除隊(duì)列尾部元素
public E removeLast() {
// 調(diào)用 pollLast() 方法嘗試獲取并移除隊(duì)列尾部的元素,并將結(jié)果賦值給變量 x
E x = pollLast();
// 如果 pollLast() 返回 null(意味著隊(duì)列為空),則拋出一個(gè) NoSuchElementException 異常
if(x == null)
throw new NoSuchElementException();
// (隊(duì)列不為空)返回獲取到的元素 x
return x;
}
public E pollLast() {
// 計(jì)算隊(duì)尾元素索引
// tail 指向下一個(gè)可用元素的位置(即當(dāng)前隊(duì)尾元素的下一位)
// 實(shí)際的最后一個(gè)元素索引是 tail - 1
int t = (tail - 1) & (elements.length - 1);
@SuppressWarnings("unchecked")
// 取出數(shù)組中索引為 t 的元素,并強(qiáng)制轉(zhuǎn)換為泛型 E 類型,賦值給 result
E result = (E) elements[t];
// 判斷取出的元素是否為 null
if (result == null)
return null;
// 顯式置空,幫助 GC
elements[t] = null;
// 將隊(duì)尾指針 tail 移動(dòng)到剛才取出元素的位置
tail = t;
// 返回取出的元素
return result;
}擴(kuò)容機(jī)制:doubleCapacity
當(dāng) head == tail 時(shí),數(shù)組已滿,需要進(jìn)行 2 倍擴(kuò)容。這個(gè)方法非常巧妙,它不僅擴(kuò)大了容量,還重排了數(shù)據(jù)。
// java.util.ArrayDeque
private void doubleCapacity() {
// 斷言 head 等于 tail
// 在循環(huán)數(shù)組中,當(dāng)隊(duì)列滿時(shí),head 和 tail 會(huì)指向同一個(gè)位置
// (即下一個(gè)要插入的位置會(huì)被覆蓋,或者表示沒有空間)
// 這個(gè)斷言確保該方法只在隊(duì)列滿時(shí)被調(diào)用
assert head == tail;
// 保存當(dāng)前隊(duì)首的索引位置到變量 p
int p = head;
// 獲取舊數(shù)組的長度 n
int n = elements.length;
// 計(jì)算從 head (即 p) 到數(shù)組末尾的元素個(gè)數(shù)
int r = n - p;
// 計(jì)算新容量,n 左移 1 位相當(dāng)于 n * 2
int newCapacity = n << 1;
// 檢查新容量是否溢出
// 如果 n 已經(jīng)非常大(接近 Integer.MAX_VALUE),n * 2 會(huì)變成負(fù)數(shù)(整數(shù)溢出)
// 此時(shí)隊(duì)列已經(jīng)無法繼續(xù)擴(kuò)容,拋出異常
if (newCapacity < 0)
throw new IllegalStateException("Sorry, deque too big");
// 創(chuàng)建一個(gè)容量為 newCapacity 的新數(shù)組 a
Object[] a = new Object[newCapacity];
// 復(fù)制 head 到數(shù)組末尾的部分 到 新數(shù)組的開頭
System.arraycopy(elements, p, a, 0, r);
// 復(fù)制 數(shù)組開頭 到 tail 的部分 到 新數(shù)組的后面
System.arraycopy(elements, 0, a, r, p);
// 將隊(duì)列內(nèi)部的數(shù)組引用指向新數(shù)組 a
elements = a;
// 重置隊(duì)首 head 為新數(shù)組的 0 索引
head = 0;
// 重置隊(duì)尾 tail 為舊數(shù)組的長度 n
tail = n;
}ArrayDeque 的數(shù)據(jù)在循環(huán)數(shù)組中可能跨越了數(shù)組末尾(一部分在頭,一部分在尾)。擴(kuò)容時(shí),它通過兩次 System.arraycopy 將這兩部分?jǐn)?shù)據(jù)“拼”在一起,在新數(shù)組中變成了連續(xù)數(shù)據(jù)。這種重排保證了后續(xù)操作的內(nèi)存連續(xù)性,進(jìn)一步提高緩存命中率。
圖解擴(kuò)容過程:
假設(shè) ArrayDeque 的底層數(shù)組 elements 的初始容量為 8(索引 0 ~7)。
1.擴(kuò)容前的狀態(tài)(隊(duì)列已滿)
此時(shí),隊(duì)列中已經(jīng)存入了 8 個(gè)元素,數(shù)組已滿,無法再插入新元素
- head 指向索引 4(隊(duì)首元素是 A)
- tail 指向索引 4(下一個(gè)要插入的位置)
- 注意:因?yàn)閿?shù)組是循環(huán)使用的,元素被分成了兩部分:
- 右半部分:從 head(4)到數(shù)組末尾(7),存放了元素 A、B、C、D
- 左半部分:從 數(shù)組開頭(0)到 head(4)之前,存放了元素 E、F、G、H
數(shù)組視圖:
索引: 0 1 2 3 4 5 6 7
數(shù)據(jù): [E] [F] [G] [H] [A] [B] [C] [D]
2、執(zhí)行 doubleCapacity() 的過程
// java.util.ArrayDeque( doubleCapacity() ) int p = head; // p = 4 int n = elements.length; // n = 8 int r = n - p; // r = 8 - 4 = 4 (head右邊有4個(gè)元素) int newCapacity = n << 1; // newCapacity = 16 Object[] a = new Object[newCapacity]; // 創(chuàng)建新數(shù)組,長度 16 // 步驟 1: 復(fù)制右半部分 [A, B, C, D] 到新數(shù)組開頭 System.arraycopy(elements, p, a, 0, r); // 步驟 2: 復(fù)制左半部分 [E, F, G, H] 到新數(shù)組后面 System.arraycopy(elements, 0, a, r, p); elements = a; // 引用指向新數(shù)組 head = 0; // 重置 head tail = n; // 重置 tail = 8
步驟1:復(fù)制右半部分
將舊數(shù)組中從 head(索引 4)開始的 r(4個(gè))元素復(fù)制到新數(shù)組的索引 0 處
舊數(shù)組:
索引: 0 1 2 3 4 5 6 7
數(shù)據(jù): [E] [F] [G] [H] [A] [B] [C] [D]
|--- 這4個(gè) ---|
新數(shù)組(復(fù)制右半部分后):
索引: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
數(shù)據(jù): [A] [B] [C] [D] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ]
|--- 這4個(gè) ---|
步驟2:復(fù)制左半部分
將舊數(shù)組中從索引 0 開始的 p(4個(gè))元素復(fù)制到新數(shù)組的索引 r (4) 處
舊數(shù)組:
索引: 0 1 2 3 4 5 6 7
數(shù)據(jù): [E] [F] [G] [H] [A] [B] [C] [D]
|--- 這4個(gè) ---|
新數(shù)組(復(fù)制左半部分后):
索引: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
數(shù)據(jù): [A] [B] [C] [D] [E] [F] [G] [H] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ]
|--- 這4個(gè) ---|
3、擴(kuò)容后的狀態(tài)
復(fù)制完成后,更新指針:
- head 設(shè)為 0
- tail 設(shè)為舊數(shù)組長度 8
最終新數(shù)組視圖:
索引: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
數(shù)據(jù): [A] [B] [C] [D] [E] [F] [G] [H] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ]
^ ^
head=0 tail=8
通過這個(gè)過程,可以看到:
- 邏輯連續(xù),物理分離的數(shù)據(jù)(A,B,C,D 和 E,F,G,H)被重新拼接成物理連續(xù)的數(shù)據(jù)(A,B,C,D,E,F,G,H)
- head 回到了數(shù)組的起點(diǎn),這簡化了后續(xù)的索引計(jì)算
- tail 指向了最后一個(gè)元素的下一個(gè)位置(8),正好是舊數(shù)組的長度,為新元素的插入留出了空間
3.3 PriorityQueue(打破先來后到的“特權(quán)隊(duì)列”)
在 Java 集合框架中,如果說 LinkedList 和 ArrayDeque 遵循的是嚴(yán)格的 “先來后到”(FIFO,先進(jìn)先出)原則,那么 PriorityQueue 就是一個(gè)徹底的 “打破規(guī)則者”。
它不關(guān)心你是什么時(shí)候排隊(duì)的,它只關(guān)心你的優(yōu)先級(jí)。
PriorityQueue(優(yōu)先級(jí)隊(duì)列)是 Java 中基于優(yōu)先級(jí)堆(Priority Heap)實(shí)現(xiàn)的無界優(yōu)先級(jí)隊(duì)列。
核心特性:
- 無界:理論上可以無限擴(kuò)容(直到內(nèi)存溢出),初始容量默認(rèn)為 11
- 無序:當(dāng)你遍歷 PriorityQueue 時(shí),元素不一定是有序的
- 有序的出隊(duì):每次調(diào)用 pop() 或 remove() 時(shí),它保證取出的都是當(dāng)前隊(duì)列中優(yōu)先級(jí)最高(最小或最大)的元素
- 不支持 null:不允許插入 null 值
注意:
PriorityQueue 具有"內(nèi)部無序但出隊(duì)有序"的特性,這一特點(diǎn)容易讓人產(chǎn)生混淆。要準(zhǔn)確理解這一機(jī)制,關(guān)鍵在于區(qū)分"內(nèi)存中的實(shí)際存儲(chǔ)結(jié)構(gòu)"和"邏輯上的優(yōu)先級(jí)關(guān)系"。
簡而言之:PriorityQueue 就像一個(gè)雜亂無章的房間,但配備了精準(zhǔn)的導(dǎo)航雷達(dá)。
它是堆,不是有序數(shù)組
很多人的直覺認(rèn)為,優(yōu)先級(jí)隊(duì)列應(yīng)該排列似 [1,2,3,4,5] 這樣整齊的數(shù)組。但 PriorityQueue 底層維護(hù)的是二叉堆。
堆的核心規(guī)則只有一條:
小頂堆:父節(jié)點(diǎn)的值不大于其子節(jié)點(diǎn)的
- 規(guī)則僅限于父子之間,不涉及兄弟節(jié)點(diǎn)
這導(dǎo)致了只需確保父節(jié)點(diǎn)不小于子節(jié)點(diǎn)即可,而對左右子節(jié)點(diǎn)的大小關(guān)系,或是同一層級(jí)最后一個(gè)節(jié)點(diǎn)與下一層級(jí)首個(gè)節(jié)點(diǎn)的相對大小并無具體要求。
圖解存儲(chǔ)過程:
假設(shè)我們依次插入數(shù)字:5,3,1,4,2。
如果是有序數(shù)組,它們在內(nèi)存中是這樣的:[ 1,2,3,4,5 ](非常整齊)
但在 PriorityQueue(二叉堆)中,它們的邏輯結(jié)構(gòu)(樹狀)是這樣的:
1 <-- 堆頂 (最小)
/ \
3 2
/ \
5 4
對應(yīng)到底層數(shù)組 Object[] queue 的存儲(chǔ)順序是:
索引:[0] [1] [2] [3] [4]
數(shù)值:[ 1, 3, 2, 5, 4]
這個(gè)數(shù)組的順序是錯(cuò)亂的!3排在2前面,5又排在4前面,完全沒有按照順序排列。如果你直接遍歷數(shù)組(比如用for(int i : pq)),得到的結(jié)果會(huì)是1、3、2、5、4,這就是典型的"內(nèi)部無序"現(xiàn)象。
出隊(duì)操作:有序
雖然數(shù)組里的元素亂七八槽,但堆頂(索引為 0 的位置)永遠(yuǎn)是所有元素中最小的那個(gè)。
調(diào)用 poll 方法時(shí)的執(zhí)行邏輯如下:
- 移除堆頂元素:直接取出數(shù)組首元素
queue[0](當(dāng)前最小值) - 填補(bǔ)空缺:將數(shù)組末尾元素移動(dòng)到堆頂位置
- 下沉調(diào)整:
- 若當(dāng)前父節(jié)點(diǎn)(堆頂)值大于子節(jié)點(diǎn),則進(jìn)行下沉操作
- 父節(jié)點(diǎn)與較小的子節(jié)點(diǎn)交換位置
- 若交換后仍不滿足堆性質(zhì),則繼續(xù)下沉直至堆底
- 形成新堆:經(jīng)過調(diào)整后,原第二小的元素會(huì)升至堆頂位置
圖解過程:
第一次 poll 前:
1
/ \
3 2
/ \
5 4
輸出:1
調(diào)整后(poll 后):
2 <-- 新的王者 (第二小)
/ \
4 3
/
5
數(shù)組變成了:[2, 4, 3, 5, .....] (依然是亂序的,但堆頂變了)
第二次 poll(),你會(huì)得到 2。
第三次 poll(),你會(huì)得到 3。
結(jié)論:盡管數(shù)組內(nèi)部元素始終是無序排列的,但每次調(diào)用 poll() 方法都能準(zhǔn)確獲取當(dāng)前最小值。通過連續(xù)調(diào)用該方法,最終就能得到一個(gè)有序的輸出序列。
import java.util.PriorityQueue;
public class priorityQueueDemo {
public static void main(String[] args) {
PriorityQueue<Integer> queue = new PriorityQueue<>();
// 亂序插入
queue.add(3);
queue.add(1);
queue.add(2);
queue.add(5);
queue.add(4);
// 驗(yàn)證“內(nèi)部無序”:直接打印集合
System.out.println("直接打印內(nèi)部結(jié)構(gòu)(無序): " + queue);
// 輸出可能是: [1, 2, 3, 5, 4] <-- 注意這不是完全排序,只是堆結(jié)構(gòu)
// 驗(yàn)證“出隊(duì)有序”:使用 poll 遍歷
System.out.print("依次出隊(duì)(有序): ");
while (!queue.isEmpty()) {
System.out.println(queue.poll());
}
// 輸出: 1 2 3 4 5
}
}為什么這樣設(shè)計(jì)?
你可能會(huì)疑惑:"既然有序數(shù)組也能實(shí)現(xiàn)快速遍歷和出隊(duì),為什么不直接采用這種存儲(chǔ)方式呢?"
這就涉及到了時(shí)間復(fù)雜度的權(quán)衡:
| 操作 | PriorityQueue | 有序數(shù)組 | 差距 |
|---|---|---|---|
| 插入(add) | O(log n) | O(n) | 堆完勝 |
| 刪除堆頂(poll) | O(log n) | O(n) | 堆完勝 |
| 獲取最小值 | O(1) | O(1) | 平手 |
原因:
- 有序數(shù)組:每次插入一個(gè)新元素,都要把后面比它大的元素全部往后挪一格(System.arraycopy),非常慢
- PriorityQueue(堆):插入操作只需將新元素置于末尾,然后像冒泡排序那樣逐層上浮(最多 log n 次),無需移動(dòng)其他元素
底層原理解析
PriorityQueue 是 Java 集合框架中的特殊實(shí)現(xiàn)。與 ArrayList 的線性存儲(chǔ)方式和 HashMap 的哈希結(jié)構(gòu)不同,它采用二叉堆算法實(shí)現(xiàn),能夠在 O(log n) 時(shí)間復(fù)雜度內(nèi)高效獲取極值(最大值或最小值)。
核心源碼結(jié)構(gòu):簡而精
// java.util
public class PriorityQueue<E> extends AbstractQueue<E>
implements java.io.Serializable {
// 默認(rèn)初始容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 底層存儲(chǔ)數(shù)組:通過二叉堆的邏輯來維護(hù)這個(gè)數(shù)組
transient Object[] queue;
// 當(dāng)前元素個(gè)數(shù)
private int size = 0;
// 比較器:如果為 null,則使用自然排序
private final Comparator<? super E> comparator;
// 修改次數(shù)(用于 fail-fast 機(jī)制)
transient int modCount = 0;
}盡管名為 Queue(隊(duì)列),但其底層實(shí)現(xiàn)采用了 Object[] queue 數(shù)組結(jié)構(gòu)。通過巧妙運(yùn)用數(shù)組索引關(guān)系,該結(jié)構(gòu)完美模擬了完全二叉樹的特性。
具體實(shí)現(xiàn)細(xì)節(jié)如下:
數(shù)組索引從0開始,通過數(shù)學(xué)關(guān)系構(gòu)建樹形結(jié)構(gòu):
- 對于任意節(jié)點(diǎn) i(0 ≤ i < size):
- 其左子節(jié)點(diǎn)索引為 2 * i + 1
- 其右子節(jié)點(diǎn)索引為 2 * i + 2
- 其父節(jié)點(diǎn)索引為 (i - 1) >>> 1(無符號(hào)右移,等同于除以 2)
這種數(shù)組實(shí)現(xiàn)方式具有以下優(yōu)勢:
- 內(nèi)存連續(xù),訪問效率高
- 不需要額外的指針存儲(chǔ)空間
- 完全二叉樹的性質(zhì)保證了O(log n)的時(shí)間復(fù)雜度
入隊(duì):offer(e) 與 “上浮”算法
PriorityQueue 的 add 方法實(shí)際上是調(diào)用了 offer 方法。其核心入隊(duì)邏輯分為兩步:首先將新元素添加到隊(duì)列末尾,然后通過"上浮"操作將其調(diào)整到合適的位置。
// java.util.PriorityQueue
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
// 非空檢查
// PriorityQueue 不允許插入 null 元素,如果插入 null 拋出異常
if(e == null)
throw new NullPointerException();
// 修改計(jì)數(shù)器更新
modCount++;
// 獲取當(dāng)前隊(duì)列的大小 i
int i = size;
// 如果當(dāng)前元素?cái)?shù)量 i 已經(jīng)大于或等于底層數(shù)組 queue 的長度,說明數(shù)組已滿
if (i >= queue.length)
// 調(diào)用 grow(i + 1) 方法進(jìn)行擴(kuò)容,以確保能容納新元素
grow(i + 1);
// 更新隊(duì)列大小 i
size = i + 1;
// 如果插入前隊(duì)列是空的(i == 0),直接將元素放在數(shù)組的第一個(gè)位置 queue[0]
if (i == 0)
queue[0] = e;
// 如果隊(duì)列不為空,調(diào)用 siftUp(i, e) 方法,執(zhí)行上浮調(diào)整
else
siftUp(i, e);
return true;
}核心算法:siftUp
這是維護(hù)小頂堆性質(zhì)的核心操作。具體實(shí)現(xiàn)時(shí),先將新元素 e 插入數(shù)組末尾(位置 i),然后將其與父節(jié)點(diǎn)進(jìn)行比較。若發(fā)現(xiàn)新元素小于父節(jié)點(diǎn)(符合小頂堆特性),則交換兩者位置。該過程循環(huán)執(zhí)行,直至新元素到達(dá)堆頂或不再小于其父節(jié)點(diǎn)時(shí)終止。
// java.util.PriorityQueue
private void siftUp(int k,E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
private void siftUpUsingComparator(int k,E x) {
// 循環(huán)條件:while (k > 0)
// 只要當(dāng)前節(jié)點(diǎn)索引 k 不是根節(jié)點(diǎn)(根節(jié)點(diǎn)索引為 0),就繼續(xù)嘗試上浮
while (k > 0) {
// 計(jì)算當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)索引
int parent = (k - 1) >>> 1;
// 取出父節(jié)點(diǎn)存儲(chǔ)的元素
Object e = queue[parent];
// 使用比較器比較當(dāng)前待插入元素 x 和父節(jié)點(diǎn)元素 e
// 如果 x 大于或等于 e,則 break 跳出循環(huán)。此時(shí)位置 k 就是 x 的最終歸宿
if (comparator.compare(x, (E) e) >= 0)
break;
// 如果 x 小于 e,說明違反了堆序性質(zhì)(子節(jié)點(diǎn)比父節(jié)點(diǎn)?。枰粨Q位置。
queue[k] = e;
// 更新當(dāng)前索引 k 為父節(jié)點(diǎn)的索引,準(zhǔn)備繼續(xù)向上層比較
k = parent;
}
// 循環(huán)結(jié)束后(要么找到了合適的位置,要么到了根節(jié)點(diǎn)),將目標(biāo)元素 key 放入最終確定的位置 k
queue[k] = x;
}
private void siftUpComparable(int k,E x) {
// 將傳入的元素 x 強(qiáng)制轉(zhuǎn)換為 Comparable 類型
// PriorityQueue 需要比較元素的大小來確定優(yōu)先級(jí)
Comparable<? super E> key = (Comparable<? super E>) x;
// 開始循環(huán)。只要當(dāng)前節(jié)點(diǎn)索引 k 大于 0(即當(dāng)前節(jié)點(diǎn)不是根節(jié)點(diǎn)),就繼續(xù)嘗試上浮
while (k > 0) {
// 計(jì)算當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)索引
int parent = (k - 1) >>> 1;
// 取出父節(jié)點(diǎn)存儲(chǔ)的元素
Object e = queue[parent];
// 比較當(dāng)前插入元素 key 和父節(jié)點(diǎn)元素 e 的大小
// 如果 key >= e
// 說明堆的性質(zhì)已經(jīng)滿足(父節(jié)點(diǎn)小于等于子節(jié)點(diǎn)),無需繼續(xù)交換,直接 break 跳出循環(huán)
if (key.compareTo((E) e) >= 0)
break;
// 如果 key < e,說明子節(jié)點(diǎn)比父節(jié)點(diǎn)小,違反了最小堆性質(zhì)
// 則將父節(jié)點(diǎn) e 向下移動(dòng)到當(dāng)前子節(jié)點(diǎn) k 的位置
queue[k] = e;
// 更新當(dāng)前索引 k 為父節(jié)點(diǎn)的索引,準(zhǔn)備繼續(xù)向上層比較
k = parent;
}
// 循環(huán)結(jié)束后(要么找到了合適的位置,要么到了根節(jié)點(diǎn)),將目標(biāo)元素 key 放入最終確定的位置 k
queue[k] = key;
}入隊(duì)操作流程:將新元素添加至數(shù)組末尾,隨后進(jìn)行上浮調(diào)整。循環(huán)比較該元素與其父節(jié)點(diǎn),若優(yōu)先級(jí)更高則交換位置,直至到達(dá)堆頂或滿足堆條件為止。
出隊(duì):poll() 與 下沉算法
poll() 操作需要移除堆頂?shù)淖钚≈翟亍R瞥?,我們將?shù)組末尾的元素移至堆頂位置,然后通過"下沉"操作使其重新找到合適的位置,以維持堆結(jié)構(gòu)的完整性。
// java.util.PriorityQueue
public E poll() {
// 檢查隊(duì)列是否為空
if (size == 0)
return null; // 如果為空,返回 null(區(qū)別于 remove() 方法拋出異常)
// 記錄操作前的元素個(gè)數(shù),并將 size 減 1
// s 變成了原數(shù)組最后一個(gè)元素的下標(biāo)
int s = --size;
// 更新修改計(jì)數(shù)器
modCount++;
// 獲取堆頂元素(即最小值),作為最終要返回的結(jié)果
E result = (E) queue[0];
// 獲取隊(duì)列末尾的元素
E x = (E) queue[s];
// 將原末尾位置置為 null,幫助 GC 回收,避免內(nèi)存泄漏
queue[s] = null;
// 如果隊(duì)列中不止一個(gè)元素,則需要執(zhí)行下沉操作
// 將剛才取出的末尾元素 x 放到堆頂位置(下標(biāo) 0),并開始下沉
if (s != 0)
siftDown(0, x);
// 返回之前獲取的最小值
return result;
}核心算法:siftDown
將堆末尾的大元素移至堆頂后,由于它必然大于子節(jié)點(diǎn),需要通過下沉操作進(jìn)行調(diào)整。具體下沉規(guī)則是:父節(jié)點(diǎn)需要與左右孩子中較小者進(jìn)行交換。
// java.util.PriorityQueue
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
private void siftDownUsingComparator(int k, E x) {
// 計(jì)算非葉子節(jié)點(diǎn)邊界
int half = size >>> 1; // 無符號(hào)右移(等同于 size / 2)
while (k < half) {
// 計(jì)算當(dāng)前節(jié)點(diǎn) k 的左孩子的索引
int child = (k << 1) + 1;
// 獲取左孩子元素
Object c = queue[child];
// 計(jì)算右孩子的索引
int right = child + 1;
// 如果右孩子存在,且右孩子比左孩子小
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right]; // 更新 child 指向右孩子
// 如果當(dāng)前元素 x 已經(jīng)小于等于較小的孩子,說明下沉結(jié)束
if (comparator.compare(x, (E) c) <= 0)
break;
// 否則,交換位置(孩子上浮成為父節(jié)點(diǎn))
queue[k] = c;
k = child;
}
// 將末尾元素放到最終的位置
queue[k] = x;
}
private void siftDownComparable(int k, E x) {
// 將待下沉的元素 x 強(qiáng)制轉(zhuǎn)換為 Comparable 接口類型
// 因?yàn)樾枰{(diào)用 compareTo 方法來比較元素大小
Comparable<? super E> key = (Comparable<? super E>)x;
// 計(jì)算非葉子節(jié)點(diǎn)邊界
int half = size >>> 1; // 無符號(hào)右移(等同于 size / 2)
while (k < half) {
// 計(jì)算當(dāng)前節(jié)點(diǎn) k 的左孩子的索引
int child = (k << 1) + 1;
// 獲取左孩子元素
Object c = queue[child];
// 計(jì)算右孩子的索引
int right = child + 1;
// 比較左右孩子
// right<size:檢查右孩子是否存在(是否越界)
// c.compareTo(queue[right])>0:如果左孩子比右孩子大(compareTo 返回正數(shù))說明右孩子更小
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
// 滿足條件,將 c 指向右孩子,并將 child 更新為右孩子的索引
c = queue[child = right];
// 如果 key 小于等于 c(compareTo 返回 0 或負(fù)數(shù)),說明堆的順序已經(jīng)正確(父節(jié)點(diǎn)小于子節(jié)點(diǎn))
if (key.compareTo((E) c) <= 0)
// 跳出循環(huán),下沉結(jié)束
break;
// 如果 key 大于 c,說明堆性質(zhì)被破壞。將較小的子節(jié)點(diǎn) c 移動(dòng)到當(dāng)前父節(jié)點(diǎn) k 的位置
queue[k] = c;
// 更新 k 為子節(jié)點(diǎn)的位置,準(zhǔn)備進(jìn)行下一輪比較
k = child;
}
// 循環(huán)結(jié)束后,將原始元素 key 放到最終確定的位置 k 上
queue[k] = key;
}出隊(duì)操作:移除堆頂?shù)淖钚≈翟睾?,將?shù)組末尾元素移至堆頂。隨后將該元素與其左右子節(jié)點(diǎn)中較小者進(jìn)行比較,若當(dāng)前元素較大則交換位置(下沉操作)。重復(fù)此過程直至堆恢復(fù)平衡狀態(tài)。
擴(kuò)容機(jī)制:grow
PriorityQueue 的擴(kuò)容策略很有意思,不是簡單的 2 倍擴(kuò)容,而是根據(jù)當(dāng)前容量階段性的增長。
// java.util.PriorityQueue
private void grow(int minCapacity) {
// 獲取當(dāng)前底層數(shù)組 queue 的長度
int oldCapacity = queue.length;
// 如果舊容量小于 64,則擴(kuò)容 2 倍 + 2
// 如果舊容量大于 64,則擴(kuò)容 1.5 倍 (即 oldCapacity + (oldCapacity >> 1))
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// 防止溢出,如果計(jì)算出的容量過大
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 調(diào)用 hugeCapacity 方法嘗試分配
newCapacity = hugeCapacity(minCapacity);
// 調(diào)用 Arrays.copyOf 方法,將原數(shù)組中的數(shù)據(jù)復(fù)制到一個(gè)長度為 newCapacity 的新數(shù)組中,并將 queue 引用指向這個(gè)新數(shù)組
queue = Arrays.copyOf(queue, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
// 溢出檢查:通過檢查 minCapacity 是否小于 0,來判斷是否發(fā)生了整數(shù)溢出
// 在 Java 中,int 是有符號(hào)的 32 位整數(shù),最大值為 Integer.MAX_VALUE(約 21 億)
// 如果之前的計(jì)算(例如 minCapacity = oldCapacity + (oldCapacity >> 1))導(dǎo)致數(shù)值超過了 int 的最大值
// 由于整數(shù)溢出,結(jié)果會(huì)變成負(fù)數(shù)
if (minCapacity < 0)
throw new OutOfMemoryError();
// 如果 minCapacity 大于 MAX_ARRAY_SIZE,則直接返回 Integer.MAX_VALUE(即 2147483647)嘗試分配理論上的最大數(shù)組長度
// 如果 minCapacity 小于或等于 MAX_ARRAY_SIZE,則返回 MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8(即 2147483639))
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}PriorityQueue 是非線程安全的類,其 grow 方法未實(shí)現(xiàn)同步機(jī)制。在多線程并發(fā)添加元素觸發(fā)擴(kuò)容時(shí),可能導(dǎo)致數(shù)據(jù)不一致或數(shù)組越界問題。建議在多線程場景下使用線程安全的 PriorityBlockingQueue 替代。
到此這篇關(guān)于Java集合框架的 Collection 分支的文章就介紹到這了,更多相關(guān)Java集合框架Collection 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Java單列集合Collection常用方法示例詳解
- java?Collection集合接口的介紹和使用詳解
- 關(guān)于Java集合框架Collection接口詳解
- Java Map集合與Collection類的使用詳解
- java集合collection接口與子接口及實(shí)現(xiàn)類
- Java?詳解Collection集合之ArrayList和HashSet
- java9新特性Collection集合類的增強(qiáng)與優(yōu)化方法示例
- Java集合的Collection接口和List接口詳解
- java集合Collection實(shí)現(xiàn)類解析ArrayList?LinkedList及Vector
相關(guān)文章
java?11新特性HttpClient主要組件及發(fā)送請求示例詳解
這篇文章主要為大家介紹了java?11新特性HttpClient主要組件及發(fā)送請求示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06
Spring4改造Dubbo實(shí)現(xiàn)注解配置兼容的完整指南
在微服務(wù)架構(gòu)中,Dubbo作為一款高性能的Java RPC框架,被廣泛應(yīng)用于分布式系統(tǒng)中,本文將探討如何改造Dubbo,使其能夠更好地兼容Spring4的注解配置2025-07-07
java數(shù)據(jù)結(jié)構(gòu)和算法學(xué)習(xí)之漢諾塔示例
這篇文章主要介紹了java數(shù)據(jù)結(jié)構(gòu)和算法中的漢諾塔示例,需要的朋友可以參考下2014-02-02
Spring Cloud Gateway網(wǎng)關(guān)XSS過濾方式
這篇文章主要介紹了Spring Cloud Gateway網(wǎng)關(guān)XSS過濾方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10
SpringBoot后端進(jìn)行數(shù)據(jù)校驗(yàn)JSR303的使用詳解
這篇文章主要介紹了SpringBoot后端進(jìn)行數(shù)據(jù)校驗(yàn)JSR303的使用詳解,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03

