HashMap,難的不在Map,而在Hash

在日常的開發當中,HashMap是 最經常使用的Map類(沒有之一),它支持null鍵和null值,是絕大部分利用鍵值對存取場景的首選。須要切記的一點是——HashMap不是線程安全的數據結構,因此不要在多線程場景中應用它。java

一般狀況下,咱們使用Map的主要目的是用來放入(put)、訪問(get)或者刪除(remove),而對順序沒有特別的要求——HashMap在這種狀況下就是最好的選擇。程序員

0一、Hash

對於HashMap來講,難理解的不在於Map,而在於Hash。算法

Hash,通常譯做「散列」,也有直接音譯爲「哈希」的,這玩意什麼意思呢?就是把任意長度的數據經過一種算法映射到固定長度的域上(散列值)。安全

再直觀一點,就是對一串數據wang進行雜糅,輸出另一段固定長度的數據er——做爲數據wang的特徵。咱們一般用一串指紋來映射某一我的,別小瞧手指頭那麼大點的指紋,在你所處的範圍內很難找出第二個和你相同的(人的散列算法也好厲害,有沒有?)。微信

對於任意兩個不一樣的數據塊,其散列值相同的可能性極小,也就是說,對於一個給定的數據塊,找到和它散列值相同的數據塊極爲困難。再者,對於一個數據塊,哪怕只改動它的一個比特位,其散列值的改動也會很是的大——這正是Hash存在的價值!數據結構

在Java中,String字符串的散列值計算方法以下:多線程

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}
複製代碼

看得懂看不懂都不要緊,咱們就當是一個「乘加迭代運算」的算法。藉此機會,咱們來看一下「沉」、「默」、「王」、「二」四個字符串的散列值是多少。this

String[] cmower = { "沉", "默", "王", "二" };
for (String s : cmower) {
	System.out.println(s.hashCode());
}
複製代碼

輸出的結果以下(5位數字):spa

沉的散列值:27785 默的散列值:40664 王的散列值:29579 二的散列值:20108線程

對於HashMap來講,Hash(key,鍵位)存在的目的是爲了加速鍵值對的查找(你想,若是電話薄不是按照人名的首字母排列的話,找一我的該多困難「個人微信好友有很多在暱稱前加了A,好狠」)。一般狀況下,咱們習慣使用String字符串來做爲Map的鍵,請看如下代碼:

Map<String, String> map = new HashMap<>();
String[] cmower = { "沉", "默", "王", "二" };
for (String s : cmower) {
	map.put(s, s + "月入25萬");
}
複製代碼

那HashMap會真的會將String字符串做爲實際的鍵嗎?咱們來看HashMap的put方法源碼:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
複製代碼

雖然只有一個putVal()方法的調用,可是你應該已經發現,HashMap內部會把key進行一個hash運算,具體代碼以下:

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

假如key是String字符串的話,hash()會先獲取字符串的hashCode(散列值),再對散列值進行位於運算,最終的值爲HashMap實際的鍵(int值)。

既然HashMap在put的時候使用鍵的散列值做爲實際的鍵,那麼在根據鍵獲取值的時候,天然也要先對get(key)方法的key進行hash運算,請看如下代碼:

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
複製代碼

0二、散列值衝突怎麼解決

儘管散列值很難重複,咱們仍是要明白,這種轉換是一種壓縮映射,也就是,散列值的空間一般遠小於輸入的空間,不一樣的輸入可能會散列成相同的輸出。

也就是說,key1 ≠ key2,但function(key1)有可能等於function(key2)——散列值衝突了。怎麼辦?

最容易想到的解決辦法就是:當關鍵字key2的散列值value與key1的散列值value出現衝突時,以value爲基礎,產生另外一個散列值value1,若是value1與value再也不衝突,則將value1做爲key2的散列值。

依照這個辦法,總會找到不衝突的那個。

0三、初始容量和負載因子

HashMap的構造方法主要有三種:

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}


public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}


public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
}
複製代碼

其中initialCapacity爲初始容量(默認爲1 << 4 = 16),loadFactor爲負載因子(默認爲0.75)。初始容量是HashMap在建立時的容量(HashMap中桶的數量);負載因子是HashMap在其容量自動增長以前能夠達到多滿的一種尺度。

當HashMap中的條目數超出了負載因子與當前容量的乘積時,則要對HashMap擴容,增長大約兩倍的桶數。

一般,默認的負載因子 (0.75) 是時間和空間成本上的一種折衷。負載因子太高雖然減小了空間開銷,但同時增長了查詢成本。若是負載因子太小,則初始容量要增大,不然會致使頻繁的擴容。

在設置初始容量時應該考慮到映射中所需的條目數及其加載因子,以便最大限度地減小擴容的操做次數。

若是可以提早預知要存取的鍵值對數量的話,能夠考慮設置合適的初始容量(大於「預估元素數量 / 負載因子」,而且是2的冪數)。

0四、小結

在以前很長的一段時間內,我對HashMap的認知僅限於會用它的put(key, value)value = get(key)

但,當我強迫本身每週要輸出一篇Java方面的技術文章後,我對HashMap真的「深刻淺出」了——散列值(哈希值)、散列衝突(哈希衝突)、初始容量和負載因子,居然能站在我面前一直笑——而原先,我見到這些關鍵字就溜之大吉了,我怕見到它們。


PS:歡迎個人公衆號「沉默王二」,一個不止寫代碼的程序員,還寫有趣有益的文字,給不喜歡嚴肅的你。

相關文章
相關標籤/搜索