HashMap的原理

1、HashMap的概述

HashMap是基於哈希表的Map 接口的實現。


2、HashMap的特點

a)Hash M ap是非線程安全     

b)HashMap允許使用null 值和null 鍵。即允許key和value爲null

c)HashMap相對執行效率要高


3、HashMap的內部存儲結構

         Java中數據結構最底層的兩種結構,一種是數組,一種是鏈表(有指針引用),數組的特點:空間連續,尋址迅速,但是在執行增加和刪除操作的時候需要有較大幅度的元素移動,代價較大,所以查詢速度快,增刪較慢。而鏈表的特性剛好相反,由於存儲空間不連續,尋址困難,但是增刪只需要修改指針,所以查詢慢,增刪快。有沒有一種數據結構來綜合一下數組和鏈表,以便發揮他們各自的優勢呢?答案就是:哈希表!哈希表具有較快的查詢速度,以及相對較快的增刪速度,所以很適合在海量的數據環境中使用。一般實現哈希表的方法採用“拉鍊法”,我們可以理解爲“鏈表的數組”,如下圖:



從上圖中,我們可以發現哈希表是由數組+鏈表組成的,一個長度爲16的數組中,每個元素存儲的是一個鏈表的頭結點。那麼這些元素是按照什麼樣的規則存儲到數組中呢。一般情況是通過hash(key)%len獲得,也就是元素的key的哈希值對數組長度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存儲在數組下標爲12的位置。它的內部其實是用一個Entity數組來實現的,屬性有key、value、next。


4、HashMap的存取

a) 存儲

當程序試圖將多個key-value放入HashMap中時,如下代碼片段爲例:


Map<String,String> map = new HashMap<String,String>();  
        map.put("姓名", "張三");  
        map.put("年齡", "25");  
        map.put("性別", "男");  


HashMap採用一種所謂的“Hash算法”來決定每個元素的存放位置。

當程序執行map.put("姓名","張三"); 時,系統會調用“姓名“的hashCode()方法,得到到一個hashCode值,(每個Java對象都有hashCode()方法,都可以通過該方法獲得他們的hashCode值。)得到這個hashCode值之後,系統就會根據這個hashCode值來決定元素的存放位置。


看一下Java  HashMap在存放元素時候 put(K key , V  value ) 源碼:


 public V put(K key, V value) 
 { 
	 // 如果 key 爲 null,調用 putForNullKey 方法進行處理
	 if (key == null) 
		 return putForNullKey(value); 
	 // 根據 key 的 keyCode 計算 Hash 值
	 int hash = hash(key.hashCode()); 
	 // 搜索指定 hash 值在對應 table 中的索引
 	 int i = indexFor(hash, table.length);
	 // 如果 i 索引處的 Entry 不爲 null,通過循環不斷遍歷 e 元素的下一個元素
	 for (Entry<K,V> e = table[i]; e != null; e = e.next) 
	 { 
		 Object k; 
		 // 找到指定 key 與需要放入的 key 相等(hash 值相同
		 // 通過 equals 比較放回 true)
		 if (e.hash == hash && ((k = e.key) == key 
			 || key.equals(k))) 
		 { 
			 V oldValue = e.value; 
			 e.value = value; 
			 e.recordAccess(this); 
			 return oldValue; 
		 } 
	 } 
	 // 如果 i 索引處的 Entry 爲 null,表明此處還沒有 Entry 
	 modCount++; 
	 // 將 key、value 添加到 i 索引處
	 addEntry(hash, key, value, i); 
	 return null; 
 } 

上面程序中用到了一個重要的內部接口:Map.Entry,每個 Map.Entry 其實就是一個 key-value 對。從上面程序中可以看出:當系統決定存儲HashMap 中的key-value 對時,完全沒有考慮 Entry 中的 value,僅僅只是根據 key 來計算並決定每個 Entry 的存儲位置。這也說明了前面的結論:我們完全可以把 Map 集合中的 value 當成 key 的附屬,當系統決定了 key 的存儲位置之後,value 隨之保存在那裏即可。 

上面方法提供了一個根據 hashCode() 返回值來計算 Hash 碼的方法:hash(),這個方法是一個純粹的數學計算,其方法如下:

static int hash(int h) 
{ 
    h ^= (h >>> 20) ^ (h >>> 12); 
    return h ^ (h >>> 7) ^ (h >>> 4); 
} 

