詳解Java HashMap實(shí)現(xiàn)原理
HashMap是基于哈希表的Map接口實(shí)現(xiàn),提供了所有可選的映射操作,并允許使用null值和null建,不同步且不保證映射順序。下面記錄一下研究HashMap實(shí)現(xiàn)原理。
HashMap內(nèi)部存儲(chǔ)
在HashMap內(nèi)部,通過(guò)維護(hù)一個(gè) 瞬時(shí)變量數(shù)組table (又稱:桶) 來(lái)存儲(chǔ)所有的鍵值對(duì)關(guān)系,桶 是個(gè)Entry對(duì)象數(shù)組,桶 的大小可以按需調(diào)整大小,長(zhǎng)度必須是2的次冪。如下代碼:
/** * 一個(gè)空的entry數(shù)組,桶 的默認(rèn)值 */ static final Entry<?,?>[] EMPTY_TABLE = {}; /** * 桶,按需調(diào)整大小,但必須是2的次冪 */ transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
初始容量與負(fù)載因子
HashMap有兩個(gè)參數(shù)影響性能,初始容量和負(fù)載因子。容量是哈希表中 桶 的數(shù)量,初始容量只是哈希表在創(chuàng)建時(shí)的容量,負(fù)載因子是哈希表在其容量自動(dòng)增加之前可以達(dá)到多滿的一種尺度。當(dāng)哈希表中條目數(shù)超出了負(fù)載因子與當(dāng)前容量的乘積時(shí),則要對(duì)該Hash表進(jìn)行rehash操作(即重建內(nèi)部數(shù)據(jù)結(jié)構(gòu)),重建時(shí)以當(dāng)前容量的兩倍數(shù)目新建??梢酝ㄟ^(guò)構(gòu)造器設(shè)置初始容量與負(fù)載因子,默認(rèn)初始容量是16個(gè)條目,最大容量是2^30次方個(gè)條目,默認(rèn)負(fù)載因子是0.75
桶 就像一個(gè)存水的水桶,它默認(rèn)的初始存水容量是16個(gè)單位的水,默認(rèn)在灌水灌到16*0.75時(shí),在下次添加數(shù)據(jù)時(shí)會(huì)先擴(kuò)充容量,擴(kuò)充到32單位。0.75就是負(fù)載因子,初始容量與負(fù)載因子可以通過(guò)創(chuàng)建水桶的時(shí)候進(jìn)行設(shè)置。水桶最大的容量是2的30次方個(gè)單位的水。當(dāng)初始容量設(shè)置的數(shù)量大于最大容量時(shí),以最大容量為準(zhǔn)。當(dāng)擴(kuò)展時(shí)如果大于等于最大容量時(shí)則直接返回。
如下為HashMap的部分源碼,定義了默認(rèn)初始容量、負(fù)載因子及其他一些常量:
/** * 默認(rèn)初始化容量,必須為2的次冪The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * 最大容量,如果通過(guò)構(gòu)造函數(shù)參數(shù)中傳遞初始化容量大于該最大容量了,也會(huì)使用該容量為初始化容量 * 必須是2的次冪且小于等于2的30次方 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 默認(rèn)的負(fù)載因子,可以通過(guò)構(gòu)造函數(shù)指定 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 一個(gè)空的數(shù)組表,當(dāng) 桶沒有初始化的時(shí)候 */ static final Entry<?,?>[] EMPTY_TABLE = {}; /** * 桶 , 存儲(chǔ)所有的鍵值對(duì)條目,可以按需調(diào)整大小,長(zhǎng)度大小必須為2的次冪 */ transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; /** * Map中鍵值對(duì)的數(shù)量,在每次新增或刪除的時(shí)候都會(huì)對(duì)size進(jìn)行+1或者-1操作. */ transient int size; /** * 負(fù)載值,需要調(diào)整大小的臨界值,為:(capacity * load factor).在每次調(diào)整大小后會(huì)使用新的容量計(jì)算一下 * @serial */ // If table == EMPTY_TABLE then this is the initial capacity at which the // table will be created when inflated. int threshold; /** * 負(fù)載因子,如果構(gòu)造函數(shù)中沒有指定,則采用默認(rèn)的負(fù)載因子, * * @serial */ final float loadFactor; /** * HashMap結(jié)構(gòu)修改次數(shù),結(jié)構(gòu)修改時(shí)改變HashMap中的映射數(shù)量或修改其內(nèi)部結(jié)構(gòu)(例如,* rehash方法,重建內(nèi)部數(shù)據(jù)結(jié)構(gòu)),此字段用于在 * HashMap的集合視圖上生成的迭代器都處理成快速失敗的 */ transient int modCount;
初始容量與負(fù)載因子性能調(diào)整
通常,默認(rèn)負(fù)載因子(0.75)在時(shí)間和空間成本上尋求一種折中。負(fù)載因子過(guò)高雖然減少了空間開銷,但同時(shí)也增加了查詢成本(在大多數(shù)HashMap類的操作中,包括get和put操作,都反映了這一點(diǎn))。在設(shè)置初始容量時(shí)應(yīng)該考慮到映射中所需的條目數(shù)及其負(fù)載因子,以便最大限度的減少rehash操作次數(shù)。如果初始容量大于最大條目數(shù)除以加載因子,則不會(huì)發(fā)生rehash操作。
如果很多映射關(guān)系要存儲(chǔ)在HashMap實(shí)例中,則相對(duì)于按需執(zhí)行自動(dòng)的rehash操作以增大表的容量來(lái)說(shuō),使用足夠大的初始容量創(chuàng)建它將使得映射關(guān)系能更有效的存儲(chǔ)。
如下為重建HashMap數(shù)據(jù)結(jié)構(gòu)的代碼:
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { // 如果容量已達(dá)最大限制,則設(shè)置下負(fù)載值后直接返回 threshold = Integer.MAX_VALUE; return; } // 創(chuàng)建新的table存儲(chǔ)數(shù)據(jù) Entry[] newTable = new Entry[newCapacity]; // 將舊table中的數(shù)據(jù)轉(zhuǎn)存到新table中去,這一步會(huì)花費(fèi)比較多的時(shí)間 transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; // 最后設(shè)置下下次調(diào)整大小的負(fù)載值 threshold = (int) Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
HashMap構(gòu)造方法
第四個(gè)構(gòu)造方法是以已經(jīng)存在的Map創(chuàng)建一個(gè)新的HashMap,稍后再說(shuō),前三個(gè)構(gòu)造方法,其實(shí)最終調(diào)用的都是第三個(gè)帶兩個(gè)參數(shù)的方法,如果沒有傳遞參數(shù)則使用默認(rèn)的數(shù)值,代碼如下:
/** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */ public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } /** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and the default load factor (0.75). * * @param initialCapacity the initial capacity. * @throws IllegalArgumentException if the initial capacity is negative. */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and load factor. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */ public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; threshold = initialCapacity; init(); }
由上可以看出,在構(gòu)造函數(shù)中,如果初始容量給的大于最大容量,則直接以最大容量代替。
put方法
接下來(lái)就看看HashMap中比較重要的部分
/** * 在此映射中關(guān)聯(lián)指定值與指定建。如果該映射以前包含了一個(gè)該鍵的映射關(guān)系,則舊值被替換 * * @param 指定將要關(guān)聯(lián)的鍵 * @param 指定將要關(guān)聯(lián)的值 * @return 與key關(guān)聯(lián)的舊值,如果key沒有任何映射關(guān)系,則返回null(返回null還可能表示該映射之前將null與key關(guān)聯(lián)) */ public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } 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; }
1. 首先put方法中,先判斷 桶 是否為默認(rèn)的未初始化狀態(tài),如果未初始化則調(diào)用 inflateTable 方法去初始化,然后判斷參數(shù)key是否為null,如果為null,則調(diào)用putForNullKey專門進(jìn)行放key為null的數(shù)據(jù),putForNullKey方法與下面的第3步開始其實(shí)都是一樣的,只不過(guò)key為null的數(shù)據(jù)默認(rèn)存儲(chǔ)位置就是第一個(gè),即下標(biāo)默認(rèn)為0。
2. 如果key不是null,則調(diào)用hash()方法獲取key的hash值,可以根據(jù)hash值、桶的長(zhǎng)度通過(guò)indexFor方法計(jì)算該key可以放到桶的位置。
3. Entry對(duì)象中有一個(gè)屬性next,可以形成一個(gè)單向鏈表,用來(lái)存儲(chǔ)哈希值相同的元素。因此當(dāng)計(jì)算出來(lái)key的hash值重復(fù)時(shí),存儲(chǔ)位置也會(huì)重復(fù),只要判斷一下存儲(chǔ)位置的元素及該元素的next屬性鏈表中是否與給定的key和key的hash值是否完全一致就可以了。如果有完全一致的,代表已經(jīng)存在,則替換舊值,并把舊值做為返回值直接返回。
4. 把結(jié)構(gòu)修改次數(shù)自增1
5. 調(diào)用addEntry方法將新的鍵值對(duì)增加到HashMap中。addEntity方法首先判斷當(dāng)前條目數(shù)據(jù)是否已經(jīng)大于等于負(fù)載值(桶的容量*負(fù)載因子)且桶的指定位置不為null,如果已經(jīng)大于且指定位置不為null,則調(diào)調(diào)整桶的容量為當(dāng)前容量的2倍,調(diào)整桶的容量參照上面的初始容量與負(fù)載因子性能調(diào)整 目錄。重新計(jì)算Hash值,計(jì)算存放位置。調(diào)用createEntry方法存放到 桶 中
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構(gòu)造方法,創(chuàng)建一個(gè)新的Entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }
6. 在 createEntry 方法中,首先獲取指定位置的entry,然后新生成一個(gè)entry,在生成entry時(shí)把原有的entry存儲(chǔ)到新生成的entry的next屬性中(參考Entry的構(gòu)造方法),并把指定位置的entry替換成新生成的。
因?yàn)樾略鰲l目的時(shí)候,需要計(jì)算hash值,長(zhǎng)度不夠時(shí)需要調(diào)整長(zhǎng)度,當(dāng)計(jì)算的存儲(chǔ)位置已有元素的時(shí)候需要進(jìn)行鏈表式的存儲(chǔ),所以使用HashMap新增操作的效率并不是太高。
get方法
首先看下get方法的源碼:
/** * 返回指定鍵所映射的值;如果對(duì)于該鍵來(lái)說(shuō),此映射不包含任何映射關(guān)系,則返回null * 返回null值并不一定表明該映射不包含該鍵的映射,也可能改映射將該鍵顯示的映射為null,可使用containsKey操作來(lái)區(qū)分這兩種情況 * @see #put(Object, Object) */ public V get(Object key) { if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); } final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } 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; }
get方法實(shí)現(xiàn)較簡(jiǎn)單,以下是幾個(gè)步驟:
- 首先判斷key是否為null,如果為null,則調(diào)用 getForNullKey 方法來(lái)獲取,如果不為null則調(diào)用 getEntry 方法來(lái)獲取。getForNullKey方法與getEntity基本上一致,只不過(guò)少了一個(gè)步驟,就是默認(rèn)的key為null的存儲(chǔ)位置在第一個(gè),即下標(biāo)為0,沒有去計(jì)算位置而已。
- getEntity方法根據(jù)key計(jì)算哈希值,然后用哈希值、桶的長(zhǎng)度計(jì)算存儲(chǔ)位置。
- getEntity以獲取指定位置的entry作為遍歷的開始,遍歷entry的next單鏈表,如果entry的哈希值與計(jì)算的哈希值一致且entry的key與指定的相等則返回entry
- 根據(jù)getEntity返回的值,get方法返回對(duì)應(yīng)的值。
通過(guò)查看get的源碼可以發(fā)現(xiàn),get方法通過(guò)key的哈希值與桶的長(zhǎng)度計(jì)算存儲(chǔ)位置,基本上一下就能定位到要找的元素,即使再遍歷幾個(gè)重復(fù)哈希值的key,也是很快速的,因?yàn)楣V迪鄬?duì)唯一,所以HashMap對(duì)于查找性能是非??斓?。
自定義對(duì)象作為HashMap的鍵
class User { // 身份證號(hào)碼 protected int idNumber; public User(int id){ idNumber = id; } } public class TestUser{ public static void main(String[] args) { Map<User, String> map = new HashMap<User, String>(); for (int i=0; i<5; i++) { map.put(new User(i), "姓名: " + i); } System.out.println("User3 的姓名:" + map.get(new User(3))); } } 輸出: User3 的姓名:null
如上代碼,通過(guò)自定義的User類實(shí)例作為HashMap的對(duì)象時(shí),在打印的時(shí)候是無(wú)法找到User3的姓名的,因?yàn)閁ser類自動(dòng)繼承基類Object,所以這里會(huì)自動(dòng)使用Object的hashCode方法生成哈希值,而它默認(rèn)是使用對(duì)象的地址計(jì)算哈希值的。因此new User(3)生成的第一個(gè)實(shí)例的哈希值與生成的第二個(gè)實(shí)例的哈希值是不一樣的。但是如果只需要簡(jiǎn)單的覆蓋hashCode方法,也是無(wú)法正常運(yùn)作的,除非同時(shí)覆蓋equals方法,它也是Object的一部分。HashMap使用equals()判斷當(dāng)前的鍵是否與表中存在的鍵相同,可以參考上面的get或put方法。
正確equals()方法必須滿足下列5個(gè)條件:---參考《Java編程思想》—489頁(yè)
- 自反性。對(duì)任意x,x.equals(x)一定返回true
- 對(duì)稱性。對(duì)任意x和y,如果有y.equals(x)返回true,則x.equals(y)也返回true
- 傳遞性。對(duì)任意x,y,z,如果有x.equals(y)返回true,y.equals(z)返回true,則x.equals(z)一定返回true
- 一致性,對(duì)任意x和y,如果對(duì)象中用于等價(jià)比較的信息沒有改變,那么無(wú)論調(diào)用x.equals(y)多少次,返回的結(jié)果應(yīng)該保持一致,要么一致是true,要么一致是false.
- 對(duì)任何不是null的x,x.equals(null)一定返回false
再次強(qiáng)調(diào):默認(rèn)的Object.equals()只是比較對(duì)象的地址,所以一個(gè)new User(3)并不等于另一個(gè)new User(3)。因此,如果要使用自己的類作為HashMap的鍵,必須同時(shí)重載hashCode()和equals().
如下代碼可以正常運(yùn)作:
class User { // 身份證號(hào)碼 protected int idNumber; public User(int id){ idNumber = id; } @Override public int hashCode() { return idNumber; } @Override public boolean equals(Object obj) { return obj instanceof User && (idNumber==((User)obj).idNumber); } } public class TestUser{ public static void main(String[] args) { Map<User, String> map = new HashMap<User, String>(); for (int i=0; i<5; i++) { map.put(new User(i), "姓名: " + i); } System.out.println("User3 的姓名:" + map.get(new User(3))); } } 輸出: User3 的姓名:姓名: 3
上面只是簡(jiǎn)單的在hashCode中返回了idNumber作為唯一的判別,用戶也可以根據(jù)自己的業(yè)務(wù)實(shí)現(xiàn)自己的方法。在equals方法中,instanceof會(huì)悄悄的檢查對(duì)象是否為null,如果instanceof左邊的參數(shù)為null,則會(huì)返回false,如果equals()的參數(shù)不為null且類型正確,則基于每個(gè)對(duì)象中的實(shí)際的idNumber進(jìn)行比較。從輸出可以看出,現(xiàn)在的方式是正確的。
參考:
以上就是本文的全部?jī)?nèi)容,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來(lái)一定的幫助,同時(shí)也希望多多支持腳本之家!
相關(guān)文章
Idea 搭建Spring源碼環(huán)境的超詳細(xì)教程
這篇文章主要介紹了Idea 搭建Spring源碼環(huán)境,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10聊聊@RequestParam,@PathParam,@PathVariable等注解的區(qū)別
這篇文章主要介紹了聊聊@RequestParam,@PathParam,@PathVariable等注解的區(qū)別,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-02-02Mybatis-Plus 映射匹配兼容性的問(wèn)題解決
本文主要介紹了Mybatis-Plus 映射匹配兼容性的問(wèn)題解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01淺談Spring Data Redis讀不到設(shè)進(jìn)去的值
本文主要介紹了Spring Data Redis怎么讀不到我剛才設(shè)進(jìn)去的值,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09SpringBoot項(xiàng)目中連接Gauss數(shù)據(jù)庫(kù)
本文主要介紹了SpringBoot項(xiàng)目中連接Gauss數(shù)據(jù)庫(kù),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-06-06Spring Cloud Zuul集成Swagger實(shí)現(xiàn)過(guò)程解析
這篇文章主要介紹了Spring Cloud Zuul集成Swagger實(shí)現(xiàn)過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11springboot中request和response的加解密實(shí)現(xiàn)代碼
這篇文章主要介紹了springboot中request和response的加解密實(shí)現(xiàn),在springboot中提供了RequestBodyAdviceAdapter和ResponseBodyAdvice,利用這兩個(gè)工具可以非常方便的對(duì)請(qǐng)求和響應(yīng)進(jìn)行預(yù)處理,需要的朋友可以參考下2022-06-06springboot 實(shí)現(xiàn)Http接口加簽、驗(yàn)簽操作方法
這篇文章主要介紹了springboot 實(shí)現(xiàn)Http接口加簽、驗(yàn)簽操作,服務(wù)之間接口調(diào)用,通過(guò)簽名作為安全認(rèn)證來(lái)保證API的安全性,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-09-09JAVA多線程之中斷機(jī)制stop()、interrupted()、isInterrupted()
這篇文章主要介紹了JAVA多線程之中斷機(jī)制stop()、interrupted()、isInterrupted()的相關(guān)資料,需要的朋友可以參考下2016-05-05