日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪(fǎng)問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) >

Java集合源码学习(四)HashMap

發(fā)布時(shí)間:2025/7/14 45 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java集合源码学习(四)HashMap 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

?一、數(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[]。

/*** 內(nèi)部實(shí)際的存儲(chǔ)數(shù)組,如果需要調(diào)整,容量必須是2的冪*/transient Entry[] table;

再來(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,

DEFAULT_LOAD_FACTOR = 0.75f;

也就是數(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)造和初始化。

public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR;threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);table = new Entry[DEFAULT_INITIAL_CAPACITY];init();}public HashMap(Map<? extends K, ? extends V> m) {this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);putAllForCreate(m);}public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}public HashMap(int initialCapacity, float loadFactor) {......}

?

四、解決哈希沖突的辦法

(1)什么是哈希沖突

理論上哈希函數(shù)的輸入域是無(wú)限的,優(yōu)秀的哈希函數(shù)可以將沖突減少到最低,但是卻不能避免,下面是一個(gè)典型的哈希沖突的例子:

用班級(jí)同學(xué)做比喻,現(xiàn)有如下同學(xué)數(shù)據(jù)

張三,李四,王五,趙剛,吳露.....
假如我們編址規(guī)則為取姓氏中姓的開(kāi)頭字母在字母表的相對(duì)位置作為地址,則會(huì)產(chǎn)生如下的哈希表

位置字母姓名?
0a??
1b??
2c??

...

10???L????李四?

...

22W王五,吳露?

..

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鏈的頭部。

public V put(K key, V value) {if (key == null)return putForNullKey(value);int hash = hash(key.hashCode());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;}

 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)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。