1. 程式人生 > 其它 >Java集合(HashMap核心問題)

Java集合(HashMap核心問題)

集合

1. Java集合類基本概念

在程式設計中,常常需要集中存放多個數據。從傳統意義上講,陣列是我們的一個很好的選擇,前提是我們事先已經明確知道我們將要儲存的物件的數量。一旦在陣列初始化時指定了這個陣列長度,這個陣列長度就是不可變的,如果我們需要儲存一個可以動態增長的資料(在編譯時無法確定具體的數量),java的集合類就是一個很好的設計方案了。

集合類主要負責儲存、盛裝其他資料,因此集合類也被稱為容器類。所有的集合類都位於java.util包下,後來為了處理多執行緒環境下的併發安全問題,java5還在java.util.concurrent包下提供了一些多執行緒支援的集合類。

在學習Java中的集合類的API、程式設計原理的時候,我們一定要明白,"集合"是一個很古老的數學概念,它遠遠早於Java的出現。從數學概念的角度來理解集合能幫助我們更好的理解程式設計中什麼時候該使用什麼型別的集合類。

Java容器類類庫的用途是"儲存物件",並將其劃分為兩個不同的概念:

  • Collection :一組"對立"的元素,通常這些元素都服從某種規則

    • List必須保持元素特定的順序
    • Set不能有重複元素
    • Queue保持一個佇列(先進先出)的順序
  • Map :一組成對的"鍵值對"物件

Collection和Map的區別在於容器中每個位置儲存的元素個數:

  • Collection 每個位置只能儲存一個元素(物件)

  • Map儲存的是"鍵值對",就像一個小型資料庫。我們可以通過"鍵"找到該鍵對應的"值"

2. Java集合類架構層次關係

1. Interface Iterable

迭代器介面,這是Collection類的父介面。實現這個Iterable介面的物件允許使用foreach進行遍歷,也就是說,所有的Collection集合物件都具有"foreach可遍歷性"。這個Iterable介面只有一個方法: iterator()。它返回一個代表當前集合物件的泛型迭代器,用於之後的遍歷操作

1.1 Collection(單列集合)

Collection是最基本的集合介面,一個Collection代表一組Object的集合,這些Object被稱作Collection的元素。Collection是一個介面,用以提供規範定義,不能被例項化使用

1) Set

1.概述:Set介面  繼承自 Collection介面
      Set介面中的方法,並沒有對Collection介面進行擴充
      底層都是依靠Map實現

Set集合類似於一個罐子,"丟進"Set集合裡的多個物件之間沒有明顯的順序。Set繼承自Collection介面,不能包含有重複元素(記住,這是整個Set類層次的共有屬性)。

Set判斷兩個物件相同不是使用"=="運算子,而是根據equals方法。也就是說,我們在加入一個新元素的時候,如果這個新元素物件和Set中已有物件進行注意equals比較都返回false,  則Set就會接受這個新元素物件,否則拒絕。

因為Set的這個制約,在使用Set集合的時候,應該注意兩點:

  • 為Set集合裡的元素的實現類實現一個有效的equals(Object)方法、
  • 對Set的建構函式,傳入的Collection引數不能包含重複的元素

1.1) HashSet

1.概述:HashSet 實現  Set介面
2.特點:
  元素無序
  元素唯一(如果元素一樣,後面的會把前面的覆蓋掉)
  沒有索引(迭代器遍歷,增強for遍歷)
3.資料結構:雜湊表
  jdk8之前:雜湊表 = 陣列+連結串列
  jdk8之後:雜湊表 = 陣列+連結串列+紅黑樹
  加入紅黑樹的目的:查詢快,提高效率
4.方法:
  和Collection一樣

雜湊值

1.概述:計算機計算出來的十進位制數,可以理解為物件的地址值
2.獲取雜湊值:
  呼叫Object類中的hashCode()方法
