jdk7中HashMap知識點整理

HashMap中的幾個重要變量

默認初始容量,必須是2的n次方 
static final int DEFAULT_INITIAL_CAPACITY = 16;

最大容量,當經過構造方法傳入的容量比它還大時,就用這個最大容量,必須是2的n次方
static final int MAXIMUM_CAPACITY = 1 << 30;

默認負載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

用來存儲鍵值對,能夠看到鍵值對都是存儲在Entry中的
transient Entry<K,V>[] table;

//capacity * load factor,超過這個數就會進行再哈希
int threshold;

HashMap中的元素是用名爲tableEntry數組來保存的,默認大小是16java

capacity:數組的容量
load_factor:負載因子
threshold:實際能承載的容量,等於上面兩個相乘,當size大於threshold時,就會進行rehash數組

jdk7中在面對key爲String的時候採用了區別對待,會有alternative hashing,可是這個在jdk8中已經被刪除了性能

存儲結構

圖片描述

Entry是一個鏈表結構,不只包含keyvalue,還有能夠指向下一個的nextui

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;
        }
        ...

put方法

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

首先經過hash方法對hashcode進行處理:this

final int hash(Object k) {
        int h = 0;
        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

能夠看到只是在keyhashcode值上作了一些處理,經過hash計算出來的值將會使用indexFor方法找到它應該所在的table下標:spa

static int indexFor(int h, int length) {
        return h & (length-1);
    }

這個方法其實至關於對table.length取模。.net

當須要插入的keynull時,調用putForNullKey方法處理:指針

private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

putForNullKey方法只從table[0]這個位置開始遍歷,由於keynull只放在table中的第一個位置,下標爲0,在遍歷中若是發現已經有keynull了,則替換新value,返回舊value,結束;若是尚未keynull,調用addEntry方法增長一個Entry:code

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);
    }

能夠看到jdk7中resize的條件已經發生改變了,只有當 size>=threshold而且 table中的那個槽中已經有Entry時,纔會發生resize。即有可能雖然size>=threshold,可是必須等到每一個槽都至少有一個Entry時,纔會擴容。還有注意每次resize都會擴大一倍容量對象

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++;
    }

最後看createEntry,它先保存這個桶中的第一個Entry,建立新的Entry放入第一個位置,將原來的Entry接在後面。這裏採用的是頭插法插入元素。

get方法

其實get方法和put方法一模一樣,怎麼放的怎麼拿

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

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

key爲null時,仍是去table[0]去取:

private V getForNullKey() {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

不然調用getEntry方法:

final Entry<K,V> getEntry(Object key) {
        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;
    }

這個方法也是經過key的hashcode計算出它應該所在的下標,再遍歷這個下標的Entry鏈,若是key的內存地址相等(即同一個引用)或者equals相等,則說明找到了

hash的原則

A、等冪性。無論執行多少次獲取Hash值的操做,只要對象不變,那麼Hash值是固定的。若是第一次取跟第N次取不同,那就用起來很麻煩.

B、對等性。若兩個對象equal方法返回爲true,則其hash值也應該是同樣的。舉例說明:若你將objA做爲key存入HashMap中,而後new了一個objB。在你看來objB和objA是一個東西(由於他們equal),可是使用objB到hashMap中卻取不出來東西。

C、互異性。若兩個對象equal方法返回爲false,hash值有可能相同,但最好是不一樣的,這個不是必須的,只是這樣作會提升hash類操做的性能(碰撞概率低)。

解決hash碰撞的方法:
開放地址法
鏈地址法

hashmap採用的就是鏈地址法,這種方法好處是無堆積現象,可是next指針會佔用額外空間

和jdk8中的HashMap區別

在jdk8中,仍然會根據key.hashCode()計算出hash值,再經過這個hash值去定位這個key,可是不一樣的是,當發生衝突時,會採用鏈表和紅黑樹兩種方法去處理,當結點個數較少時用鏈表(用Node存儲),個數較多時用紅黑樹(用TreeNode存儲),同時結點也不叫Entry了,而是分紅了Node和TreeNode。再最壞的狀況下,鏈表查找的時間複雜度爲O(n),而紅黑樹一直是O(logn),這樣會提升HashMap的效率。
jdk8中的HashMap中定義了一個變量TREEIFY_THRESHOLD,當節點個數>= TREEIFY_THRESHOLD - 1時,HashMap將採用紅黑樹存儲

參考資料:jdk8 HashMap

和Hashtable的區別

Hashtable 和 HashMap的區別

相關文章
相關標籤/搜索