Java 集合底層原理剖析(List、Set、Map、Queue)

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;

**數據結構:**動態數組

特徵:

  1. 允許元素爲 null;
  2. 查詢效率高、插入、刪除效率低,因爲大量 copy 原來元素;
  3. 線程不安全。

使用場景:

  1. 需要快速隨機訪問元素
  2. 單線程環境

add(element) 流程:

  1. 判斷當前數組是否爲空,如果是則創建長度爲 10(默認)的數組,因爲 new ArrayList 的時是沒有初始化;
  2. 判斷是否需要擴容,如果當前數組的長度加 1(即 size+1)後是否大於當前數組長度,則進行擴容 grow();
  3. 最後在數組末尾添加元素,並 size+1。

grow() 流程:

  1. 創建新數組,長度擴大爲原數組的 1.5 倍;
  2. 如果擴大 1.5 倍還是不夠,則根據實際長度來擴容,比如 addAll() 場景;
  3. 將原數組的數據使用 System.arraycopy(native 方法)複製到新數組中。

add(index,element) 流程:

  1. 檢查 index 是否在數組範圍內,假如數組長度是 2,則 index 必須 >=0 並且 <=2,否則報 IndexOutOfBoundsException 異常;
  2. 擴容檢查;
  3. 通過拷貝方式,把數組位置爲 index 至 size-1 的元素都往後移動一位,騰出位置之後放入元素,並 size+1。

set(index,element) 流程:

  1. 檢查 index 是否在數組範圍內,假如數組長度是 2,則 index 必須 >=0 並且 <2;
  2. 保留被覆蓋的值,因爲最後需要返回舊的值;
  3. 新元素覆蓋位置爲 index 的舊元素,返回舊值。

get(index) 流程:

  1. 判斷下標有沒有越界;
  2. 通過數組下標來獲取元素,get 的時間複雜度是 O(1)。

remove(index) 流程:

  1. 檢查指定位置是否在數組範圍內,假如數組長度是 2,則 index 必須 >=0 並且 < 2;
  2. 保留要刪除的值,因爲最後需要返回舊的值;
  3. 計算出需要移動元素個數,再通過拷貝使數組內位置爲 index+1 到 size-1 的元素往前移動一位,把數組最後一個元素設置爲 null(精闢小技巧),返回舊值。

注意事項:

  1. new ArrayList 創建對象時,如果沒有指定集合容量則初始化爲 0;如果有指定,則按照指定的大小初始化;
  2. 擴容時,先將集合擴大 1.5 倍,如果還是不夠,則根據實際長度來擴容,保證都能存儲所有數據,比如 addAll() 場景。
  3. 如果新擴容後數組長度大於(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;
}

**數據結構:**雙向鏈表

特徵:

  1. 允許元素爲 null;
  2. 插入和刪除效率高,查詢效率低;
  3. 順序訪問會非常高效,而隨機訪問效率(比如 get 方法)比較低;
  4. 既能實現棧 Stack(後進先出),也能實現隊列(先進先出), 也能實現雙向隊列,因爲提供了 xxxFirst()、xxxLast() 等方法;
  5. 線程不安全。

使用場景:

  1. 需要快速插入,刪除元素
  2. 按照順序訪問其中的元素
  3. 單線程環境

add() 流程:

  1. 創建一個新結點,結點元素 item 爲傳入參數,前繼節點 prev 爲“當前鏈表 last 結點”,後繼節點 next 爲 null;
  2. 判斷當前鏈表 last 結點是否爲空,如果是則把新建結點作爲頭結點,如果不是則把新結點作爲 last 結點。
  3. 最後返回 true。

get(index,element) 流程:

  1. 檢查 index 是否在數組範圍內,假如數組長度是 2,則 index 必須 >=0 並且 < 2;
  2. index 小於“雙向鏈表長度的 1/2”則從頭開始往後遍歷查找,否則從鏈表末尾開始向前遍歷查找。

remove() 流程:

  1. 判斷 first 結點是否爲空,如果是則報 NoSuchElementException 異常;
  2. 如果不爲空,則把待刪除結點的 next 結點的 prev 屬性賦值爲 null,達到刪除頭結點的效果。
  3. 返回刪除值。