對於任意給定的對象,只要它的 hashCode() 返回值相同,那麼程序調用 hash(int h) 方法所計算得到的 Hash 碼值總是相同的。接下來程序會調用 indexFor(int h, int length) 方法來計算該對象應該保存在 table 數組的哪個索引處。indexFor(int h, int length) 方法的代碼如下: 

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

根據上面 put 方法的源代碼可以看出,當程序試圖將一個 key-value 對放入 HashMap 中時,程序首先根據該 key 的 hashCode() 返回值決定該 Entry 的存儲位置:如果兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同。如果這兩個 Entry 的 key 通過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但 key 不會覆蓋。如果這兩個 Entry 的 key 通過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 形成 Entry 鏈,而且新添加的 Entry 位於 Entry 鏈的頭部——具體說明繼續看 addEntry() 方法的說明。 


當向 HashMap 中添加 key-value 對,由其 key 的 hashCode() 返回值決定該 key-value 對(就是 Entry 對象)的存儲位置。當兩個 Entry 對象的 key 的 hashCode() 返回值相同時,將由 key 通過 eqauls() 比較值決定是採用覆蓋行爲(返回 true),還是產生 Entry 鏈(返回 false)。 


上面程序中還調用了 addEntry(hash, key, value, i); 代碼,其中 addEntry 是 HashMap 提供的一個包訪問權限的方法,該方法僅用於添加一個 key-value 對。下面是該方法的代碼: 

void addEntry(int hash, K key, V value, int bucketIndex) 
{ 
    // 獲取指定 bucketIndex 索引處的 Entry 
    Entry<K,V> e = table[bucketIndex]; 	 // ①
    // 將新創建的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry 
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 
    // 如果 Map 中的 key-value 對的數量超過了極限
    if (size++ >= threshold) 
        // 把 table 對象的長度擴充到 2 倍。
        resize(2 * table.length); 	 // ②
} 


總結:keyàhashcodeàhàindexà遍歷鏈表à插入


b) 取出

當 HashMap 的每個 bucket 裏存儲的 Entry 只是單個 Entry ——也就是沒有通過指針產生 Entry 鏈時,此時的 HashMap 具有最好的性能:當程序通過 key 取出對應 value 時,系統只要先計算出該 key 的 hashCode() 返回值,在根據該 hashCode 返回值找出該 key 在 table 數組中的索引,然後取出該索引處的 Entry,最後返回該 key 對應的 value 即可。看 HashMap 類的 get(K key) 方法代碼: 


 public V get(Object key) 
 { 
	 // 如果 key 是 null,調用 getForNullKey 取出對應的 value 
	 if (key == null) 
		 return getForNullKey(); 
	 // 根據該 key 的 hashCode 值計算它的 hash 碼
	 int hash = hash(key.hashCode()); 
	 // 直接取出 table 數組中指定索引處的值,
	 for (Entry<K,V> e = table[indexFor(hash, table.length)]; 
		 e != null; 
		 // 搜索該 Entry 鏈的下一個 Entr 
		 e = e.next) 		 // ①
	 { 
		 Object k; 
		 // 如果該 Entry 的 key 與被搜索 key 相同
		 if (e.hash == hash && ((k = e.key) == key 
			 || key.equals(k))) 
			 return e.value; 
	 } 
	 return null; 
 } 

從上面代碼中可以看出,如果 HashMap 的每個 bucket 裏只有一個 Entry 時,HashMap 可以根據索引、快速地取出該 bucket 裏的 Entry;在發生“Hash 衝突”的情況下,單個 bucket 裏存儲的不是一個 Entry,而是一個 Entry 鏈,系統只能必須按順序遍歷每個 Entry,直到找到想搜索的 Entry 爲止——如果恰好要搜索的 Entry 位於該 Entry 鏈的最末端(該 Entry 是最早放入該 bucket 中),那系統必須循環到最後才能找到該元素。 


歸納起來簡單地說,HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Entry 對象。HashMap 底層採用一個 Entry[] 數組來保存所有的 key-value 對,當需要存儲一個 Entry 對象時,會根據 Hash 算法來決定其存儲位置;當需要取出一個 Entry 時,也會根據 Hash 算法找到其存儲位置,直接取出該 Entry。由此可見:HashMap 之所以能快速存、取它所包含的 Entry,完全類似於現實生活中母親從小教我們的:不同的東西要放在不同的位置,需要時才能快速找到它。 











發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章