jdk原始碼分析之HashMap

王世暉發表於2016-05-25

HashMap的底層資料結構

HashMap底層採用陣列加連結串列的資料結構儲存鍵值對
Hash根據key的雜湊值轉化為陣列的下標將鍵值對存入陣列中,陣列的元素是一個連結串列,衝突的key放置在陣列的同一個位置,使用連結串列將衝突的資料連結起來
陣列的底層結構如下:

     /**
     * An empty table instance to share when the table is not inflated.
     */
    static final Entry<?,?>[] EMPTY_TABLE = {};

    /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

可見陣列儲存的元素是Entry,而Entry表示連結串列的節點,節點儲存了key、value,key的hash值,指向下一個節點的指標域next
Entry資料hashCode()方法是通過key的hash值和value的hash值異或得到的
equals方法判斷兩個Entry相等的標準是key相等且value相等
重寫equals方法必須重寫hashCode方法,滿足兩個物件相等(equals返回true)則這兩個物件的hash值必須相等

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }

        public final int hashCode() {
            return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
        }

        public final String toString() {
            return getKey() + "=" + getValue();
        }

        /**
         * This method is invoked whenever the value in an entry is
         * overwritten by an invocation of put(k,v) for a key k that's already
         * in the HashMap.
         */
        void recordAccess(HashMap<K,V> m) {
        }

        /**
         * This method is invoked whenever the entry is
         * removed from the table.
         */
        void recordRemoval(HashMap<K,V> m) {
        }
    }

根據hash值計算桶中的位置

    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

陣列的長度是2的整次冪,h & (length-1)等價於

h % length

但是求模運算通過除法實現,除法的的運算比較耗費CPU時間,因此巧妙地轉化為位運算來求模

根據key獲取entry

    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

首先判斷size是否為0,size為0表示HashMap中沒有鍵值對,此時通過key獲取value必然返回null
然後判斷key是否為null,null作為key,其hash值為0,null key在entry陣列中的位置為index=0處
key不為null的話通過indexFor(hash, table.length)計算桶的位置
根據桶的位置獲取連結串列頭,然後遍歷連結串列獲取所求的entry

Entry<K,V> e = table[indexFor(hash, table.length)];

此時的e表示連結串列的頭部,此連結串列的所有節點key的hash值相同(衝突),但是key不同,因此掃描連結串列,獲取對應key的entry,然後返回此entry。

鍵key為null時候的特殊處理

HashMap允許null的鍵和null的值,鍵為null的entry都放在entry陣列索引為0地方,即下標為0的桶內(也有可能其他非null鍵hash值剛好為0,因此需要掃描比較)

    private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }
    如果size==0,則value必然為null,直接返回null
    否則通過Entry<K,V> e = table[0]獲取連結串列頭部
    通過e.key == null找到key為null的entry,返回此entry的value

通過鍵獲取值

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

分類處理,key為null通過getForNullKey()獲取對應的value
否則通過getEntry(key)獲取對應的entry,通過entry獲取value

向entry連結串列新增一個新的entry

 /**
     * Adds a new entry with the specified key, value and hash code to
     * the specified bucket.  It is the responsibility of this
     * method to resize the table if appropriate.
     *
     * Subclass overrides this to alter the behavior of put method.
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

首先判斷size >= threshold,如果儲存的鍵值對數量size大於閾值threshold的話,需要擴容,然後rehash,重新計算hash值和桶的位置
然後呼叫createEntry把entry新增到連結串列頭

    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

根據key刪除資料

根據key的hash值找到桶,遍歷桶中連結串列,找到對應key的entry,刪除。刪除的辦法就是entry的前序節點的後繼指標直接指向entry節點的後繼節點

    final Entry<K,V> removeEntryForKey(Object key) {
        if (size == 0) {
            return null;
        }
        int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }
        return e;
    }

如果size==0,HashMap中沒有儲存任何鍵值對,對任何key取value均返回null
否則:
計算key的hash值,key==null設定key的hash值為0
否則通過hash(key)計算hash值
然後根據key的hash值計算桶的位置

int i = indexFor(hash, table.length);

然後開始遍歷連結串列,找到與傳入key相等(==或equals)的entry
此entry的前序節點的後繼指標指向entry的後繼節點,跳過此entry節點即可

判斷是否包含某個value

    public boolean containsValue(Object value) {
        if (value == null)
            return containsNullValue();

        Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (value.equals(e.value))
                    return true;
        return false;
    }

HashMap判斷是否包含某個value,只能通過窮舉的辦法,在每個桶內一個一個挨著比較,找到就返回true
HashMap接受null的鍵值,null的鍵儲存在下標為0的桶內,null的值儲存在對應key所在的桶內
判斷是否包含某一特定的value,需要分類討論
如果該value是null,則不能通過equals方法比較,通過==比較

    /**
     * Special-case code for containsValue with null argument
     */
    private boolean containsNullValue() {
        Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (e.value == null)
                    return true;
        return false;
    }

如果value不為null,窮舉查詢比較

        Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (value.equals(e.value))
                    return true;
        return false;

相關文章