2.3 Vector

Vector 是矢量隊列,也是基於動態數組實現,容量可以自動擴容。跟 ArrayList 實現原理一樣,但是 Vector 是線程安全,使用 Synchronized 實現線程安全,性能非常差,已被淘汰,使用 CopyOnWriteArrayList 替代 Vector

主要屬性:

//存儲實際數據
protected Object[] elementData;
//動態數組的實際大小
protected int elementCount;
//動態數組的擴容係數
protected int capacityIncrement;

**數據結構:**動態數組

特徵:

  1. 允許元素爲 null;
  2. 查詢效率高、插入、刪除效率低,因爲需要移動元素;
  3. 默認的初始化大小爲 10,沒有指定增長係數則每次都是擴容一倍,如果擴容後還不夠,則直接根據參數長度來擴容;
  4. 線程安全,性能差(Synchronized),使用 CopyOnWriteArrayList 替代 Vector。

**使用場景:**多線程環境

2.4 Stack

Stack 是棧,先進後出原則,Stack 繼承 Vector,也是通過數組實現,線程安全。因爲效率比較低,不推薦使用,可以使用 LinkedList(線程不安全)或者 ConcurrentLinkedDeque(線程安全)來實現先進先出的效果。

**數據結構:**動態數組

**構造函數:**只有一個默認 Stack()

**特徵:**先進後出

實現原理:

  1. Stack 執行 push() 時,將數據推進棧,即把數據追加到數組的末尾。
  2. Stack 執行 peek 時,取出棧頂數據,不刪除此數據,即獲取數組首個元素。
  3. Stack 執行 pop 時,取出棧頂數據,在棧頂刪除數據,即刪除數組首個元素。
  4. Stack 繼承於 Vector,所以 Vector 擁有的屬性和功能,Stack 都擁有,比如 add()、set() 等等。

2.5 CopyOnWriteArrayList

CopyOnWriteArrayList 是線程安全的 ArrayList,寫操作(add、set、remove 等等)時,把原數組拷貝一份出來,然後在新數組進行寫操作,操作完後,再將原數組引用指向到新數組。CopyOnWriteArrayList 可以替代 Collections.synchronizedList(List list)。

**數據結構:**動態數組

特徵:

  1. 線程安全;
  2. 讀多寫少,比如緩存;
  3. 不能保證實時一致性,只能保證最終一致性。

缺點:

  1. 寫操作,需要拷貝數組,比較消耗內存,如果原數組容量大的情況下,可能觸發頻繁的 Young GC 或者 Full GC;
  2. 不能用於實時讀的場景,因爲讀取到數據可能是舊的,可以保證最終一致性。

實現原理:

CopyOnWriteArrayList 寫操作加了鎖,不然多線程進行寫操作時會複製多個副本;讀操作沒有加鎖,所以可以實現併發讀,但是可能讀到舊的數據,比如正在執行讀操作時,同時有多個寫操作在進行,遇到這種場景時,就會都到舊數據。

2.6 CopyOnWriteArraySet

CopyOnWriteArraySet 是線程安全的無序並且不能重複的集合,可以認爲是線程安全的 HashSet,底層是通過 CopyOnWriteArrayList 機制實現。

**數據結構:**動態數組(CopyOnWriteArrayList),並不是散列表。

特徵:

  1. 線程安全
  2. 讀多寫少,比如緩存
  3. 不能存儲重複元素

2.7 ArrayList 和 Vector 區別

  1. Vector 線程安全,ArrayList 線程不安全;
  2. ArrayList 在擴容時默認是擴展 1.5 倍,Vector 是默認擴展 1 倍;
  3. ArrayList 支持序列化,Vector 不支持;
  4. Vector 提供 indexOf(obj, start) 接口,ArrayList 沒有;
  5. Vector 構造函數可以指定擴容增加係數,ArrayList 不可以。

2.8 ArrayList 與 LinkedList 的區別

  1. ArrayList 的數據結構是動態數組,LinkedList 的數據結構是鏈表;
  2. ArrayList 不支持高效的插入和刪除元素,LinkedList 不支持高效的隨機訪問元素;
  3. 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 倍
  • 線程不安全

