Java 集合底層原理剖析(List、Set、Map、Queue)
溫馨提示:下面是以 Java 8 版本進行講解,除非有特定說明。
一、Java 集合介紹
Java 集合是一個存儲相同類型數據的容器,類似數組,集合可以不指定長度,但是數組必須指定長度。集合類主要從 Collection 和 Map 兩個根接口派生出來,比如常用的 ArrayList、LinkedList、HashMap、HashSet、ConcurrentHashMap 等等。
Collection 根接口框架結構圖:
Map 根接口框架結構圖:
二、List
2.1 ArrayList
ArrayList 是基於動態數組實現,容量能自動增長的集合。隨機訪問效率高,隨機插入、隨機刪除效率低。線程不安全,多線程環境下可以使用 Collections.synchronizedList(list) 函數返回一個線程安全的 ArrayList 類,也可以使用 concurrent 併發包下的 CopyOnWriteArrayList 類。
動態數組,是指當數組容量不足以存放新的元素時,會創建新的數組,然後把原數組中的內容複製到新數組。
主要屬性:
//存儲實際數據,使用transient修飾,序列化的時不會被保存
transient Object[] elementData;
//元素的數量,即容量。
private int size;
**數據結構:**動態數組
特徵:
- 允許元素爲 null;
- 查詢效率高、插入、刪除效率低,因爲大量 copy 原來元素;
- 線程不安全。
使用場景:
- 需要快速隨機訪問元素
- 單線程環境
add(element) 流程:
- 判斷當前數組是否爲空,如果是則創建長度爲 10(默認)的數組,因爲 new ArrayList 的時是沒有初始化;
- 判斷是否需要擴容,如果當前數組的長度加 1(即 size+1)後是否大於當前數組長度,則進行擴容 grow();
- 最後在數組末尾添加元素,並 size+1。
grow() 流程:
- 創建新數組,長度擴大爲原數組的 1.5 倍;
- 如果擴大 1.5 倍還是不夠,則根據實際長度來擴容,比如 addAll() 場景;
- 將原數組的數據使用 System.arraycopy(native 方法)複製到新數組中。
add(index,element) 流程:
- 檢查 index 是否在數組範圍內,假如數組長度是 2,則 index 必須 >=0 並且 <=2,否則報 IndexOutOfBoundsException 異常;
- 擴容檢查;
- 通過拷貝方式,把數組位置爲 index 至 size-1 的元素都往後移動一位,騰出位置之後放入元素,並 size+1。
set(index,element) 流程:
- 檢查 index 是否在數組範圍內,假如數組長度是 2,則 index 必須 >=0 並且 <2;
- 保留被覆蓋的值,因爲最後需要返回舊的值;
- 新元素覆蓋位置爲 index 的舊元素,返回舊值。
get(index) 流程:
- 判斷下標有沒有越界;
- 通過數組下標來獲取元素,get 的時間複雜度是 O(1)。
remove(index) 流程:
- 檢查指定位置是否在數組範圍內,假如數組長度是 2,則 index 必須 >=0 並且 < 2;
- 保留要刪除的值,因爲最後需要返回舊的值;
- 計算出需要移動元素個數,再通過拷貝使數組內位置爲 index+1 到 size-1 的元素往前移動一位,把數組最後一個元素設置爲 null(精闢小技巧),返回舊值。
注意事項:
- new ArrayList 創建對象時,如果沒有指定集合容量則初始化爲 0;如果有指定,則按照指定的大小初始化;
- 擴容時,先將集合擴大 1.5 倍,如果還是不夠,則根據實際長度來擴容,保證都能存儲所有數據,比如 addAll() 場景。
- 如果新擴容後數組長度大於(Integer.MAX_VALUE-8),則拋出 OutOfMemoryError。
2.2 LinkedList
LinkedList 是可以在任何位置進行插入和移除操作的有序集合,它是基於雙向鏈表實現的,線程不安全。LinkedList 功能比較強大,可以實現棧、隊列或雙向隊列。
主要屬性:
//鏈表長度
transient int size = 0;
//頭部節點
transient Node<E> first;
//尾部節點
transient Node<E> last;
/\*\* \* 靜態內部類,存儲數據的節點 \*/
private static class Node\<E\> {
//自身結點
E item;
//下一個節點
Node<E> next;
//上一個節點
Node<E> prev;
}
**數據結構:**雙向鏈表
特徵:
- 允許元素爲 null;
- 插入和刪除效率高,查詢效率低;
- 順序訪問會非常高效,而隨機訪問效率(比如 get 方法)比較低;
- 既能實現棧 Stack(後進先出),也能實現隊列(先進先出), 也能實現雙向隊列,因爲提供了 xxxFirst()、xxxLast() 等方法;
- 線程不安全。
使用場景:
- 需要快速插入,刪除元素
- 按照順序訪問其中的元素
- 單線程環境
add() 流程:
- 創建一個新結點,結點元素 item 爲傳入參數,前繼節點 prev 爲“當前鏈表 last 結點”,後繼節點 next 爲 null;
- 判斷當前鏈表 last 結點是否爲空,如果是則把新建結點作爲頭結點,如果不是則把新結點作爲 last 結點。
- 最後返回 true。
get(index,element) 流程:
- 檢查 index 是否在數組範圍內,假如數組長度是 2,則 index 必須 >=0 並且 < 2;
- index 小於“雙向鏈表長度的 1/2”則從頭開始往後遍歷查找,否則從鏈表末尾開始向前遍歷查找。
remove() 流程:
- 判斷 first 結點是否爲空,如果是則報 NoSuchElementException 異常;
- 如果不爲空,則把待刪除結點的 next 結點的 prev 屬性賦值爲 null,達到刪除頭結點的效果。
- 返回刪除值。
2.3 Vector
Vector 是矢量隊列,也是基於動態數組實現,容量可以自動擴容。跟 ArrayList 實現原理一樣,但是 Vector 是線程安全,使用 Synchronized 實現線程安全,性能非常差,已被淘汰,使用 CopyOnWriteArrayList 替代 Vector。
主要屬性:
//存儲實際數據
protected Object[] elementData;
//動態數組的實際大小
protected int elementCount;
//動態數組的擴容係數
protected int capacityIncrement;
**數據結構:**動態數組
特徵:
- 允許元素爲 null;
- 查詢效率高、插入、刪除效率低,因爲需要移動元素;
- 默認的初始化大小爲 10,沒有指定增長係數則每次都是擴容一倍,如果擴容後還不夠,則直接根據參數長度來擴容;
- 線程安全,性能差(Synchronized),使用 CopyOnWriteArrayList 替代 Vector。
**使用場景:**多線程環境
2.4 Stack
Stack 是棧,先進後出原則,Stack 繼承 Vector,也是通過數組實現,線程安全。因爲效率比較低,不推薦使用,可以使用 LinkedList(線程不安全)或者 ConcurrentLinkedDeque(線程安全)來實現先進先出的效果。
**數據結構:**動態數組
**構造函數:**只有一個默認 Stack()
**特徵:**先進後出
實現原理:
- Stack 執行 push() 時,將數據推進棧,即把數據追加到數組的末尾。
- Stack 執行 peek 時,取出棧頂數據,不刪除此數據,即獲取數組首個元素。
- Stack 執行 pop 時,取出棧頂數據,在棧頂刪除數據,即刪除數組首個元素。
- Stack 繼承於 Vector,所以 Vector 擁有的屬性和功能,Stack 都擁有,比如 add()、set() 等等。
2.5 CopyOnWriteArrayList
CopyOnWriteArrayList 是線程安全的 ArrayList,寫操作(add、set、remove 等等)時,把原數組拷貝一份出來,然後在新數組進行寫操作,操作完後,再將原數組引用指向到新數組。CopyOnWriteArrayList 可以替代 Collections.synchronizedList(List list)。
**數據結構:**動態數組
特徵:
- 線程安全;
- 讀多寫少,比如緩存;
- 不能保證實時一致性,只能保證最終一致性。
缺點:
- 寫操作,需要拷貝數組,比較消耗內存,如果原數組容量大的情況下,可能觸發頻繁的 Young GC 或者 Full GC;
- 不能用於實時讀的場景,因爲讀取到數據可能是舊的,可以保證最終一致性。
實現原理:
CopyOnWriteArrayList 寫操作加了鎖,不然多線程進行寫操作時會複製多個副本;讀操作沒有加鎖,所以可以實現併發讀,但是可能讀到舊的數據,比如正在執行讀操作時,同時有多個寫操作在進行,遇到這種場景時,就會都到舊數據。
2.6 CopyOnWriteArraySet
CopyOnWriteArraySet 是線程安全的無序並且不能重複的集合,可以認爲是線程安全的 HashSet,底層是通過 CopyOnWriteArrayList 機制實現。
**數據結構:**動態數組(CopyOnWriteArrayList),並不是散列表。
特徵:
- 線程安全
- 讀多寫少,比如緩存
- 不能存儲重複元素
2.7 ArrayList 和 Vector 區別
- Vector 線程安全,ArrayList 線程不安全;
- ArrayList 在擴容時默認是擴展 1.5 倍,Vector 是默認擴展 1 倍;
- ArrayList 支持序列化,Vector 不支持;
- Vector 提供 indexOf(obj, start) 接口,ArrayList 沒有;
- Vector 構造函數可以指定擴容增加係數,ArrayList 不可以。
2.8 ArrayList 與 LinkedList 的區別
- ArrayList 的數據結構是動態數組,LinkedList 的數據結構是鏈表;
- ArrayList 不支持高效的插入和刪除元素,LinkedList 不支持高效的隨機訪問元素;
- ArrayList 的空間浪費在數組末尾預留一定的容量空間,LinkedList 的空間浪費在每一個結點都要消耗空間來存儲 prev、next 等信息。
三、Map
3.1 HashMap
HashMap 是以key-value 鍵值對形式存儲數據,允許 key 爲 null(多個則覆蓋),也允許 value 爲 null。底層結構是數組 + 鏈表 + 紅黑樹。
主要屬性:
- initialCapacity:初始容量,默認 16,2 的 N 次方。
- loadFactor:負載因子,默認 0.75,用於擴容。
- threshold:閾值,等於 initialCapacity * loadFactor,比如:16 * 0.75 = 12。
- size:存放元素的個數,非 Node 數組長度。
- Node
//存儲元素的數組
transient Node<K,V>[] table;
//存放元素的個數,非Node數組長度
transient int size;
//記錄結構性修改次數,用於快速失敗
transient int modCount;
//閾值
int threshold;
//負載因子,默認0.75,用於擴容
final float loadFactor;
/\*\* \* 靜態內部類,存儲數據的節點 \*/
static class Node\<K,V\> implements Map.Entry\<K,V\> {
//節點的hash值
final int hash;
//節點的key值
final K key;
//節點的value值
V value;
//下一個節點的引用
Node<K,V> next;
}
**數據結構:**數組 + 單鏈表,Node 結構:hash|key|value|next
**只允許一個 key 爲 Null(多個則覆蓋),但允許多個 value 爲 Null **
- 查詢、插入、刪除效率都高(集成了數組和單鏈表的特性)
** * 默認的初始化大小爲 16,之後每次擴充爲原來的 2 倍 - 線程不安全
使用場景:
- 快速增刪改查
- 隨機存取
- 緩存
哈希衝突的解決方案:
- 開放定址法
- 再散列函數法
- 鏈地址法(拉鍊法,常用)
put() 存儲的流程(Java 8):
- 計算待新增數據 key 的 hash 值;
- 判斷 Node[] 數組是否爲空或者數據長度爲 0 的情況,則需要進行初始化;
- 根據 hash 值通過位運算定計算出 Node 數組的下標,判斷該數組第一個 Node 節點是否有數據,如果沒有數據,則插入新值;
- 如果有數據,則根據具體情況進行操作,如下:
- 如果該 Node 結點的 key(即鏈表頭結點)與待新增的 key 相等(== 或者 equals),則直接覆蓋值,最後返回舊值;
- 如果該結構是樹形,則按照樹的方式插入新值;
- 如果是鏈表結構,則判斷鏈表長度是否大於閾值 8,如果 >=8 並且數組長度 >=64 才轉爲紅黑樹,如果 >=8 並且數組長度 < 64 則進行擴容;
- 如果不需要轉爲紅黑樹,則遍歷鏈表,如果找到 key 和 hash 值同時相等,則進行覆蓋返回舊值,如果沒有找到,則將新值插入到鏈表的最後面(尾插法);
- 判斷數組長度是否大於閾值,如果是則進入擴容階段。
resize() 擴容的流程(Java 8):
擴容過程比較複雜, 遷移算法與 Java 7 不一樣,Java 8 不需要每個元素都重新計算 hash,遷移過程中元素的位置要麼是在原位置,要麼是在原位置再移動 2 次冪的位置。
get() 查詢的流程(Java 8):
- 根據 put() 方法的方式計算出數組的下標;
- 遍歷數組下標對應的鏈表,如果找到 key 和 hash 值同時相等就返回對應的值,否則返回 null。
get() 注意事項:Java 8 沒有把 key 爲 null 放到數組 table[0] 中。
remove() 刪除的流程(Java 8):
- 根據 get() 方法的方式計算出數組的下標,即定位到存儲刪除元素的 Node 結點;
- 如果待刪結點是頭節點,則用它的 next 結點頂替它作爲頭節點;
- 如果待刪結點是紅黑樹結點,則直接調用紅黑樹的刪除方法進行刪除;
- 如果待刪結點是鏈表中的一個節點,則用待刪除結點的前一個節點的 next 屬性指向它的 next 結點;
- 如果刪除成功則返回被刪結點的 value,否則返回 null。
remove() 注意事項:刪除單個 key,注意返回是的鍵值對中的 value。
爲什麼使用位運算(&)來代替取模運算(%):
- 效率高,位運算直接對內存數據進行操作,不需轉成十進制,因此處理速度非常快;
- 可以解決負數問題,比如:-17 % 10 = -7。
HashMap 在 Java 7 和 Java 8 中的區別:
- 存放數據的結點名稱不同,作用都一樣,存的都是 hashcode、key、value、next 等數據:
- Java 7:使用 Entry 存放數據
- Java 8:改名爲 Node
- 定位數組下標位置方法不同:
- Java 7:計算 key 的 hash,將 hash 值進行了四次擾動,再進行取模得出;
- Java 8:計算 key 的 hash,將 hash 值進行高 16 位異或低 16 位,再進行與運算得出。
- 擴容算法不同:
- Java 7:擴容要重新計算 hash
- Java 8:不用重新計算
- put 方法插入鏈表位置不同:
- Java 7:頭插法
- Java 8:尾插法
- Java 8 引入了紅黑樹,當鏈表長度 >=8 時,並且同時數組的長度 >=64 時,鏈表就轉換爲紅黑樹,利用紅黑樹快速增刪改查的特點提高 HashMap 的性能。
3.2 HashTable
和 HashMap 一樣,Hashtable 也是一個哈希散列表,Hashtable 繼承於 Dictionary,使用重入鎖 Synchronized 實現線程安全,key 和 value 都不允許爲 Null。HashTable 已被高性能的 ConcurrentHashMap 代替。
主要屬性:
- initialCapacity:初始容量,默認 11。
- loadFactor:負載因子,默認 0.75。
- threshold:閾值。
- modCount:記錄結構性修改次數,用於快速失敗。
//真正存儲數據的數組
private transient Entry<?,?>[] table;
//存放元素的個數,非Entry數組長度
private transient int count;
//閾值
private int threshold;
//負載因子,默認0.75
private float loadFactor;
//記錄結構性修改次數,用於快速失敗
private transient int modCount = 0;
/\*\* \* 靜態內部類,存儲數據的節點 \*/
private static class Entry\<K,V\> implements Map.Entry\<K,V\> {
//節點的hash值
final int hash;
//節點的key值
final K key;
//節點的value值
V value;
//下一個節點的引用
Entry<K,V> next;
}
快速失敗原理是在併發場景下進行遍歷操作時,如果有另外一個線程對它執行了寫操作,此時迭代器可以發現並拋出 ConcurrentModificationException,而不需等到遍歷完後才報異常。
**數據結構:**鏈表的數組,數組 + 鏈表,Entry 結構:hash|key|value|next
特徵:
- key 和 value 都不允許爲 Null;
- HashTable 默認的初始大小爲 11,之後每次擴充爲原來的 2 倍;
- 線程安全。
原理:
與 HashMap 不一樣的流程是定位數組下標邏輯,HashTable 是在 key.hashcode() 後使用取模,HashMap 是位運算。HashTable 是 put() 之前進行判斷是否擴容 resize(),而 HashMap 是 put() 之後擴容。
3.3 ConcurrentHashMap
ConcurrentHashMap 在 Java 8 版本中丟棄了 Segment(分鎖段)、ReentrantLock、HashEntry 數組這些概念,而是採用CAS + Synchronized 實現鎖操作,Node 改名爲 HashEntry,引入了紅黑樹保證查詢效率,底層數據結構由數組 + 鏈表 + 紅黑樹組成,Node 數組默認爲 16。數據結構如下圖:
數據結構(Java 8):Node[] 數組 + 單鏈表 + 紅黑樹
- 判斷待新增數據 key 和 value 是否爲空,如果是則拋空指針異常;
** * 計算待新增數據 key 的 hash 值; - 判斷 Node[] 數組是否爲空或者數據長度爲 0 的情況,則需要進行初始化;
- 根據 hash 值通過位運算定計算出 Node 數組的下標,判斷該數組第一個 Node 節點是否有數據,如果沒有數據,則使用 CAS 操作將這個新值插入;
- 如果有數據,則判斷頭結點的 hashCode 是否等於 MOVED(即 -1),即檢查是否正在擴容,如果等於 - 1 則幫助擴容;
- 如果有數據,則對頭結點進行加鎖(synchronized),如果頭結點的 hashCode>=0,說明是鏈表,遍歷鏈表,如果找到 key 和 hash 值同時相等,則進行覆蓋,如果沒有找到,則將新值插入到鏈表的最後面(尾插法);如果 hashCode<0,說明是紅黑樹,調用紅黑樹的插值方法插入新節點;
- 插值完成之後,判斷鏈表元素是否 >=8,如果 >=8 並且數組長度 >=64 才轉爲紅黑樹,如果 >=8 並且數組長度 < 64 則進行擴容。
resize() 擴容的流程(Java 8):
擴容的原理是創建新的數組,長度是原來的兩倍,然後把舊數組數據遷移到新的數組中,在多線程情況下,需要注意線程安全問題,在解決安全問題的同時,還需要關注其效率。
get() 查詢的流程(Java 8):
- 計算獲取數據 key 的 hash 值;
- 根據 hashCode 通過位運算定得到 Node 數組的下標,即得到頭節點;
- 如果頭結點爲空,則返回 null;
- 如果頭結點的 key 與參數 key 可以相等,則返回頭結點的值;
- 如果頭結點的 hashCode 小於 0,說明是紅黑樹,則調用 find() 方法按照樹的方式獲取值;
- 如果都不滿足 3、4 和 5 條件,說明是鏈表,則按照鏈表的方式遍歷獲取值。整個過程都不需要加鎖。
get() 注意事項:
整個過程都不需要加鎖,因爲讀取數據的屬性使用 volatile 修飾,實現線程可見性。
remove() 刪除的流程(Java 8):
- 計算待新增數據 key 的 hash 值;
- 判斷 Node[] 數組是否爲空或者數據長度爲 0 的情況,如果是則返回 null;如果不是則根據 hashCode 通過位運算定得到 Node 數組的下標,即得到頭結點;
- 判斷頭結點的 hashCode 是否等於 MOVED(即 -1),檢查是否正在擴容,如果是則幫助擴容;
- 如果都不滿足 2 和 3 條件,則加鎖進行刪除操作;
- 首先判斷頭結點有無發生變化(步驟 3 點轉移操作會改變頭結點),如果有改變則返回 null;
- 如果頭結點的 hashCode 大於 0 說明是鏈表,則按照鏈表方式遍歷刪值;
- 如果頭結點是 TreeBin 類型,說明是紅黑樹,則按照樹的方式刪值。
remove() 注意事項:
remove 函數底層是調用 replaceNode 函數實現結點的刪除。
**使用場景:**併發、線程不安全場景
ConcurrentHashMap 在 Java 7 和 Java 8 中的區別:
- Java 7 使用 Segment 分段加鎖機制,Segment 繼承 ReentrantLock 實現鎖操作,而 Java 8 使用 CAS + Synchronized 實現鎖操作;
- Java 7 查詢時遍歷鏈表效率低,而 Java 8 採用紅黑樹提高查詢效率;
- Java 7 使用 HashEntry 存放數據,而 Java 8 改名爲 Node,作用都是一樣,存放的都是 hashcode、key、value、next 等數據。
- Java 7 是把數據插入到鏈表的表頭(頭插法),而 Java 8 是將數據插入到鏈表的最後面(尾插法);
- Java 7 先擴容,再插值,而 Java 8 先插值,再擴容;
- Java 7 的最大併發個數是 Segment 的個數默認值是 16,鎖住整個段,不影響其他段;而 Java 8 去掉了分段鎖,更細粒度,只鎖住一個 Node 節點,不影響其他 Node 節點;
- Java 7 在擴容時鎖住一個段,當前段可讀不能寫,其他段可讀寫,只開啓一個線程執行擴容操作;而 Java 8 鎖住一個 Node 結點,當前結點可讀不能寫,其他結點可讀寫,1 個線程執行擴容 + 可能多個 put()/remove 線程幫助擴容。
HashMap、Hashtable、ConccurentHashMap 三者的區別(Java 8):
- HashMap 線程不安全,沒有鎖機制,數組 + 鏈表 + 紅黑樹
- Hashtable 線程安全,鎖住整個對象,數組 + 鏈表
- ConccurentHashMap 線程安全,CAS+Synchronized,數組 + 鏈表 + 紅黑樹
- HashMap 的 key 和 value 都可爲 null,其他兩個都不可以。
3.4 TreeMap
TreeMap 實現了 SotredMap 接口,意味着可以排序,是一個有序的集合。底層數據結構是紅黑樹結構,TreeMap 中的每個元素都存放在紅黑樹的節點上,默認使用自然排序,也可以自定排序,線程不安全。
主要屬性:
//排序比較器
private final Comparator<? super K> comparator;
//紅黑樹根節點
private transient Entry<K,V> root;
//集合長度
private transient int size = 0;
//記錄結構性修改次數,用於快速失敗
private transient int modCount = 0;
//紅黑樹常量
private static final boolean RED = false;
private static final boolean BLACK = true;
/\*\* \* 實際存儲數據的節點 \*/
static final class Entry\<K,V\> implements Map.Entry\<K,V\> {
//節點的key值
K key;
//節點的value值
V value;
//左子樹引用
Entry<K,V> left;
//右子樹引用
Entry<K,V> right;
//父節點引用
Entry<K,V> parent;
//節點顏色(默認黑色)
boolean color = BLACK;
}
**數據結構:**紅黑樹(高效的檢索二叉樹),Entry 結構:key|value|left|right|parent|color
特徵:
- key 不允許爲 Null,但允許多個 value 爲 Null
- 線程不安全
put() 存儲的流程:
主要分爲兩個步驟,構建排序二叉樹和構建平衡二叉樹。
構建排序二叉樹,過程如下:
- 從根節點 root 開始查找;
- 如果 root 節點比待插入節點值小,則在 root 節點左子樹查找,如果大於,則在右子樹查找;
- 遞歸循環步驟 2,找到合適的節點爲止;
- 把待插入節點與步驟 3 中找到的節點進行比較,如果待插入節點小於找到節點,則把待插入節點作爲左子節點;否則作爲右子節點。
舉個栗子:put(9),步驟如下:
**
** * 從 root 節點 8 開始檢索;
- 如果 8 小於 9,則從 8 的右子樹繼續找,10 大於 9,10 沒有左子樹;
- 循環遞歸步驟 2,找到 10 這個合適節點;
**1. 10box-sizing:border-box;outline:0px;">構建平衡二叉樹,經過左旋、右旋、着色這些步驟後,得到一棵平衡二叉樹。
remove() 的流程:比 put() 複雜,也是分兩個步驟,刪除節點和着色旋轉。
刪除節點,刪除時出現以下 3 種情況:
- 待刪除節點,如果沒有左和右子節點時,則直接刪除;
- 待刪除節點,如果有一個子節點時,則把它的子節點指向它的上級節點(即父節點);
- 待刪除節點,如果有兩個非空的子節點時,流程複雜,暫不在此解釋。
着色旋轉,進行顏色的對調和旋轉,達到紅黑樹的特徵。
3.5 LinkedHashMap
LinkedHashMap 是使用 HashMap 機制實現。LinkedHashMap 在 HashMap 的基礎上增加 before 和 after 兩個屬性來保證了迭代順序。迭代順序可以是插入順序(默認),也可以是訪問順序。線程不安全。
主要屬性:
/\*\* \* 靜態內部類,存儲數據的節點 \*/
static class Entry\<K,V\> extends HashMap.Node\<K,V\> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
//頭結點
transient LinkedHashMap.Entry<K,V> head;
//尾結點
transient LinkedHashMap.Entry<K,V> tail;
//排序標識,true訪問順序迭代; false插入順序迭代(默認)
final boolean accessOrder;
**數據結構:**數組 + 雙向鏈表,Entry 結構:before|hash|key|value|next|after,before 和 after 用於維護整個雙向鏈表。
特徵:
- 只允許一個 key 爲 Null(多個則覆蓋),允許多個 value 爲 Null;
- 線程不安全。
原理:
LinkedHashMap 繼承了 HashMap,hash 算法也是跟 HashMap 一致。LinkedHashMap 在 Entry 中新增了 before 和 after 兩個屬性來維護雙向鏈表的迭代順序。Entry 的 next 屬性是維護 Entry 連接順序,而 after 是維護迭代順序。LinkedHashMap 使用 LRU 算法實現訪問順序排序,比如 get() 操作後的元素會移動到鏈表末尾,從而實現按訪問順序迭代。
**使用場景:**保證插入和訪問順序
插入順序(默認)和訪問順序區別,請看下面的代碼:
public static void main(String[] args) {
LinkedHashMap<String, String> map1 = new LinkedHashMap<>();
map1.put("name", "joseph");
map1.put("age", "33");
map1.put("job", "敲代碼");
map1.put("hobby", "打籃球");
map1.get("job");
System.out.println("----start----按插入順序遍歷----");
Iterator<Map.Entry<String, String>> iterator = map1.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
System.out.println("key=" + entry.getKey() + ", value=" + entry.getValue());
}
System.out.println("-----end----按插入順序遍歷-----");
System.out.println();
System.out.println("----start----按訪問順序遍歷----");
LinkedHashMap<String, String> map2 = new LinkedHashMap<>(16, 0.75f, true);
map2.put("name", "joseph");
map2.put("age", "33");
map2.put("job", "敲代碼");
map2.put("hobby", "打籃球");
map2.get("job");
Iterator<Map.Entry<String, String>> iterator2 = map2.entrySet().iterator();
while (iterator2.hasNext()) {
Map.Entry<String, String> entry = iterator2.next();
System.out.println("key=" + entry.getKey() + ", value=" + entry.getValue());
}
System.out.println("----end----按訪問順序遍歷-----");
}
輸出結果:
—-start—- 按插入順序遍歷—-
key=name, value=joseph
key=age, value=33
key=job, value = 敲代碼
key=hobby, value = 打籃球
—–end—- 按插入順序遍歷—–
—-start—- 按訪問順序遍歷—-
key=name, value=joseph
key=age, value=33
key=hobby, value = 打籃球
key=job, value = 敲代碼
—-end—- 按訪問順序遍歷—–
3.6 WeakHashMap
WeakHashMap 中的 Weak 是“弱”的含義,即弱化版的 HashMap。key 和 value 都允許爲 null,線程不安全。
3.7 HashMap 與 Hashtable 的區別
- HashMap 只允許一個 key 爲 Null(多個則覆蓋),允許多個 value 爲 Null,HashTable 中的 key 和 value 都不允許爲 null;
- HashMap 線程不安全,效率高,HashTable 線程安全,效率低;
- HashMap 和 Hashtable 二者都實現了 Map 接口,HashMap 是繼承於 AbstractMap,HashTable 繼承於 Dictionary 類;
- 定位存儲位置邏輯不一樣,HashMap 是在 key.hashcode() 後使用位運算,HashTable 是在 key.hashcode() 後使用取模;
- 判斷擴容順序不一樣,HashMap 是 put() 之後擴容,HashTabel 是 put() 之前擴容;
- 初始容量不一樣,HashMap 默認 16,HashTabel 默認 11。
3.8 HashMap 與 TreeMap 的區別
- 數據結構不一樣,HashMap 基於“數組 + 單鏈表”實現(達到一定條件時轉爲紅黑樹),TreeMap 基於紅黑樹實現 ;
- HashMap 隨機存儲,TreeMap 默認按 key 的字典升序排序;
- HashMap 只允許一個 key 爲 Null(多個則覆蓋),允許多個 value 爲 Null,TreeMap 的 key 不允許爲 Null,但允許多個 value 爲 Null;
- HashMap 效率高,TreeMap 效率低。
四、Set
4.1 HashSet
HashSet 是用來存儲沒有重複元素的集合類,並且是無序的。實現了 Set 接口。底層使用 HashMap 機制實現,所以也是線程不安全。
主要屬性:
//定義了一個HashMap類型的成員變量,即擁有HashMap的所有屬性
private transient HashMap<E,Object> map;
特徵:
- 不可重複
- 無序
- 線程不安全
- 集合元素可以是 null,但只能放入一個 null
**使用場景:**去重、不要求順序
**原理:**底層使用 HashMap 的 key 不能重複機制來實現沒有重複的 HashSet。
4.2 TreeSet
TreeSet 實現了 SortedSet 接口,意味着可以排序,它是一個有序並且沒有重複的集合類,底層是通過 TreeMap 實現。TreeSet 並不是根據插入的順序來排序,而是字典自然排序。線程不安全。
TreeSet 支持兩種排序方式:自然升序排序和自定義排序。
特徵:
- 不可重複
- 有序,默認自然升序排序
- 線程不安全
- 集合元素不可以爲 null
原理:
TreeSet 底層是基於 treeMap(紅黑樹結構)實現的,可以自定義比較器對元素進行排序,或是使用元素的自然順序。
**使用場景:**去重、要求排序
4.3 LinkedHashSet
LinkedHashSet 是使用 HashSet 機制實現,它是一個可以保證插入順序或是訪問順序,並且沒有重複的集合類。線程不安全。
**數據結構:**數組 + 雙向鏈表,Entry 結構:before|hash|key|value|next|after,before 和 after 用於維護整個雙向鏈表。
特徵:
- 集合元素不可以爲 null;
- 線程不安全。
原理:
LinkedHashSet 底層使用了 LinkedHashMap 機制(比如 before 和 after),加上又繼承了 HashSet,所以可以實現既可以保證迭代順序,又可以達到不出現重複元素。
**使用場景:**去重、需要保證插入或者訪問順序
4.4 HashSet、TreeSet、LinkedHashSet 的區別
HashSet,TreeSet,LinkedHashSet 之間的區別:HashSet 只去重,TreeSet 去重並排序,LinkedHashSet 去重並保證迭代順序。
五、隊列(Queue)
Queue 是一個**先入先出(FIFO)**的集合,它有 3 種實現方式:阻塞隊列、非阻塞隊列、雙向隊列。Queue 跟 List、Set 一樣,也是繼承了 Collection 接口。
5.1 阻塞隊列
阻塞隊列是一個可以阻塞的先進先出集合,比如某個線程在空隊列獲取元素時、或者在已存滿隊列存儲元素時,都會被阻塞。BlockingQueue 接口常用的實現類如下:
- ArrayBlockingQueue :基於數組的有界阻塞隊列,必須指定大小。
- LinkedBlockingQueue :基於單鏈表的無界阻塞隊列,不需指定大小。
- DelayQueue:基於延遲、優先級、無界阻塞隊列。
- SynchronousQueue :基於 CAS 的阻塞隊列。
常用方法:
- add():新增一個元索,假如隊列已滿,則拋異常。
- offer():新增一個元素,假如隊列沒滿則返回 true,假如隊列已滿,則返回 false。
- put():新增一個元素,假如隊列滿,則阻塞。
- element():獲取隊列頭部一個元素,假如隊列爲空,則拋異常。
- peek():獲取隊列頭部一個元素,假如隊列爲空,則返回 null。
- remove():執行刪除操作,返回隊列頭部的元素,假如隊列爲空,則拋異常。
- poll():執行刪除操作,返回隊列頭部的元素,假如隊列爲空,則返回 null。
- take():執行刪除操作,返回隊列頭部的元素,假如隊列爲空,則阻塞。
5.2 非阻塞隊列
非阻塞隊列是使用**CAS(compare and set)**機制實現,類似 volatile,併發性能好。常用的阻塞隊列有 PriorityQueue 和 ConcurrentLinkedQueue。
- PriorityQueue :基於優先級的無界優先級隊列
- ConcurrentLinkedDeque:基於雙向鏈表結構的無界併發隊列。
5.3 雙端隊列(Deque)
Deque 是一個既可以在頭部操作元素,又可以爲尾部操作元素,俗稱爲雙向(雙端)隊列。Deque 繼承自 Queue,Deque 實現類有 LinkedList、 ArrayDeque、ConcurrentLinkedDeque 等等。
- LinkedList:基於單鏈表的無界雙端隊列,允許元素爲 null。
- ArrayDeque:基於數組的有界雙端隊列,不允許 null。
PS:如有寫錯請指正,感謝您閱讀。
歡迎關注我的公衆號,回覆關鍵字“Java” ,將會有大禮相送!!! 祝各位面試成功!!!