1. 程式人生 > >HashMap儲存原理以及與hashcode、equals方法的關係

HashMap儲存原理以及與hashcode、equals方法的關係

一、HashMap 儲存/讀取資料原理:
先放原始碼:

public class HashMap<K, V> extends AbstractMap<K, V> implements Cloneable, Serializable {
    private static final int MINIMUM_CAPACITY = 4;
...
    transient HashMapEntry<K, V>[] table;
...
    private static final Entry[] EMPTY_TABLE
            = new
HashMapEntry[MINIMUM_CAPACITY >>> 1]; ... @Override public V put(K key, V value) { if (key == null) { return putValueForNullKey(value); } int hash = Collections.secondaryHash(key); HashMapEntry<K, V>[] tab = table; int index
= hash & (tab.length - 1); for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) { if (e.hash == hash && key.equals(e.key)) { preModify(e); V oldValue = e.value; e.value = value; return oldValue; } } // No entry for (non-null) key is present; create one
modCount++; if (size++ > threshold) { tab = doubleCapacity(); index = hash & (tab.length - 1); } addNewEntry(key, value, hash, index); return null; } ... public V get(Object key) { if (key == null) { HashMapEntry<K, V> e = entryForNullKey; return e == null ? null : e.value; } int hash = Collections.secondaryHash(key); HashMapEntry<K, V>[] tab = table; for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)]; e != null; e = e.next) { K eKey = e.key; if (eKey == key || (e.hash == hash && key.equals(eKey))) { return e.value; } } return null; } ... }
    HashMap中儲存資料是用一個數組來儲存的,也就是上面的table變數,其型別是HashMapEntry的陣列,
    而HashMapEntry則是儲存鍵值對的資料結構,並且有本身型別的next變數,可以構成連結串列。

    HashMap儲存資料時,首先根據key的hashcode值找到應該儲存在table陣列的下標位置,如果該位置之前沒有
    儲存過值,也就是沒有發生碰撞,則儲存這個鍵值對物件到該位置中;如果發生了碰撞,也就是說有兩個物件的key
    的hashcode值相等,那麼則需要通過key的equals方法判斷這兩個物件是否是同一個物件,如果是,那麼原本存
    儲的舊值會被新值所替換;如果不是同一個物件,則把新的鍵值對物件儲存到舊的鍵值對物件next變數中,構成連結串列。


    我們分析下put方法的實現:
    1、if (key == null) {
        return putValueForNullKey(value);
    }
        首先判斷是否為null,如果為null則特殊處理;

    2、int hash = Collections.secondaryHash(key);
    獲取Key的二級hash值,其中Collections.secondaryHash方法的實現就是把Key的hashcode值
    做一定改變;

    3、int index = hash & (tab.length - 1);
    通過剛才計算的hash值來獲取該key應該存放在陣列的下標位置,也就是獲取該資料應該儲存在table數
    組的哪個位置;

    4、for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
        if (e.hash == hash && key.equals(e.key)) {
            preModify(e);
            V oldValue = e.value;
            e.value = value;
            return oldValue;
        }
    }
    如果已經有該key存在了,則覆蓋這個key的值value。
    注意這裡的判斷:因為只有兩個物件的hashcode值相等並且兩個物件用equals判斷返回true時,才
    去覆蓋原有的值;

    5、
    if (size++ > threshold) {
        tab = doubleCapacity();
        index = hash & (tab.length - 1);
    }
    addNewEntry(key, value, hash, index);
        如果該key不存在,或者發生碰撞的物件不是一個物件時,則需要把它儲存下來。首先如果儲存數量已經
        大於陣列大小,則把陣列雙倍擴大。然後再把鍵值對儲存到陣列中。
    注意這裡儲存的時候,如果陣列儲存位置原本就存在鍵值對,那麼則把新的鍵值對物件儲存到舊的鍵值對
    物件next變數中,構成連結串列。

二、HashMap與hashcode、equals方法的關係
它們的關係從上面的原始碼都能略知一二,再說個實際情況。
假設你用自定義型別MyClass作為HashMap的Key,同時為了需求重寫了hashcode、equals方法(這個
很常見),那麼很有可能會影響HashMap的執行效率,例如:

1、重寫hashcode方法後,任何物件返回都是同一個hash值,那麼,每次儲存都會發生碰撞,所有物件都只會儲存
在HaspMap的一格中,HashMap就等於廢了;

2、重寫hashcode方法後,其返回值會隨屬性的變化而變化,這樣的話,因為HashMap是根據Key的hashcode
值儲存讀取的,如果同一個物件每次返回的hashcode都不一樣,則根本無法讀取你上次儲存的位置,也就是
HashMap會失效;

3、重寫equals方法後,只根據物件的某些屬性值相等與否來決定equals方法是否返回true。這樣的話,就有可
能兩個其實不是一個物件的,但是儲存到HashMap時,則被認為是一個物件,導致其值被覆蓋了;

還有很多很多要注意的情況,為了避免這些情況,我們需要注意一些地方:當你使用任何物件作為Key,那麼它必
須遵守了equals()和hashCode()方法的定義規則,並且當物件插入到Map中之後將不會再改變。