Java集合框架————Map集合(1)

Collection集合的特點是每次進行單個對象的保存,如果現在要進行一對對象(偶對象)的保存就只能使用Map集合來完成,即Map集合中會一次性保存兩個對象,且這兩個對象的關係:key=value結構。這種結構最大的特點是可以通過key找到對應的value內容。

1.Map接口簡述

首先來觀察Map接口定義:public interface Map<K,V>

在Map接口中有如下常用方法:
在這裏插入圖片描述

Map本身是一個接口,要使用Map需要通過子類進行對象實例化。Map接口的常用子類有如下四個: HashMap、Hashtable、TreeMap、ConcurrentHashMap。

在這裏插入圖片描述

2.HashMap子類

HashMap是使用Map集合中最爲常用的子類。
範例:Map基本操作

import java.util.HashMap;

public class MapDemo {
    public static void main(String[] args) {
        HashMap<Integer,String> map = new HashMap<>();
        map.put(1,"hello");
        map.put(2,"bad");
        map.put(3,"man");
        map.put(4,"Hello");
        System.out.println(map);
        System.out.println(map.get(2));
        System.out.println(map.get(6));//查不到key 返回null
    }
}

切記key值不能重複
根據key取得value。

範例:取得Map中所有key信息


import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;

public class MapDemo {
    public static void main(String[] args) {
        HashMap<Integer,String> map = new HashMap<>();
        map.put(1,"hello");
        map.put(2,"bad");
        map.put(3,"man");
        map.put(4,"Hello");
//        System.out.println(map);
//        System.out.println(map.get(2));
//        System.out.println(map.get(6));
        Set<Integer> set = map.keySet();
        Iterator<Integer> iterator = set.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}

2.1HashMap內部實現基本點分析

首先,我們來一起看看 HashMap 內部的結構,它可以看作是數組(Node[] table)和鏈表結合組成的複合結構,數組被分爲一個個桶(bucket),通過哈希值決定了鍵值對在這個數組的尋址;哈希值相同的鍵值對,則以鏈表形
式存儲,你可以參考下面的示意圖。這裏需要注意的是,如果鏈表大小超過閾值(TREEIFY_THRESHOLD, 8),圖中的鏈表就會被改造爲樹形結構。

ad

從構造函數的實現來看,這個表格(數組)似乎並沒有在最初就初始化好,僅僅設置了一些初始值而已。

在這裏插入圖片描述

所以,我們深刻懷疑,HashMap 也許是按照 lazy-load 原則,在首次使用時被初始化既然如此,我們去看看 put方法實現,似乎只有一個 putVal 的調用:
在這裏插入圖片描述

看來主要的密碼似乎藏在 putVal 裏面,到底有什麼祕密呢?

在這裏插入圖片描述

從 putVal 方法最初的幾行,我們就可以發現幾個有意思的地方:
~如果表格是 null,resize 方法會負責初始化它,這從 tab = resize() 可以看出。
~resize 方法兼顧兩個職責,創建初始存儲表格,或者在容量不滿足需求的時候,進行擴容(resize)。具體鍵值對在哈希表中的位置(數組 index)取決於下面的位運算:

i = (n-1) & hash

我們會發現,它並不是 key 本身的 hashCode,而是來自於 HashMap內部的另外一個 hash 方法。注意,爲什麼這裏需要將高位數據移位到低位進行異或運算呢?這除是因爲有些數據計算出的哈希值差異主要在高位,而 HashMap 裏的哈希尋址是忽略容量以上的高位的,那麼這種處理就可以有效避免類似情況下的哈希碰撞。

下面進一步分析一下身兼多職的 resize 方法,面試官經常追問它的源碼設計。

在這裏插入圖片描述

依據 resize 源碼,不考慮極端情況(容量理論最大極限MAXIMUM_CAPACITY 指定,數值爲 1<<30,也就是 2的 30 次方),我們可以歸納爲:
門限值等於(負載因子)*(容量),如果構建 HashMap 的時候沒有指定它們,那麼就是依據相應的默認常量值。

門限通常是以倍數進行調整 (newThr = oldThr << 1),我前面提到,根據 putVal 中的邏輯,當元素。

個數超過門限大小時,則調整 Map 大小。

擴容後,需要將老的數組中的元素重新放置到新的數組,這是擴容的一個主要開銷來源。

2.2 容量、負載因子和樹化

前面我們快速梳理了一下 HashMap 從創建到放入鍵值對的相關邏輯,現在思考一下,爲什麼我們需要在乎容量和負載因子呢?

這是因爲容量和負載係數決定了可用的桶的數量,空桶太多會浪費空間,如果使用的太滿則會嚴重影響操作的性能。極端情況下,假設只有一個桶,那麼它就退化成了鏈表,完全不能提供所謂常數時間存的性能。 既然容量和負載因子這麼重要,我們在實踐中應該如何選擇呢?

如果能夠知道 HashMap 要存取的鍵值對數量,可以考慮預先設置合適的容量大小。具體數值我們可以根據擴容發生的條件來做簡單預估,根據前面的代碼分析,我們知道它需要符合計算條件:

負載因子 * 容量 > 元素數量

所以,預先設置的容量需要滿足,大於“預估元素數量 / 負載因子”,同時它是 2 的冪數,結論已經非常清晰了。

而對於負載因子,我建議:

  • 如果沒有特別需求,不要輕易進行更改,因爲 JDK 自身的默認負載因子是非常符合通用場景的需求的。
  • 如果確實需要調整,建議不要設置超過 0.75 的數值,因爲會顯著增加衝突,降低 HashMap的性能。
  • 如果使用太小的負載因子,按照上面的公式,預設容量值也進行調整,否則可能會導致更加頻繁的擴容,增加無謂的開銷,本身訪問性能也會受影

樹化:

樹化改造,對應邏輯主要在 putVal 和 treeifyBin 中。

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        // 樹化改造邏輯
  }

上面是精簡過的 treeifyBin 示意,綜合這兩個方法,樹化改造的邏輯就非常清晰了,可以理解爲,當 bin 的數量大於 TREEIFY_THRESHOLD 時:

  • 如果容量小於 MIN_TREEIFY_CAPACITY,只會進行簡單的擴容。
  • 如果容量大於 MIN_TREEIFY_CAPACITY ,則會進行樹化改造。

那麼,爲什麼 HashMap 要樹化呢?

本質上這是個安全問題。因爲在元素放置過程中,如果一個對象哈希衝突,都被放置到同一個桶裏,則會形成一個鏈表,我們知道鏈表查詢是線性的,會嚴重影響存取的性能。
而在現實世界,構造哈希衝突的數據並不是非常複雜的事情,惡意代碼就可以利用這些數據大量與服務器端交互,導致服務器端 CPU 大量佔用,這就構成了哈希碰撞拒絕服務攻擊,國內一線互聯網公司就發生過類似攻擊事件。

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