使用場景:

  1. 快速增刪改查
  2. 隨機存取
  3. 緩存

哈希衝突的解決方案:

  1. 開放定址法
  2. 再散列函數法
  3. 鏈地址法(拉鍊法,常用)

put() 存儲的流程(Java 8):

  1. 計算待新增數據 key 的 hash 值;
  2. 判斷 Node[] 數組是否爲空或者數據長度爲 0 的情況,則需要進行初始化;
  3. 根據 hash 值通過位運算定計算出 Node 數組的下標,判斷該數組第一個 Node 節點是否有數據,如果沒有數據,則插入新值;
  4. 如果有數據,則根據具體情況進行操作,如下:
  • 如果該 Node 結點的 key(即鏈表頭結點)與待新增的 key 相等(== 或者 equals),則直接覆蓋值,最後返回舊值;
  • 如果該結構是樹形,則按照樹的方式插入新值;
  • 如果是鏈表結構,則判斷鏈表長度是否大於閾值 8,如果 >=8 並且數組長度 >=64 才轉爲紅黑樹,如果 >=8 並且數組長度 < 64 則進行擴容;
  • 如果不需要轉爲紅黑樹,則遍歷鏈表,如果找到 key 和 hash 值同時相等,則進行覆蓋返回舊值,如果沒有找到,則將新值插入到鏈表的最後面(尾插法);
  1. 判斷數組長度是否大於閾值,如果是則進入擴容階段。

resize() 擴容的流程(Java 8):

擴容過程比較複雜, 遷移算法與 Java 7 不一樣,Java 8 不需要每個元素都重新計算 hash,遷移過程中元素的位置要麼是在原位置,要麼是在原位置再移動 2 次冪的位置。

get() 查詢的流程(Java 8):

  1. 根據 put() 方法的方式計算出數組的下標;
  2. 遍歷數組下標對應的鏈表,如果找到 key 和 hash 值同時相等就返回對應的值,否則返回 null。

get() 注意事項:Java 8 沒有把 key 爲 null 放到數組 table[0] 中。

remove() 刪除的流程(Java 8):

  1. 根據 get() 方法的方式計算出數組的下標,即定位到存儲刪除元素的 Node 結點;
  2. 如果待刪結點是頭節點,則用它的 next 結點頂替它作爲頭節點;
  3. 如果待刪結點是紅黑樹結點,則直接調用紅黑樹的刪除方法進行刪除;
  4. 如果待刪結點是鏈表中的一個節點,則用待刪除結點的前一個節點的 next 屬性指向它的 next 結點;
  5. 如果刪除成功則返回被刪結點的 value,否則返回 null。

remove() 注意事項:刪除單個 key,注意返回是的鍵值對中的 value。

爲什麼使用位運算(&)來代替取模運算(%):

  1. 效率高,位運算直接對內存數據進行操作,不需轉成十進制,因此處理速度非常快;
  2. 可以解決負數問題,比如:-17 % 10 = -7。

HashMap 在 Java 7 和 Java 8 中的區別:

  1. 存放數據的結點名稱不同,作用都一樣,存的都是 hashcode、key、value、next 等數據:
  • Java 7:使用 Entry 存放數據
  • Java 8:改名爲 Node
  1. 定位數組下標位置方法不同:
  • Java 7:計算 key 的 hash,將 hash 值進行了四次擾動,再進行取模得出;
  • Java 8:計算 key 的 hash,將 hash 值進行高 16 位異或低 16 位,再進行與運算得出。
  1. 擴容算法不同:
  • Java 7:擴容要重新計算 hash
  • Java 8:不用重新計算
  1. put 方法插入鏈表位置不同:
  • Java 7:頭插法
  • Java 8:尾插法
  1. 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

特徵:

  1. key 和 value 都不允許爲 Null;
  2. HashTable 默認的初始大小爲 11,之後每次擴充爲原來的 2 倍;
  3. 線程安全。

原理:

