HashMap原始碼解讀

z1340954953發表於2018-03-23

介紹了ArrayList和LinkedList,就兩者而言,反應的是兩種思想

1> ArrayList底層是以陣列構成的,查詢和在不擴容的情況下,順序新增元素很快,插入和刪除較慢

2> LinkedList底層是雙向連結串列實現的,查詢需要從header節點向前或向後遍歷,插入和刪除元素快

是否存在一個集合具備上面兩個的優點,就是HashMap

HashMap是一種key-value形式儲存的資料結構

HashMap的關注點

是否允許為空key和value都允許為空
是否允許重複key重複會覆蓋,value允許重複
是否有序無序,遍歷得到的順序基本上不可能是put的順序
是否執行緒安全非執行緒安全

HashMap的資料結構

java中,最基本的結構就是兩種,一個是陣列,另一個是模擬指標(引用),HashMap實際上一個連結串列雜湊的資料結構,就是陣列和連結串列的結合體


從上圖可以看出,HashMap底層就是一個陣列結構,陣列中的每一項又是一個連結串列,新建一個HashMap的時候,就會初始化一個陣列.

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
{
    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
    transient Entry[] table;
 static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        final int hash;

從原始碼可以看出,hashmap的底層是一個Entry陣列,每一個entry儲存了key-vlaue 和下一個entry的引用,是個連結串列結構

HashMap的儲存

public V put(K key, V value) {
		//如果key是null,將這個entry放在table[0]的位置
        if (key == null)
            return putForNullKey(value);
		//根據key的hashcode計算hash值
        int hash = hash(key.hashCode());
		//根據key的hash值計算出在陣列中的索引位置
        int i = indexFor(hash, table.length);
		//遍歷entry[]索引的連結串列,找到hash值和key值相同的節點,覆蓋value,返回
        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;
    }

整個過程:

根據key計算他的hash值,找到在陣列中的位置(即下標),如果該陣列改位置上已經有元素,就會已連結串列的形式儲存,新加入的放在前面,原來的放後面,如果改位置上沒有元素,就會建立一個元素放在改位置。

下面詳細的介紹下,每個方法是怎麼執行的

第一步: 如果key是null,從table[0]這個索引,找到連結串列進行遍歷,如果找到節點的key為null,就將value替換原來的value,

如果table[0]處的節點為空,就建立節點放到哪裡,後面檢查size+1是否超過容量*載入因子的值,超過的話按2倍大小擴容。

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);//每次呼叫put方法重寫節點的value,都會呼叫
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }
 void addEntry(int hash, K key, V value, int bucketIndex) {
	Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }

第二步:計算hash值,找到索引位置,遍歷索引處的連結串列,找到相同的key,進行替換。

static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
static int indexFor(int h, int length) {
        return h & (length-1);
    }

在hashmap中要找到某個元素,需要根據key的hash值求得在陣列中索引位置使用hash演算法求的這個位置的時候,檢查這個位置上的連結串列是否存在相同的key,存在的話,替換value,否則,就是在這個索引位置新增元素。

在陣列索引位置處新增節點

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);
    }
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++;
    }
Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

對於createEntry方法

如果在某個索引位置處沒有元素,執行put("111","111")就建立一個節點e1,此時next = null,table[2]=e1

假設如果執行put節點e2,也放在這個索引位置,此時next=table[2],就是e1,然後有table[2] = e2 

結論:在一個索引位置,每新增一個元素,就會將table[index]指定它,新增的這個元素的next執行原來table[index]的元素

,形成了單向連結串列

為什麼HashMap的長度是2的n次方

hashmap預設長度是16,其他情況取的是大於設定容量的2的n次方的最小值

當長度是2的n次方時候,能夠減少元素得到的陣列下標在同一個位置的概率,減少碰撞。

h & (table.length-1)             hash                  table.length-1        
4 & (15-1)                       0100                    1110              = 0100
5 & (15-1)                       0101                    1110              = 0100

4 & (16-1)                       0100                    1111              = 0100
5 & (16-1)                       0101                    1111              = 0101

和15-1運算的後果是0001,0011,0111,0101,1001,1101這幾個位置都無法存放元素,因為運算的結果根本就不會算到這個索引上,不僅浪費了空間,還增加了元素碰撞的效率, 降低了查詢效率

長度是2的n次方長度,(length-1)每個位都是1,因為hash值是均勻分佈的,不同key算到的結果相同的概率很小,碰撞的機會少,效率更高

讀取

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        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.equals(k)))
                return e.value;
        }
        return null;
    }

有了前面的介紹,後面看起來就比較簡單了,如果是null,從table[0]找到key為null的節點,否則返回null

key不為null,計算hash值找到在陣列中的下表,從這個位置連結串列找到hash和key相同的節點,返回value,否則返回null

歸納起來說,HashMap在底層將key-value當做一個整體進行處理,這個整體是一個Entry物件。HashMap底層採用一個Entry[]陣列儲存所有的key-value對,當需要儲存Entry物件時,根據hash演算法決定其在陣列中的儲存位置,在根據equals方法找到連結串列上的儲存位置;當需要取出一個entry時候,也是根據hash演算法找到陣列上的位置,equals方法找到節點,取出entry

HashMap的擴容

void addEntry(int hash, K key, V value, int bucketIndex) {
	Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }

threshold = int(capacity*loadFactor)

當陣列中元素的個數大於等於threshold(容量*loadfactor),就會按照原來容量的2倍進行擴容

hashMap的效能引數

hashMap包括下面幾個構造器

new HashMap() 初始容量16,loadfactor(載入因子)為0.75

new HashMap(int capacity) 初始容量為大於capacity的2的n次方的最小值,loadfactor為0.75

new HashMap(int capacity,float loadFactor)

loadFactor:陣列中entry元素的個數除以總容量,載入因子如果過大,對空間利用率高,但是查詢效率低,反之,載入因子小,浪費空間.

Fast-fail 機制

 transient volatile int modCount;
 final Entry<K,V> nextEntry() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Entry<K,V> e = next;
            if (e == null)
                throw new NoSuchElementException();

            if ((next = e.next) == null) {
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
	    current = e;
            return e;
        }

在多執行緒環境下如果使用迭代器迭代,存在另一個執行緒修改了資料結構(put,remove),就會導致當前執行緒迭代器出現modCount!=exceptedModCount,並丟擲異常。迭代器就會快速失敗。

HashMap和HashTable的區別

HashTable和HashMap是一組相似的鍵值對集合,主要的區別

1. HashTable是執行緒安全的,通過synchronized鎖保證執行緒安全,HashMap則是執行緒不安全的

2. HashTable不允許key為null,HashMap允許key為null

public synchronized V put(K key, V value) {
	// Make sure the value is not null
	if (value == null) {
	    throw new NullPointerException();
	}

	// Makes sure the key is not already in the hashtable.
	Entry tab[] = table;
	int hash = key.hashCode();
	int index = (hash & 0x7FFFFFFF) % tab.length;
	for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
	    if ((e.hash == hash) && e.key.equals(key)) {
		V old = e.value;
		e.value = value;
		return old;
	    }
	}

3. HashTable和HashMap的hash演算法不同

參考部落格:http://www.cnblogs.com/xrq730/p/5030920.html



相關文章