3.結論:
  a.如果想要獲取物件內容的雜湊值,重寫hashCode方法
  b.內容一樣,算出來的雜湊值一定一樣
  c.內容不一樣,算出來的雜湊值也有可能一樣(雜湊碰撞,雜湊衝突)

hashSet去重過程

1.先計算元素的雜湊值,然後比較雜湊值
2.如果雜湊值不一樣,直接儲存
3.如果雜湊值一樣,再比較元素內容
4.如果雜湊值一樣,內容不一樣,直接存
5.如果雜湊值一樣,內容也一樣,直接去重複,後面的會把前面的覆蓋掉

HashSet是Set介面的典型實現,HashSet使用HASH演算法來儲存集合中的元素,因此具有良好的存取和查詢效能。當向HashSet集合中存入一個元素時,HashSet會呼叫該物件的hashCode()方法來得到該物件的hashCode值,然後根據該HashCode值決定該物件在HashSet中的儲存位置。

值得主要的是,HashSet集合判斷兩個元素相等的標準是兩個物件通過equals()方法比較相等,並且兩個物件的hashCode()方法的返回值相等

1.1.1) LinkedHashSet

1.概述:LinkedHashSet extends HashSet
2.特點:
  元素有序
  元素唯一(如果元素一樣,後面的會把前面的覆蓋掉)
  沒有索引(迭代器遍歷,增強for遍歷)
3.資料結構:雜湊表+連結串列
4.方法:
  和HashSet一樣

LinkedHashSet集合也是根據元素的hashCode值來決定元素的儲存位置,但和HashSet不同的是,它同時使用連結串列維護元素的次序,這樣使得元素看起來是以插入的順序儲存的。

當遍歷LinkedHashSet集合裡的元素時,LinkedHashSet將會按元素的新增順序來訪問集合裡的元素。

LinkedHashSet需要維護元素的插入順序,因此效能略低於HashSet的效能,但在迭代訪問Set裡的全部元素時(遍歷)將有很好的效能(連結串列很適合進行遍歷)

1.2) SortedSet

此介面主要用於排序操作,即實現此介面的子類都屬於排序的子類

1.2.1) TreeSet

TreeSet是SortedSet介面的實現類,TreeSet可以確保集合元素處於排序狀態

1.3) EnumSet

EnumSet是一個專門為列舉類設計的集合類,EnumSet中所有元素都必須是指定列舉型別的列舉值,該列舉型別在建立EnumSet時顯式、或隱式地指定。EnumSet的集合元素也是有序的,

它們以列舉值在Enum類內的定義順序來決定集合元素的順序

2) List

1.概述:List介面 extends Collection介面
2.特點:
  a.有序
  b.元素可重複
  c.有索引

List集合代表一個元素有序、可重複的集合,集合中每個元素都有其對應的順序索引。List集合允許加入重複元素,因為它可以通過索引來訪問指定位置的集合元素。List集合預設按元素的新增順序設定元素的索引

2.1) ArrayList

1.概述:是List介面的實現類
2.特點:
  a.有序
  b.元素可重複
  c.有索引
3.資料結構:陣列
4.使用:
  ArrayList<泛型> 集合名 = new ArrayList<>()
5.常用方法:
  boolean add(E e)  -> 將元素新增到集合中->尾部(add方法一定能新增成功的,所以我們不用boolean接收返回值)
  void add(int index, E element) ->在指定索引位置上新增元素
  boolean remove(Object o) ->刪除指定的元素,刪除成功為true,失敗為false
  E remove(int index) -> 刪除指定索引位置上的元素,返回的是被刪除的那個元素
  E set(int index, E element) -> 將指定索引位置上的元素,修改成後面的element元素
  E get(int index) -> 根據索引獲取元素
  int size()  -> 獲取集合元素個數
  a.有序
  b.有索引
  c.元素可重複
  d.資料結構:陣列
      
