Java源碼HashMap源碼的使用分析
Java 源碼 HashMap源碼分析
1 初始容量
/**
* The default initial capacity - MUST be a power of two.
* 默認(rèn)的初始容量,必須為2的冪
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16容量表示哈希表中槽的數(shù)量(即哈希數(shù)組的長度),初始容量是創(chuàng)建哈希表時的容量(從構(gòu)造函數(shù)中可以看出,如果不指明,則默認(rèn)為16)
無論我們指定的容量為多少,構(gòu)造方法都會將實(shí)際容量設(shè)為不小于指定容量的2的次方的一個數(shù),且最大值不能超過2的30次方
2 加載因子
/**
* The load factor used when none specified in constructor.
* 在構(gòu)造函數(shù)中沒有指定時使用的加載因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;哈希表在其容量自動增加之前可以達(dá)到多滿的一種尺度,當(dāng)哈希表中的條目數(shù)超出了加載因子與當(dāng)前容量的乘積時,則要對該哈希表進(jìn)行 resize 操作(即擴(kuò)容)。
- 如果加載因子越大,對空間的利用更充分,但是查找效率會降低(鏈表長度會越來越長);
- 如果加載因子太小,那么表中的數(shù)據(jù)將過于稀疏(很多空間還沒用,就開始擴(kuò)容了),對空間造成嚴(yán)重浪費(fèi)。
- 如果我們在構(gòu)造方法中不指定,則系統(tǒng)默認(rèn)加載因子為0.75,這是一個比較理想的值,一般情況下我們是無需修改的。
3 單向鏈表中的數(shù)據(jù)節(jié)點(diǎn)
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
}
}4 紅黑樹結(jié)構(gòu)
在jdk1.8版本后,java對HashMap做了改進(jìn),當(dāng)鏈表長度必須大于 2 ,并且應(yīng)該至少為 8 的時候,將后面的數(shù)據(jù)存在紅黑樹中,以加快檢索速度,我們接下來講一下紅黑樹。
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;5 概述

概括的說,HashMap 是一個關(guān)聯(lián)數(shù)組、哈希表,它是線程不安全的,允許key為null,value為null。遍歷時無序。
其底層數(shù)據(jù)結(jié)構(gòu)是數(shù)組稱之為哈希桶,每個桶里面放的是鏈表,鏈表中的每個節(jié)點(diǎn),就是哈希表中的每個元素。
在JDK8中,當(dāng)鏈表長度達(dá)到8,會轉(zhuǎn)化成紅黑樹,以提升它的查詢、插入效率,它實(shí)現(xiàn)了Map<K,V>, Cloneable, Serializable接口。
因其底層哈希桶的數(shù)據(jù)結(jié)構(gòu)是數(shù)組,所以也會涉及到擴(kuò)容的問題。
當(dāng)HashMap的容量達(dá)到threshold域值時,就會觸發(fā)擴(kuò)容。擴(kuò)容前后,哈希桶的長度一定會是2的次方。
這樣在根據(jù)key的hash值尋找對應(yīng)的哈希桶時,可以用位運(yùn)算替代取余操作,更加高效。
而key的hash值,并不僅僅只是key對象的hashCode()方法的返回值,還會經(jīng)過擾動函數(shù)的擾動,以使hash值更加均衡。
因?yàn)?code>hashCode()是int類型,取值范圍是40多億,只要哈希函數(shù)映射的比較均勻松散,碰撞幾率是很小的。
但就算原本的hashCode()取得很好,每個key的hashCode()不同,但是由于HashMap的哈希桶的長度遠(yuǎn)比hash取值范圍小,默認(rèn)是16,所以當(dāng)對hash值以桶的長度取余,以找到存放該key的桶的下標(biāo)時,由于取余是通過與操作完成的,會忽略hash值的高位。因此只有hashCode()的低位參加運(yùn)算,發(fā)生不同的hash值,但是得到的index相同的情況的幾率會大大增加,這種情況稱之為hash碰撞。 即,碰撞率會增大。
擾動函數(shù)就是為了解決hash碰撞的。它會綜合hash值高位和低位的特征,并存放在低位,因此在與運(yùn)算時,相當(dāng)于高低位一起參與了運(yùn)算,以減少hash碰撞的概率。(在JDK8之前,擾動函數(shù)會擾動四次,JDK8簡化了這個操作)
擴(kuò)容操作時,會new一個新的Node數(shù)組作為哈希桶,然后將原哈希表中的所有數(shù)據(jù)(Node節(jié)點(diǎn))移動到新的哈希桶中,相當(dāng)于對原哈希表中所有的數(shù)據(jù)重新做了一個put操作。所以性能消耗很大,可想而知,在哈希表的容量越大時,性能消耗越明顯。
- 擴(kuò)容時,如果發(fā)生過哈希碰撞,節(jié)點(diǎn)數(shù)小于8個。則要根據(jù)鏈表上每個節(jié)點(diǎn)的哈希值,依次放入新哈希桶對應(yīng)下標(biāo)位置。
- 因?yàn)閿U(kuò)容是容量翻倍,所以原鏈表上的每個節(jié)點(diǎn),現(xiàn)在可能存放在原來的下標(biāo),即low位, 或者擴(kuò)容后的下標(biāo),即high位。 high位= low位+原哈希桶容量
- 如果追加節(jié)點(diǎn)后,鏈表數(shù)量>=8,則轉(zhuǎn)化為紅黑樹
由迭代器的實(shí)現(xiàn)可以看出,遍歷HashMap時,順序是按照哈希桶從低到高,鏈表從前往后,依次遍歷的。屬于無序集合。
6 put操作
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
7 重寫equals方法需同時重寫hashCode方法
關(guān)于HashMap的源碼分析就介紹到這兒了,最后我們再聊聊老生常談的一個問題,各種資料上都會提到,“重寫equals時也要同時覆蓋hashcode”,我們舉個小例子來看看,如果重寫了equals而不重寫hashcode會發(fā)生什么樣的問題
/**
* Created by chengxiao on 2016/11/15.
*/
public class MyTest {
private static class Person{
int idCard;
String name;
public Person(int idCard, String name) {
this.idCard = idCard;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()){
return false;
}
Person person = (Person) o;
//兩個對象是否等值,通過idCard來確定
return this.idCard == person.idCard;
}
}
public static void main(String []args){
HashMap<Person,String> map = new HashMap<Person, String>();
Person person = new Person(1234,"喬峰");
//put到hashmap中去
map.put(person,"天龍八部");
//get取出,從邏輯上講應(yīng)該能輸出“天龍八部”
System.out.println("結(jié)果:"+map.get(new Person(1234,"蕭峰")));
}
}如果我們已經(jīng)對HashMap的原理有了一定了解,這個結(jié)果就不難理解了。盡管我們在進(jìn)行g(shù)et和put操作的時候,使用的key從邏輯上講是等值的(通過equals比較是相等的),但由于沒有重寫hashCode方法,所以put操作時,key(hashcode1)–>hash–>indexFor–>最終索引位置 ,而通過key取出value的時候 key(hashcode1)–>hash–>indexFor–>最終索引位置,由于hashcode1不等于hashcode2,導(dǎo)致沒有定位到一個數(shù)組位置而返回邏輯上錯誤的值null(也有可能碰巧定位到一個數(shù)組位置,但是也會判斷其entry的hash值是否相等,上面get方法中有提到。)
所以,在重寫equals的方法的時候,必須注意重寫hashCode方法,同時還要保證通過equals判斷相等的兩個對象,調(diào)用hashCode方法要返回同樣的整數(shù)值。而如果equals判斷不相等的兩個對象,其hashCode可以相同(只不過會發(fā)生哈希沖突,應(yīng)盡量避免)。
8 數(shù)組的最大容量
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;最大容量(必須是2的冪且小于2的30次方,傳入容量過大將被這個值替換)
如果傳入的容量cap不是2的冪次方,則找出"大于cap"的最小的2的冪
9 存放的最大元素?cái)?shù)量
顯然是 Integer.MAX_VALUE
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}10 擴(kuò)容
- 當(dāng)元素超過數(shù)組長度的
75%就會發(fā)生擴(kuò)容,既長度增加一倍。 - 默認(rèn)的數(shù)組長度
DEFAULT _INITIAL_ CAPACITY = 16,默認(rèn)的負(fù)載因子DEFAULT LOAD FACTOR =0.75;當(dāng)鍵值對數(shù)量超過16 * 0.75 = 12時,就會觸發(fā)擴(kuò)容導(dǎo)致數(shù)組長度變?yōu)?16 * 2 = 32。
注意:擴(kuò)容后,每個鍵值對數(shù)據(jù)存儲的索引下標(biāo)需要重新計(jì)算。通過公式:keyHash&(newLength-1)。結(jié)果會變成:newIndex = oldIndex + 擴(kuò)容增加的長度。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}總結(jié)
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java8 使用工廠方法supplyAsync創(chuàng)建CompletableFuture實(shí)例
這篇文章主要介紹了Java8 使用工廠方法supplyAsync創(chuàng)建CompletableFuture實(shí)例,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11
Java導(dǎo)入新項(xiàng)目報(bào)錯java:JDK?isn‘t?specified?for?module解決辦法
這篇文章主要給大家介紹了關(guān)于Java導(dǎo)入新項(xiàng)目報(bào)錯java:JDK?isn‘t?specified?for?module的解決辦法,當(dāng)您在導(dǎo)入Java項(xiàng)目時遇到錯誤時,可以嘗試以下面的方法來處理,需要的朋友可以參考下2024-05-05
Springboot事件監(jiān)聽與@Async注解詳解
這篇文章主要介紹了Springboot事件監(jiān)聽與@Async注解詳解,在開發(fā)中經(jīng)常可以利用Spring事件監(jiān)聽來實(shí)現(xiàn)觀察者模式,進(jìn)行一些非事務(wù)性的操作,如記錄日志之類的,需要的朋友可以參考下2024-01-01
Spring Boot 集成Shiro的多realm配置過程
這篇文章主要介紹了Spring Boot 集成Shiro的多realm配置,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-10-10
Maven介紹與配置+IDEA集成Maven+使用Maven命令小結(jié)
Maven是Apache軟件基金會的一個開源項(xiàng)目,是一個優(yōu)秀的項(xiàng)目構(gòu)建管理工具,它用來幫助開發(fā)者管理項(xiàng)目中的 jar,以及 jar 之間的依賴關(guān)系、完成項(xiàng)目的編譯、測試、打包和發(fā)布等工作,本文給大家介紹Maven介紹與配置+IDEA集成Maven+使用Maven命令,感興趣的朋友一起看看吧2024-01-01
Java語言實(shí)現(xiàn)簡單FTP軟件 FTP軟件遠(yuǎn)程窗口實(shí)現(xiàn)(6)
這篇文章主要為大家詳細(xì)介紹了Java語言實(shí)現(xiàn)簡單FTP軟件,F(xiàn)TP軟件遠(yuǎn)程窗口的實(shí)現(xiàn)方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-03-03
Spring Boot中使用Server-Sent Events (SSE) 實(shí)
Server-Sent Events (SSE) 是HTML5引入的一種輕量級的服務(wù)器向?yàn)g覽器客戶端單向推送實(shí)時數(shù)據(jù)的技術(shù),本文主要介紹了Spring Boot中使用Server-Sent Events (SSE) 實(shí)現(xiàn)實(shí)時數(shù)據(jù)推送教程,具有一定的參考價值,感興趣的可以了解一下2024-03-03

