Java集合源码学习(四)HashMap
?一、數(shù)組、鏈表和哈希表結(jié)構(gòu)
數(shù)據(jù)結(jié)構(gòu)中有數(shù)組和鏈表來(lái)實(shí)現(xiàn)對(duì)數(shù)據(jù)的存儲(chǔ),這兩者有不同的應(yīng)用場(chǎng)景,
數(shù)組的特點(diǎn)是:尋址容易,插入和刪除困難;鏈表的特點(diǎn)是:尋址困難,插入和刪除容易;
哈希表的實(shí)現(xiàn)結(jié)合了這兩點(diǎn),哈希表的實(shí)現(xiàn)方式有多種,在HashMap中使用的是鏈地址法,也就是拉鏈法。
拉鏈法實(shí)際上是一種鏈表數(shù)組的結(jié)構(gòu),由數(shù)組加鏈表組成,在這個(gè)長(zhǎng)度為16的數(shù)組中(HashMap默認(rèn)初始化大小就是16),每個(gè)元素存儲(chǔ)的是一個(gè)鏈表的頭結(jié)點(diǎn)。
通過(guò)元素的key的hash值對(duì)數(shù)組長(zhǎng)度取模,將這個(gè)元素插入到數(shù)組對(duì)應(yīng)位置的鏈表中。
例如在圖中,337%16=1,353%16=1,于是將其插入到數(shù)組位置1的鏈表頭結(jié)點(diǎn)中。
二、關(guān)于HashMap
(1)繼承和實(shí)現(xiàn)
繼承AbstractMap抽象類(lèi),Map的一些操作在AbstractMap里已經(jīng)提供了默認(rèn)實(shí)現(xiàn),
實(shí)現(xiàn)Map接口,可以應(yīng)用Map接口定義的一些操作,明確HashMap屬于Map體系,
Cloneable接口,表明HashMap對(duì)象會(huì)重寫(xiě)java.lang.Object#clone()方法,HashMap實(shí)現(xiàn)的是淺拷貝(shallow copy),
Serializable接口,表明HashMap對(duì)象可以被序列化
(2)內(nèi)部數(shù)據(jù)結(jié)構(gòu)
HashMap的實(shí)際數(shù)據(jù)存儲(chǔ)在Entry類(lèi)的數(shù)組中,
上面說(shuō)到HashMap的基礎(chǔ)就是一個(gè)線(xiàn)性數(shù)組,這個(gè)數(shù)組就是Entry[]。
再來(lái)看一下Entry這個(gè)內(nèi)部靜態(tài)類(lèi),
static class Entry<K,V> implements Map.Entry<K,V> {final K key;//Key-value結(jié)構(gòu)的keyV value;//存儲(chǔ)值Entry<K,V> next;//指向下一個(gè)鏈表節(jié)點(diǎn)final int hash;//哈希值/*** Creates new entry.*/Entry(int h, K k, V v, Entry<K,V> n) {value = v;next = n;key = k;hash = h;}......}(3)線(xiàn)程安全
HashMap是非同步的,即線(xiàn)程不安全,在多線(xiàn)程條件下,可能出現(xiàn)很多問(wèn)題,
1.多線(xiàn)程put后可能導(dǎo)致get死循環(huán),具體表現(xiàn)為CPU使用率100%(put的時(shí)候transfer方法循環(huán)將舊數(shù)組中的鏈表移動(dòng)到新數(shù)組)
2.多線(xiàn)程put的時(shí)候可能導(dǎo)致元素丟失(在addEntry方法的new Entry<K,V>(hash, key, value, e),如果兩個(gè)線(xiàn)程都同時(shí)取得了e,則他們下一個(gè)元素都是e,然后賦值給table元素的時(shí)候有一個(gè)成功有一個(gè)丟失)
關(guān)于HashMap線(xiàn)程安全性更多的了解參考相關(guān)的網(wǎng)上資源,這里不多敘述。
需要線(xiàn)程安全的哈希表結(jié)構(gòu),可以考慮以下的方式:
使用Hashtable 類(lèi),Hashtable?是線(xiàn)程安全的;
使用并發(fā)包下的java.util.concurrent.ConcurrentHashMap,ConcurrentHashMap實(shí)現(xiàn)了更高級(jí)的線(xiàn)程安全;
或者使用synchronizedMap() 同步方法包裝 HashMap object,得到線(xiàn)程安全的Map,并在此Map上進(jìn)行操作。
如:
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {return new SynchronizedMap<>(m);}
三、常用方法
(1)Map接口定義的方法
HashMap可以應(yīng)用所有Map接口定義的方法:
public interface Map<K,V> {public static interface Entry<K,V> {//獲取該Entry的key public abstract Object getKey();//獲取該Entry的valuepublic abstract Object getValue();//設(shè)置Entry的value public abstract Object setValue(Object obj);public abstract boolean equals(Object obj);public abstract int hashCode();}//返回鍵值對(duì)的數(shù)目 int size();//判斷容器是否為空 boolean isEmpty();//判斷容器是否包含關(guān)鍵字key boolean containsKey(Object key);//判斷容器是否包含值value boolean containsValue(Object value);//根據(jù)key獲取value Object get(Object key);//向容器中加入新的key-value對(duì) Object put(Object key, Object value);//根據(jù)key移除相應(yīng)的鍵值對(duì) Object remove(Object key);//將另一個(gè)Map中的所有鍵值對(duì)都添加進(jìn)去 void putAll(Map<? extends K, ? extends V> m);//清除容器中的所有鍵值對(duì) void clear();//返回容器中所有的key組成的Set集合 Set keySet();//返回所有的value組成的集合 Collection values();//返回所有的鍵值對(duì) Set<Map.Entry<K, V>> entrySet();//繼承自O(shè)bject的方法boolean equals(Object obj);int hashCode(); }(2)構(gòu)造方法
HashMap使用Entry[] 數(shù)組存儲(chǔ)數(shù)據(jù),
另外維護(hù)了兩個(gè)非常重要的變量:initialCapacity(初始容量)、loadFactor(加載因子)。
初始容量就是初始構(gòu)造數(shù)組的大小,可以指定任何值,
但最后HashMap內(nèi)部都會(huì)將其轉(zhuǎn)成一個(gè)大于指定值的最小的2的冪,比如指定初始容量12,但最后會(huì)變成16,指定16,最后就是16。
加載因子是控制數(shù)組table的飽和度的,默認(rèn)的加載因子是0.75,
也就是數(shù)組達(dá)到容量的75%,就會(huì)自動(dòng)的擴(kuò)容。
另外,HashMap的最大容量是2^30,
static final int MAXIMUM_CAPACITY = 1 << 30;
默認(rèn)的初始化大小是16,
static final int DEFAULT_INITIAL_CAPACITY = 16;
HashMap提供了四種構(gòu)造方法,可以使用默認(rèn)的容量等進(jìn)行初始化,
也可以顯式制定大小和加載因子,還可以使用另外的map進(jìn)行構(gòu)造和初始化。
?
四、解決哈希沖突的辦法
(1)什么是哈希沖突
理論上哈希函數(shù)的輸入域是無(wú)限的,優(yōu)秀的哈希函數(shù)可以將沖突減少到最低,但是卻不能避免,下面是一個(gè)典型的哈希沖突的例子:
用班級(jí)同學(xué)做比喻,現(xiàn)有如下同學(xué)數(shù)據(jù)
張三,李四,王五,趙剛,吳露.....
假如我們編址規(guī)則為取姓氏中姓的開(kāi)頭字母在字母表的相對(duì)位置作為地址,則會(huì)產(chǎn)生如下的哈希表
| 位置 | 字母 | 姓名 | ? |
| 0 | a | ? | ? |
| 1 | b | ? | ? |
| 2 | c | ? | ? |
...
| 10??? | L???? | 李四 | ? |
...
| 22 | W | 王五,吳露 | ? |
..
| 25? | Z? | 張三,趙剛 | ? |
我們注意到,灰色背景標(biāo)示的兩行里面,關(guān)鍵字王五,吳露被編到了同一個(gè)位置,關(guān)鍵字張三,趙剛也被編到了同一個(gè)位置。老師再拿號(hào)來(lái)找張三,座位上有兩個(gè)人,"你們倆誰(shuí)是張三?"(這段描述很形象,引用自hash是如何處理沖突的?)
(2)解決哈希沖突的方法
常見(jiàn)的辦法開(kāi)放定址法,再哈希法,鏈地址法以及建立一個(gè)公共溢出區(qū)等,這里只考察鏈地址法。
鏈地址法就是最開(kāi)始我們提到的鏈表-數(shù)組結(jié)構(gòu),
將所有關(guān)鍵字為同義詞的記錄存儲(chǔ)在同一線(xiàn)性鏈表中。
?
五、源碼分析
(1)HashMap的存取實(shí)現(xiàn)
HashMap的存取主要是put和get操作的實(shí)現(xiàn)。
執(zhí)行put方法時(shí)根據(jù)key的hash值來(lái)計(jì)算放到table數(shù)組的下標(biāo),
如果hash到相同的下標(biāo),則新put進(jìn)去的元素放到Entry鏈的頭部。
get操作的實(shí)現(xiàn):
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;}
注意HashMap支持key=null的情況,看這個(gè)代碼:
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;}}......}(2)哈希函數(shù)
下面看一下HashMap使用的哈希函數(shù),源碼來(lái)自JDK1.6:
/*** 哈希函數(shù)* 看一下具體的操作,首先對(duì)h分別進(jìn)行無(wú)符號(hào)右移20位和12位,* 然后對(duì)兩個(gè)值進(jìn)行按位異或,最后再與h進(jìn)行按位異或,* 得到新的h后再進(jìn)行一次同樣的操作,分別右移7位和4位,具體的哈希函數(shù)優(yōu)劣就不去研究了* 這個(gè)方法可以盡量減少碰撞*/static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12);return h ^ (h >>> 7) ^ (h >>> 4);}(3)再散列rehash過(guò)程
當(dāng)哈希表的容量超過(guò)默認(rèn)容量時(shí),必須調(diào)整table的大小。
當(dāng)容量已經(jīng)達(dá)到最大可能值時(shí),那么該方法就將容量調(diào)整到Integer.MAX_VALUE返回,這時(shí),需要?jiǎng)?chuàng)建一個(gè)新的table數(shù)組,將table數(shù)組的元素轉(zhuǎn)移到新的table數(shù)組中。
?
/*** 再散列過(guò)程* Rehashes the contents of this map into a new array with a* larger capacity. This method is called automatically when the* number of keys in this map reaches its threshold.*/void resize(int newCapacity) {Entry[] oldTable = table;int oldCapacity = oldTable.length;if (oldCapacity == MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return;}Entry[] newTable = new Entry[newCapacity];transfer(newTable);table = newTable;threshold = (int)(newCapacity * loadFactor);}/*** 把當(dāng)前Entry[]表的全部元素轉(zhuǎn)移到新的table中*/void transfer(Entry[] newTable) {Entry[] src = table;int newCapacity = newTable.length;for (int j = 0; j < src.length; j++) {Entry<K,V> e = src[j];if (e != null) {src[j] = null;do {Entry<K,V> next = e.next;int i = indexFor(e.hash, newCapacity);e.next = newTable[i];newTable[i] = e;e = next;} while (e != null);}}}
參考?
Java HashMap的死循環(huán)?
Thinking in Java之HashMap源碼分析
hash是如何處理沖突的?
?
總結(jié)
以上是生活随笔為你收集整理的Java集合源码学习(四)HashMap的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: ElasticSearch 2 (21)
- 下一篇: 解决Maven项目pom.xml文件报x