2.構造方法:
  ArrayList()構造一個初始容量為 10 的空列表
             並不是一new,長度為10的空列表就創建出來了,而是第一次add的時候才會將ArrayList的列表長度變成10
  ArrayList(int initialCapacity) 構造一個具有指定初始容量的空列表
      
3.問題:
  a.ArrayList底層資料結構為陣列,陣列是定長的,而集合是長度可變的,ArrayList底層是怎麼讓陣列可變的?
    陣列擴容->elementData = Arrays.copyOf(elementData, newCapacity);
      
  b.超出了預設的容量,會自動擴容,擴容多少倍呢?
    1.5倍

ArrayList是基於陣列實現的List類,它封裝了一個動態的增長的、允許再分配的Object[]陣列。

2.2) Vector

Vector和ArrayList在用法上幾乎完全相同,但由於Vector是一個古老的集合,所以Vector提供了一些方法名很長的方法,但隨著JDK1.2以後,java提供了系統的集合框架,就將Vector改為實現List介面,統一歸入集合框架體系中

2.2.1) Stack

Stack是Vector提供的一個子類,用於模擬"棧"這種資料結構(LIFO後進先出)

2.3) LinkedList

1.概述:List介面的實現類
2.特點:
  a.有序
  b.元素可重複
  c.有索引
3.底層資料結構:
  連結串列->雙向連結串列
4.使用:
  a.建立物件:LinkedList<泛型> 集合名 = new LinkedList<>()
  b.方法:和ArrayList一樣
  c.特有方法:
    - public void addFirst(E e):將指定元素插入此列表的開頭。
    - public void addLast(E e):將指定元素新增到此列表的結尾。
    - public E getFirst():返回此列表的第一個元素。
    - public E getLast():返回此列表的最後一個元素。
    - public E removeFirst():移除並返回此列表的第一個元素。
    - public E removeLast():移除並返回此列表的最後一個元素。
    - public E pop():從此列表所表示的堆疊處彈出一個元素。
    - public void push(E e):將元素推入此列表所表示的堆疊。
    - public boolean isEmpty():如果列表不包含元素,則返回true。
1.概述:LinkedList是List的實現類
2.特點:
  元素有序
  元素可重複
  有索引
3.資料結構:連結串列(雙向連結串列)
4.特有方法:
- public void addFirst(E e):將指定元素插入此列表的開頭。
- public void addLast(E e):將指定元素新增到此列表的結尾。
- public E getFirst():返回此列表的第一個元素。
- public E getLast():返回此列表的最後一個元素。
- public E removeFirst():移除並返回此列表的第一個元素。
- public E removeLast():移除並返回此列表的最後一個元素。
- public E pop():從此列表所表示的堆疊處彈出一個元素。
- public void push(E e):將元素推入此列表所表示的堆疊。
- public boolean isEmpty():如果列表不包含元素,則返回true。

implements List, Deque。實現List介面,能對它進行佇列操作,即可以根據索引來隨機訪問集合中的元素。同時它還實現Deque介面,即能將LinkedList當作雙端佇列使用。自然也可以被當作"棧來使用"

3) Queue

Queue用於模擬"佇列"這種資料結構(先進先出 FIFO)。佇列的頭部儲存著佇列中存放時間最長的元素,佇列的尾部儲存著佇列中存放時間最短的元素。新元素插入(offer)到佇列的尾部,訪問元素(poll)操作會返回佇列頭部的元素,佇列不允許隨機訪問佇列中的元素。結合生活中常見的排隊就會很好理解這個概念

3.1) PriorityQueue

PriorityQueue並不是一個比較標準的佇列實現,PriorityQueue儲存佇列元素的順序並不是按照加入佇列的順序,而是按照佇列元素的大小進行重新排序,這點從它的類名也可以看出來

3.2) Deque

Deque介面代表一個"雙端佇列",雙端佇列可以同時從兩端來新增、刪除元素,因此Deque的實現類既可以當成佇列使用、也可以當成棧使用