與 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):

  1. 計算獲取數據 key 的 hash 值;
  2. 根據 hashCode 通過位運算定得到 Node 數組的下標,即得到頭節點;
  3. 如果頭結點爲空,則返回 null;
  4. 如果頭結點的 key 與參數 key 可以相等,則返回頭結點的值;
  5. 如果頭結點的 hashCode 小於 0,說明是紅黑樹,則調用 find() 方法按照樹的方式獲取值;
  6. 如果都不滿足 3、4 和 5 條件,說明是鏈表,則按照鏈表的方式遍歷獲取值。整個過程都不需要加鎖。

get() 注意事項:

整個過程都不需要加鎖,因爲讀取數據的屬性使用 volatile 修飾,實現線程可見性。

remove() 刪除的流程(Java 8):

  1. 計算待新增數據 key 的 hash 值;
  2. 判斷 Node[] 數組是否爲空或者數據長度爲 0 的情況,如果是則返回 null;如果不是則根據 hashCode 通過位運算定得到 Node 數組的下標,即得到頭結點;
  3. 判斷頭結點的 hashCode 是否等於 MOVED(即 -1),檢查是否正在擴容,如果是則幫助擴容;
  4. 如果都不滿足 2 和 3 條件,則加鎖進行刪除操作;
  5. 首先判斷頭結點有無發生變化(步驟 3 點轉移操作會改變頭結點),如果有改變則返回 null;
  6. 如果頭結點的 hashCode 大於 0 說明是鏈表,則按照鏈表方式遍歷刪值;
  7. 如果頭結點是 TreeBin 類型,說明是紅黑樹,則按照樹的方式刪值。

remove() 注意事項:

remove 函數底層是調用 replaceNode 函數實現結點的刪除。

**使用場景:**併發、線程不安全場景

ConcurrentHashMap 在 Java 7 和 Java 8 中的區別:

  1. Java 7 使用 Segment 分段加鎖機制,Segment 繼承 ReentrantLock 實現鎖操作,而 Java 8 使用 CAS + Synchronized 實現鎖操作;
  2. Java 7 查詢時遍歷鏈表效率低,而 Java 8 採用紅黑樹提高查詢效率;
  3. Java 7 使用 HashEntry 存放數據,而 Java 8 改名爲 Node,作用都是一樣,存放的都是 hashcode、key、value、next 等數據。
  4. Java 7 是把數據插入到鏈表的表頭(頭插法),而 Java 8 是將數據插入到鏈表的最後面(尾插法);
  5. Java 7 先擴容,再插值,而 Java 8 先插值,再擴容;
  6. Java 7 的最大併發個數是 Segment 的個數默認值是 16,鎖住整個段,不影響其他段;而 Java 8 去掉了分段鎖,更細粒度,只鎖住一個 Node 節點,不影響其他 Node 節點;
  7. Java 7 在擴容時鎖住一個段,當前段可讀不能寫,其他段可讀寫,只開啓一個線程執行擴容操作;而 Java 8 鎖住一個 Node 結點,當前結點可讀不能寫,其他結點可讀寫,1 個線程執行擴容 + 可能多個 put()/remove 線程幫助擴容。

HashMap、Hashtable、ConccurentHashMap 三者的區別(Java 8):

  1. HashMap 線程不安全,沒有鎖機制,數組 + 鏈表 + 紅黑樹
  2. Hashtable 線程安全,鎖住整個對象,數組 + 鏈表
  3. ConccurentHashMap 線程安全,CAS+Synchronized,數組 + 鏈表 + 紅黑樹
  4. 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

特徵:

  1. key 不允許爲 Null,但允許多個 value 爲 Null
  2. 線程不安全

put() 存儲的流程:

主要分爲兩個步驟,構建排序二叉樹構建平衡二叉樹

構建排序二叉樹,過程如下:

  1. 從根節點 root 開始查找;
  2. 如果 root 節點比待插入節點值小,則在 root 節點左子樹查找,如果大於,則在右子樹查找;
  3. 遞歸循環步驟 2,找到合適的節點爲止;
  4. 把待插入節點與步驟 3 中找到的節點進行比較,如果待插入節點小於找到節點,則把待插入節點作爲左子節點;否則作爲右子節點。

舉個栗子:put(9),步驟如下:

**