3.2.1) ArrayDeque

是一個基於陣列的雙端佇列,和ArrayList類似,它們的底層都採用一個動態的、可重分配的Object[]陣列來儲存集合元素,當集合元素超出該陣列的容量時,系統會在底層重新分配一個Object[]陣列來儲存集合元素

3.2.2) LinkedList

1.概述:LinkedList是List的實現類
2.特點:
  元素有序
  元素可重複
  有索引
3.資料結構:連結串列(雙向連結串列)
4.特有方法:
- public void addFirst(E e):將指定元素插入此列表的開頭。
- public void addLast(E e):將指定元素新增到此列表的結尾。
- public E getFirst():返回此列表的第一個元素。
- public E getLast():返回此列表的最後一個元素。
- public E removeFirst():移除並返回此列表的第一個元素。
- public E removeLast():移除並返回此列表的最後一個元素。
- public E pop():從此列表所表示的堆疊處彈出一個元素。
- public void push(E e):將元素推入此列表所表示的堆疊。
- public boolean isEmpty():如果列表不包含元素,則返回true。

1.2 Map(雙列集合)

Map用於儲存具有"對映關係"的資料,因此Map集合裡儲存著兩組值,一組值用於儲存Map裡的key,另外一組值用於儲存Map裡的value。key和value都可以是任何引用型別的資料。Map的key不允許重複,即同一個Map物件的任何兩個key通過equals方法比較結果總是返回false。

關於Map,我們要從程式碼複用的角度去理解,java是先實現了Map,然後通過包裝了一個所有value都為null的Map就實現了Set集合

Map的這些實現類和子介面中key集的儲存形式和Set集合完全相同(即key不能重複)

Map的這些實現類和子介面中value集的儲存形式和List非常類似(即value可以重複、根據索引來查詢)

1.概述:雙列集合的頂級介面
2.特點:
  a.元素都是key和value的形式
  b.key唯一,但是value可以重複
  c.無索引
  d.無序(LinkedHashMap是有序的)

1) HashMap

1.概述:HashMap 是 Map的實現類
2.特點:
  a.元素都是key和value的形式
  b.key唯一,但是value可以重複-> key重寫hashCode和equals方法,去重方式和set一樣
  c.無索引
  d.無序
3.資料結構:
  雜湊表
4.方法:
  V put(K key, V value)  -> 儲存元素
  V remove(Object key)  ->根據key刪除對應的鍵值對
  V get(Object key)  -> 根據key獲取對應的value
  boolean containsKey(Object key) ->判斷Map中是否包含指定的key
  Collection<V> values()  -> 將Map中所有的value儲存到Collection集合中
  
  Set<K> keySet() -> 將Map中所有的key獲取出來存到Set集合中 
  Set<Map.Entry<K,V>> entrySet() -> 獲取Map中所有的鍵值對物件,放到set集合中

和HashSet集合不能保證元素的順序一樣,HashMap也不能保證key-value對的順序。並且類似於HashSet判斷兩個key是否相等的標準也是: 兩個key通過equals()方法比較返回true、

同時兩個key的hashCode值也必須相等

HashMap的兩種遍歷方式

方式1:獲取key,然後根據key獲取value

Set<K> keySet() -> 將Map中所有的key獲取出來存到Set集合中 

方式2:同時獲取key和value

 Set<Map.Entry<K,V>> entrySet() -> 獲取Map中所有的鍵值對物件,放到set集合中

1.獲取Map的內部介面:Map.Entry
2.將Map.Entry放到Set集合中
3.遍歷Set集合,將每一個Map.Entry獲取出來
4.在呼叫Map.Entry中的getkey()  和  getValue()方法獲取鍵值對

* 能說下 HashMap 的實現原理嗎

其實就是有個 Entry 陣列,Entry 儲存了 key 和 value。當你要塞入一個鍵值對的時候,會根據一個 hash 演算法計算 key 的 hash 值,然後然後通過陣列大小 n-1 & hash 值之後,得到一個數組的下標,然後往那個位置塞入這個 Entry。

  hash演算法
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }


hashMap 原始碼
    if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

然後我們知道,hash 演算法是可能產生衝突的,且陣列的大小是有限的,所以很可能通過不同的 key 計算得到一樣的下標,因此為了解決 Entry 衝突的問題,採用了連結串列法,如下圖所示:

在 JDK1.7 及之前連結串列的插入採用的是頭插法,即在連結串列的頭部插入新的 Entry。

在 JDK1.8 的時候,改成了尾插法,並且引入了紅黑樹

連結串列的長度大於 8 且陣列大小大於等於 64 的時候,就把連結串列轉化成紅黑樹,當紅黑樹節點小於 6 的時候,又會退化成連結串列

* 為什麼 JDK 1.8 要對 HashMap 做紅黑樹這個改動?

主要是避免 hash 衝突導致連結串列的長度過長,這樣 get 的時候時間複雜度嚴格來說就不是 O(1) 了,因為可能需要遍歷連結串列來查詢命中的 Entry。

*** 為什麼定義連結串列長度為 8 且陣列大小大於等於 64 才轉紅黑樹?不要連結串列直接用紅黑樹不就得了嗎?**

因為[紅黑樹節點的大小是普通節點大小的兩倍,所以為了節省記憶體空間不會直接只用紅黑樹只有當節點到達一定數量才會轉成紅黑樹這裡定義的是 8

為什麼是 8 呢?這個其實 HashMap 註釋上也有說的,和泊松分佈有關係,這個大學應該都學過。

簡單翻譯下就是在預設閾值是 0.75 的情況下,衝突節點長度為 8 的概率為 0.00000006,也就概率比較小(畢竟紅黑樹耗記憶體,且連結串列長度短點時遍歷的還是很快的)。

這就是基於時間和空間的平衡了,紅黑樹佔用記憶體大,所以節點少就不用紅黑樹,如果萬一真的衝突很多,就用紅黑樹],選個引數為 8 的大小,就是為了平衡時間和空間的問題。

* 為什麼節點少於 6 要從紅黑樹轉成連結串列?

也是為了平衡時間和空間,節點太少連結串列遍歷也很快,沒必要成紅黑樹,變成連結串列節約記憶體。

為什麼定了 6 而不是小於等於 8 就變?

是因為要留個緩衝餘地,避免反覆橫跳。舉個例子,一個節點反覆新增,從 8 變成 9 ,連結串列變紅黑樹,又刪了,從 9 變成 8,又從紅黑樹變連結串列,再新增,又從連結串列變紅黑樹?

所以一點 ,畢竟樹化和反樹化都是有開銷的。