** * 從 root 節點 8 開始檢索;

  • 如果 8 小於 9,則從 8 的右子樹繼續找,10 大於 9,10 沒有左子樹;
  • 循環遞歸步驟 2,找到 10 這個合適節點;
    **1. 10box-sizing:border-box;outline:0px;">構建平衡二叉樹,經過左旋、右旋、着色這些步驟後,得到一棵平衡二叉樹。

remove() 的流程:比 put() 複雜,也是分兩個步驟,刪除節點着色旋轉

刪除節點,刪除時出現以下 3 種情況:

  1. 待刪除節點,如果沒有左和右子節點時,則直接刪除;
  2. 待刪除節點,如果有一個子節點時,則把它的子節點指向它的上級節點(即父節點);
  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 用於維護整個雙向鏈表。

特徵:

  1. 只允許一個 key 爲 Null(多個則覆蓋),允許多個 value 爲 Null;
  2. 線程不安全。

原理:

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 的區別

  1. HashMap 只允許一個 key 爲 Null(多個則覆蓋),允許多個 value 爲 Null,HashTable 中的 key 和 value 都不允許爲 null;
  2. HashMap 線程不安全,效率高,HashTable 線程安全,效率低;
  3. HashMap 和 Hashtable 二者都實現了 Map 接口,HashMap 是繼承於 AbstractMap,HashTable 繼承於 Dictionary 類;
  4. 定位存儲位置邏輯不一樣,HashMap 是在 key.hashcode() 後使用位運算,HashTable 是在 key.hashcode() 後使用取模;
  5. 判斷擴容順序不一樣,HashMap 是 put() 之後擴容,HashTabel 是 put() 之前擴容;
  6. 初始容量不一樣,HashMap 默認 16,HashTabel 默認 11。

3.8 HashMap 與 TreeMap 的區別

  1. 數據結構不一樣,HashMap 基於“數組 + 單鏈表”實現(達到一定條件時轉爲紅黑樹),TreeMap 基於紅黑樹實現 ;
  2. HashMap 隨機存儲,TreeMap 默認按 key 的字典升序排序;
  3. HashMap 只允許一個 key 爲 Null(多個則覆蓋),允許多個 value 爲 Null,TreeMap 的 key 不允許爲 Null,但允許多個 value 爲 Null;
  4. HashMap 效率高,TreeMap 效率低。

四、Set

4.1 HashSet

HashSet 是用來存儲沒有重複元素的集合類並且是無序的。實現了 Set 接口。底層使用 HashMap 機制實現,所以也是線程不安全

主要屬性:

//定義了一個HashMap類型的成員變量,即擁有HashMap的所有屬性
private transient HashMap<E,Object> map;

特徵:

  1. 不可重複
  2. 無序
  3. 線程不安全
  4. 集合元素可以是 null,但只能放入一個 null

**使用場景:**去重、不要求順序

**原理:**底層使用 HashMap 的 key 不能重複機制來實現沒有重複的 HashSet。

4.2 TreeSet

TreeSet 實現了 SortedSet 接口,意味着可以排序,它是一個有序並且沒有重複的集合類,底層是通過 TreeMap 實現。TreeSet 並不是根據插入的順序來排序,而是字典自然排序。線程不安全

TreeSet 支持兩種排序方式:自然升序排序自定義排序

特徵:

  1. 不可重複
  2. 有序,默認自然升序排序
  3. 線程不安全
  4. 集合元素不可以爲 null

原理:

TreeSet 底層是基於 treeMap(紅黑樹結構)實現的,可以自定義比較器對元素進行排序,或是使用元素的自然順序。

**使用場景:**去重、要求排序

4.3 LinkedHashSet

LinkedHashSet 是使用 HashSet 機制實現,它是一個可以保證插入順序或是訪問順序,並且沒有重複的集合類。線程不安全

**數據結構:**數組 + 雙向鏈表,Entry 結構:before|hash|key|value|next|after,before 和 after 用於維護整個雙向鏈表。

特徵:

  1. 集合元素不可以爲 null;
  2. 線程不安全。

原理:

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” ,將會有大禮相送!!! 祝各位面試成功!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章