* 那 JDK 1.8 對 HashMap 除了紅黑樹這個改動,還有哪些改動?

  1. hash 函式的優化
  2. 擴容 rehash 的優化
  3. 頭插法和尾插法
  4. 插入與擴容時機的變更
  • hash 函式的優化

    1.7是這樣實現的:

    static int hash(int h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
    

    1.8

        static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
    

    具體而言就是 1.7 的操作太多了,經歷了四次異或,所以 1.8 優化了下,它將 key 的雜湊碼的高16位和低16位進行了異或,得到的 hash 值同時擁有了高位和低位的特性,這樣做得出的碼比較均勻,不容易衝突。

    這也是 JDK 開發者根據速度、實用性、雜湊質量所做的權衡來做的實現:

  • 擴容 rehash 的優化

    按照我們的思維,正常擴容肯定是先申請一個更大的陣列,然後將原數組裡面的每一個元素重新 hash 判斷在新陣列的位置,然後一個一個搬遷過去。

    在 1.7 的時候就是這樣實現的,然而 1.8 在這裡做了優化,關鍵點就在於陣列的長度是 2 的次方,且擴容為 2 倍

    因為陣列的長度是 2 的 n 次方,所以假設以前的陣列長度(16)二進位制表示是 010000,那麼新陣列的長度(32)二進位制表示是 100000,這個應該很好理解吧?

    它們之間的差別就在於高位多了一個 1,而我們通過 key 的 hash 值定位其在陣列位置所採用的方法是 (陣列長度-1) & hash。我們還是拿 16 和 32 長度來舉例:

    16-1=15,二進位制為 001111

    32-1=31,二進位制為 011111

    所以重點就在 key 的 hash 值的從右往左數第五位是否是 1,如果是 1 說明需要搬遷到新位置,且新位置的下標就是原下標+16(原陣列大小),如果是 0 說明吃不到新陣列長度的高位,那就還是在原位置,不需要遷移。

    所以,我們剛好拿老陣列的長度(010000)來判斷高位是否是 1,這裡只有兩種情況,要麼是 1 要麼是 0 。

    從上面的原始碼可以看到,連結串列的資料是一次性計算完,然後一堆搬運的,因為擴容時候,節點的下標變化只會是原位置,或者原位置+老陣列長度,不會有第三種選擇。

    上面的位操作,包括為什麼是原下標+老陣列長度等,如果你不理解的話,可以舉幾個數帶進去算一算,就能理解了。

    總結一下,1.8 的擴容不需要每個節點重寫 hash 算下標,而是通過和老陣列長度的&計算是否為 0 ,來判斷新下標的位置。

    額外再補充一個問題:為什麼 HashMap 的長度一定要是 2 的 n 次冪

    原因就在於陣列下標的計算,由於下標的計算公式用的是 i = (n - 1) & hash,即位運算,一般我們能想到的是 %(取餘)計算,但相比於位運算而言,效率比較低,所以推薦用位運算,而要滿足上面這個公式,n 的大小就必須是 2 的 n 次冪。

    即:當 b 等於 2 的 n 次冪時,a % b 操作等於 a & ( b - 1 )

  • 頭插法和尾插法

    1.7是頭插法,

    頭插法的好處就是插入的時候不需要遍歷連結串列,直接替換成頭結點,但是缺點是擴容的時候會逆序,而逆序在多執行緒操作下可能會出現環,然後就死迴圈了。

    然後 1.8 是尾插法,每次都從尾部插入的話,擴容後連結串列的順序還是和之前一致,所以不可能出現多執行緒擴容成環的情況。

    其實我在網上找了找,很多文章說尾插法的優化就是避免多執行緒操作成環的問題,我表示懷疑。因為 HashMap 本就不是執行緒安全的,我要還優化你多執行緒的情況?我覺得開發者應該不會做這樣的優化。

    那為什麼要變成尾插法呢?

    那再延伸一下,改成尾插法之後 HashMap 就不會死迴圈了嗎

    好像還是會,這次是紅黑樹的問題 ,

  • 插入與擴容時機的變更

    1.7 是先判斷 put 的鍵值對是新增還是替換,如果是替換則直接替換,如果是新增會判斷當前元素數量是否大於等於閾值,如果超過閾值且命中陣列索引的位置已經有元素了,那麼就進行擴容。

        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        createEntry(...)
    

    而 1.8 則是先插入,然後再判斷 size 是否大於閾值,若大於則擴容。

    就這麼個差別,至於為什麼,好吧,我查了下沒查出來,我自己也不知道,我個人覺得兩者沒差。。可能是重構(引入紅黑樹)的時候改了下順序而已...其實沒什麼影響,

1.1) LinkedHashMap

LinkedHashMap 的父類是 HashMap,所以 HashMap 有的它都有,然後基於 HashMap 做了一些擴充套件。

首先它把 HashMap 的 Entry 加了兩個指標:before 和 after。

LinkedHashMap也使用雙向連結串列來維護key-value對的次序,該連結串列負責維護Map的迭代順序,與key-value對的插入順序一致(注意和TreeMap對所有的key-value進行排序進行區分)

2) Hashtable

是一個古老的Map實現類

2.1) Properties

Properties物件在處理屬性檔案時特別方便(windows平臺上的.ini檔案),Properties類可以把Map物件和屬性檔案關聯起來,從而可以把Map物件中的key-value對寫入到屬性檔案中,也可以把屬性檔案中的"屬性名-屬性值"載入到Map物件中

3) SortedMap

正如Set介面派生出SortedSet子介面,SortedSet介面有一個TreeSet實現類一樣,Map介面也派生出一個SortedMap子介面,SortedMap介面也有一個TreeMap實現類

3.1) TreeMap

TreeMap就是一個紅黑樹資料結構,每個key-value對即作為紅黑樹的一個節點。TreeMap儲存key-value對(節點)時,需要根據key對節點進行排序。TreeMap可以保證所有的

key-value對處於有序狀態。同樣,TreeMap也有兩種排序方式: 自然排序、定製排序

4) WeakHashMap

WeakHashMap與HashMap的用法基本相似。區別在於,HashMap的key保留了對實際物件的"強引用",這意味著只要該HashMap物件不被銷燬,該HashMap所引用的物件就不會被垃圾回收。

但WeakHashMap的key只保留了對實際物件的弱引用,這意味著如果WeakHashMap物件的key所引用的物件沒有被其他強引用變數所引用,則這些key所引用的物件可能被垃圾回收,當垃

圾回收了該key所對應的實際物件之後,WeakHashMap也可能自動刪除這些key所對應的key-value對

5) IdentityHashMap

IdentityHashMap的實現機制與HashMap基本相似,在IdentityHashMap中,當且僅當兩個key嚴格相等(key1 == key2)時,IdentityHashMap才認為兩個key相等

理解這個 map 的關鍵就在於它的名字 Identity,也就是它判斷是否相等的依據不是靠 equals ,而是物件本身是否是它自己。

什麼意思呢?

首先看它覆蓋的 hash 方法:

可以看到,它用了個 System.identityHashCode(x),而不是x.hashCode()。

而這個方***返回原來預設的 hashCode 實現,不管物件是否重寫了 hashCode 方法

預設的實現返回的值是:物件的記憶體地址轉化成整數,是不是有點感覺了?

然後我們再看下它的 get 方法:

可以看到,它判斷 key 是否相等並不靠 hash 值和 equals,而是直接用了 ==

而 == 其實就是地址判斷!

只有相同的物件進行 == 才會返回 true。

因此我們得知,IdentityHashMap 的中的 key 只認它自己(物件本身)

即便你偽造個物件,就算值都相等也沒用,put 進去 IdentityHashMap 只會多一個鍵值對,而不是替換,這就是 Identity 的含義。

比如以下程式碼,identityHashMap 會存在兩個 Yes:

Map<String, String> identityHashMap = new IdentityHashMap<>();
identityHashMap.put(new Yes("1"), "1");
identityHashMap.put(new Yes("1"), "2");

為什麼返回值是 tab[i+1]?

這是因為 IdentityHashMap 的儲存方式有點不一樣,它是將 value 存在 key 的後面。

6) EnumMap

EnumMap是一個與列舉類一起使用的Map實現,EnumMap中的所有key都必須是單個列舉類的列舉值。建立EnumMap時必須顯式或隱式指定它對應的列舉類。EnumMap根據key的自然順序

(即列舉值在列舉類中的定義順序)

7)TreeMap

TreeMap 內部是通過紅黑樹]實現的,可以讓 key 的實現 Comparable 介面或者自定義實現一個 comparator 傳入建構函式,這樣塞入的節點就會根據你定義的規則進行排序。

這個用的比較少,我常用在跟加密有關的時候,有些加密需要根據字母序排,然後再拼接成字串排序,在這個時候就可以把業務上的值統一都塞到 TreeMap 裡維護,取出來就是有序的。