一 前言
本篇是繼硬核ArrayList源碼分析,答應我每天看一遍好麼之後進行的第二篇源碼分析;學完本篇你將對hashMap的結構和方法有個全面的瞭解;面試自己有多強,超人都不知道;比如HashMap的擴容機制,最大容量是多少,HashMap鏈表是如何轉到紅黑樹,HashMap爲什麼線程不安全,HashMap的key,value是否能null值等等;
知識追尋者(Inheriting the spirit of open source, Spreading technology knowledge;)
二 源碼分析
2.1 官方說明實踐
- 官方說**HashMap實現了Map接口,擁有所有的Map操作;並且允許key,value都爲null;**實踐一下是不是這樣子;
public static void main(String[] args) {
HashMap<String, Object> map = new HashMap<>();
map.put(null,null);
System.out.println(map);//{null=null}
}
果然如此沒有報錯;再加一個null的key和value看看;不貼代碼啊了,最終輸出還是一個{null=null}
;說明HashMap的key是唯一的;?怎麼保證key唯一後面繼續分析;
- 繼續看官方說明,HashMap與HashTable不同之處是,HashMap不同步,HashTable不允許key,value爲null;實踐一下
public static void main(String[] args) {
Hashtable hashtable = new Hashtable();
hashtable.put(null,null);
}
控制檯紅了啊!!!輸出內容如下,震驚,HashTable的key,value真的不能爲null;
{null=null}Exception in thread "main"
java.lang.NullPointerException
at java.util.Hashtable.put(Hashtable.java:459)
at test.HashMapTest.main(HashMapTest.java:17)
Process finished with exit code 1
點擊源碼看看,HashTable的put方法特大號的Make開頭確保key,value不能爲null,synchronized修飾;人生值得懷疑一下,因爲知識追尋者以前都沒用過Hashtable耍代碼;
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
- 繼續看官方說明,HashMap無法保證Map中內容的順序;再次動手實踐一下,如果如此,到底啥原有讓map順序不一致呢?後續分析
public static void main(String[] args) {
HashMap<String, Object> map = new HashMap<>();
map.put("清晨","娃哭了");
map.put("中午","娃餓了");
map.put("晚上","娃不想睡覺了");
System.out.println(map);//{中午=娃餓了, 晚上=娃不想睡覺了, 清晨=娃哭了}
}
後續的官方說明就不是單單能幾句話可以描述的懂了,開胃菜完畢,還是看看具體的源碼吧,複習的時候記得看下小技巧Tip;
Tip:
HashMap允許key和value都爲null;key具有唯一性;
HashMap與HashTable不同之處是,HashMap不同步,HashTable不允許key,value爲null;
HashMap無法保證Map中內容的順序
2.2 空參構造方法分析
測試代碼
public static void main(String[] args) {
HashMap<String, Object> map = new HashMap<>();
}
構造方法源碼如下,發行HashMap 的加載因子loadFactor
的值是默認值0.75;
public HashMap() {//默認因子DEFAULT_LOAD_FACTOR=0.75;初始化容量爲16
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
走了一遍父類如下
HashMap --> AbstractMap --> Object
看下HashMap的父類圖譜,結構倒是很簡單對吧;
看到這邊,知識追尋者有2個疑問
問題一 : 負載因子0.75是什麼?
問題二:初始化容量16在哪裏?
先回答一下初始化容量16問題,知識追尋者找到成員變量,默認初始化容量字段值爲16;而且默認初始化值必須是2的倍數;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16默認的初始化容量
回答負載因子0.75是指HashMap的一個擴容機制;比如 擴容因子是0.7,假設容量爲10;當插入的數據達到第10*0.7 = 7 時就會發生擴容;那麼知識追尋者再計算一下HashMap的擴容算法,0.75 * 16 = 12;那麼也就是插入12 個數之後HashMap會發生擴容;
Tip: HashMap默認因子0.75;初始化容量16;如果容量是12,插入12個數之後再插入數據會發生擴容;
2.3 HashMap實現原理分析
官方源碼的說明如下:HashMap的實現是由桶和哈希表實現,當桶的容量過於龐大時會轉爲樹節點(與TreeMap類似,由於TreeMap底層是紅黑樹實現,故說桶轉爲紅黑樹也是對的);
拋出一個問題什麼是桶?
回答桶之前我們先了解一下什麼是哈希表(HashTable)
哈希表本質是一個鏈表數組;經過Hash算法計算後得出的int值找到數組上對應的位置;丟一張圖給讀者們看看;
知識追尋者真是爲讀者操碎了心,再丟一個計算hash值的方法給你們;記得溫習一下java基礎String類的hashCode() 方法;若2個對象相同,hash值相同;若2個字符串內容相同(equals方法),hash值相同;
public static void main(String[] args) {
int hash_morning = Objects.hashCode("清晨");
int hash_night = Objects.hashCode("晚上");
System.out.println("清晨的hash值:"+hash_morning);//清晨的hash值:899331
System.out.println("晚上的hash值:"+hash_night);//晚上的hash值:832240
}
繼續說我們的桶,一個桶就是一個鏈表中的每個節點,你也可以理解爲一個下圖中除了tab上的的entry ;看圖
當桶的容量達到一定數量後就會轉爲紅黑樹,看圖
看到這邊的讀者肯定收穫良多了,這還是開胃菜而已,進入我們今天的正題,源碼分析篇;
2.4 put方法源碼分析
測試代碼
public static void main(String[] args) {
HashMap<String, Object> map = new HashMap<>();
map.put("清晨","娃哭了");
System.out.println(map);
}
首先進入的是put方法裏面包含了putVal方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);//
}
先對key進行了一次hash再作爲putVal裏面的hash
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
看看putVal方法內容,因爲只插入一個數據,索引爲空的情況不會走else裏面內容,知識追尋者這裏省略了;
很清晰的可以看見創建了一個節點(Node<K,V>會在下面給出)放入了tab中,tab也很好理解,就是一個存放Node的列表;其索引 i = (容量 - 1) & hash值
, 本質上就是hash值拿去與的結果,所以是個哈希表;
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {//evict =true
Node<K,V>[] tab; Node<K,V> p; int n, i;// 聲明 樹數組, 樹節點,n,i
if ((tab = table) == null || (n = tab.length) == 0)//table 爲空
n = (tab = resize()).length; // resize()是 Node<K,V>[] resize() n 就是樹的長度 根據初始容量就是 n=16
if ((p = tab[i = (n - 1) & hash]) == null) // i = (16-1) & hash ---> 15 & 899342 = 14 ;
tab[i] = newNode(hash, key, value, null);// 創建新節點,並插入數據
else {
.......
}
++modCount;//集合結構修改次數+1
if (++size > threshold)// size=1 ; threshold=12
resize();
afterNodeInsertion(evict);
return null;
}
這邊做了個圖記錄一下
Node代碼如下
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//哈希值 key value異或的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 int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
...........
知識追尋者又加了一個晚上進去,跑過代碼後轉爲圖如下
這麼知識追尋者提出個想法,15的二進制是 1111 ,任何一個key ,value 的哈希值進行異或後得到Node的哈希值再與原來的1111相與 也就是取Node哈希值的二進制的後四位爲準;tab的容量爲16;存進去一個Node後 size 加1,最多也就是隻能存儲12個值就會發生擴容;這種情況下只要hash值相同就會發生碰撞,然後就會進入else方法,問題產生了,怎麼構造2個一樣的Node哈希值是個問題; 這邊給出了示例如下:
參考鏈接:https://blog.csdn.net/hl_java/article/details/71511815
public static void main(String[] args) {
System.out.println(HashMapTest.hash("Aa"));//2112
System.out.println(HashMapTest.hash("BB"));//2112
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
繼續試驗,發現 key的hash值相同後,會進入else方法,會在原來 AA 這個Node節點屬性next添加一個節點存放Bb;哇塞這不就是實現了一個entry鏈表了麼
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {//evict =true
Node<K,V>[] tab; Node<K,V> p; int n, i;// 聲明 樹數組, 樹節點,n,i
if ((tab = table) == null || (n = tab.length) == 0)//table 爲空
n = (tab = resize()).length; // resize()是 Node<K,V>[] resize() n 就是node的長度 根據初始容量就是 n=16
if ((p = tab[i = (n - 1) & hash]) == null) // i = (16-1) & hash ---> 15 & 哈希值
tab[i] = newNode(hash, key, value, null);// 創建新節點,並插入數據
else {
Node<K,V> e; K k; // 聲明 node k
if (p.hash == hash && // p這個node 存放 Aa-->a ; hash 2112
((k = p.key) == key || (key != null && key.equals(k))))//這邊key內容不相同所以沒進入,故不是相同節點
e = p;
else if (p instanceof TreeNode)// 這邊 p不是 TreeNode 故沒轉到紅黑樹加Node
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) { // 默認 桶的數量爲0
if ((e = p.next) == null) { // 將p這個Node的下一個節點賦值給e爲null;鏈表的開端喲
p.next = newNode(hash, key, value, null);// P創建節點存放BB--> b
// 其中 TREEIFY_THRESHOLD = 8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);//樹化
break;
}
if (e.hash == hash && //這邊還是判定p和 e 是否是同一個Node
((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;//key相同會新值會取代舊值
afterNodeAccess(e);
return oldValue;
}
}
++modCount;//集合結構修改次數+1
if (++size > threshold)// size=1 ; threshold=12
resize();
afterNodeInsertion(evict);
return null;
}
知識追尋者記錄一下,原來key經過hash後如果hash值相同就會在形成一個鏈表,看代碼也知道除了Tab上的第一個Entry,後面每一個Entry的Node都是一個桶;可以看見一個for循環是個無限循環,如果key的hash值相同次數達到8以後就會調用treeifyBin(tab, hash);
知識追尋者又理解了,只要桶的數量達到8後就會將鏈表節點樹化;這就是 數組 + 鏈表(每個列表的節點就是一個桶) —> 數組 + 紅黑樹;爲什麼鏈表要轉爲紅黑樹呢,這就考慮到算法的效率問題,鏈表時間複雜度o(n),而紅黑樹的時間複雜度o(logn),樹的查找效率更高;
Tip : hashMap 本質是一串哈希表;哈希表首先由數組 + 鏈表組成;當hash碰撞後會形成鏈表,每個鏈表的節點又是一個桶;當桶的數量達到8之後就會進行樹化,將鏈表轉爲紅黑樹,此時HashMap結構就是 數組 + 紅黑樹 ;如果插入相同的key,新的value會取代舊的value;
2.5 擴容源碼分析
測試代碼
public static void main(String[] args) {
HashMap<String, Object> map = new HashMap<>(12);
}
首先進入HashMap初始化代碼
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);// 容量12 ,負載因子0.75
}
進入構造器看看;很簡單 就是HashMap的最大容量值是2的30次方;其次由於輸入的數字是12不是2的次冪會進行擴容,擴容的結果也很簡單就是比輸出的值大的2的次冪;比12大的2 的次冪就是16;
public HashMap(int initialCapacity, float loadFactor) {//initialCapacity=12,loadFactor=0.75
if (initialCapacity < 0)//容量小於0,拋出非法參數異常
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)// MAXIMUM_CAPACITY = 1<<30 也就是 2 的30次方 ;記hashMap最大
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor)) // loadFactor=0.75>0 ,否則拋出非法參數異常
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;//負載因子賦值
this.threshold = tableSizeFor(initialCapacity);// 返回一個2次方長度的容量;此時 threshold = 16
}
點進入看看tableSizeFor(initialCapacity)是什麼;多個無符號右位或操作;其目的就是構造2次冪個1;很簡單的計算,讀者可以自行計算;
static final int tableSizeFor(int cap) {//cap=12
int n = cap - 1;// n =11
n |= n >>> 1;// 11無符號右移 1位的結果與11相或; n=15
n |= n >>> 2;//n=15
n |= n >>> 4;//n=15
n |= n >>> 8;//n=15
n |= n >>> 16;//n=15
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;//15+1=16
}
Tip : HashMap 的擴容機制就是比輸入容量值大的第一個二次冪
2.6 get源碼分析
測試代碼
public static void main(String[] args) {
HashMap<String, Object> map = new HashMap<>(12);
map.put("a","b");
map.get("a");
}
進入get方法
public V get(Object key) {
Node<K,V> e;//聲明Node 節點e
return (e = getNode(hash(key), key)) == null ? null : e.value;// 根據key的哈希值獲取Node節點返回Value
}
進入 getNode(hash(key), key)方法,可以看見進行了優先選取tab上的entry節點;如果不是纔會進入鏈表或者紅黑樹進行遍歷比對hash值獲取節點;看過put源碼之後get源碼都沒什麼奇點了;
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;// 聲明 節點數組tab , 節點first, e ; n ,k
if ((tab = table) != null && (n = tab.length) > 0 &&// tab.length =16 即容量
(first = tab[(n - 1) & hash]) != null) { // 與get的計算hash 值一樣; hash值= (容量-1)& hash 得到tab索引;然後獲取索引值賦值給first
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))//直接檢查是否是tab上的enrty,是直接就返回Node
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)//如果是紅黑樹就遍歷獲取節點
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);//否則循環遍歷鏈表獲取節點
}
}
return null;
}
2.7 hashCode源碼分析
測試代碼
public static void main(String[] args) {
HashMap<String, Object> map = new HashMap<>(12);
map.put("a","b");
map.put("b","c");
int hashCode = map.hashCode();
System.out.println(hashCode);//4
}
進入hashCode,就是遍歷每個entrySet的哈希值進行相加作爲HashMap的hash值
public int hashCode() {
int h = 0;
Iterator<Entry<K,V>> i = entrySet().iterator();//迭代器
while (i.hasNext())
h += i.next().hashCode();//迭代每個entrySet的哈希值相加
return h;
}
做個驗證
public static void main(String[] args) {
HashMap<String, Object> map = new HashMap<>(12);
map.put("a","b");
map.put("b","c");
Set<Map.Entry<String, Object>> entries = map.entrySet();
entries.stream().forEach(entry->{
System.out.println(entry.hashCode());
});
}
輸出內容是1,3;
Tip : HashMap 的hashCode 就是 entry 的hash值之和,其中Node的哈希值由key和value的哈希值之和;
三總結
- HashMap允許key和value都爲null;key具有唯一性;
- HashMap與HashTable不同之處是,HashMap不同步,HashTable不允許key,value爲null;
- HashMap無法保證Map中內容的順序
- HashMap默認因子0.75;初始化容量16;初始化容量爲12,插入12個數之後再插入數據會發生擴容;
- hashMap 本質是一串哈希表;哈希表首先由數組 + 鏈表組成;當hash碰撞後會形成鏈表,每個鏈表的節點又是一個桶;當桶的數量達到8之後就會進行樹化,將鏈表轉爲紅黑樹,此時HashMap結構就是 數組 + 紅黑樹 ;如果插入相同的key,新的value會取代舊的value;
- HashMap 的擴容機制就是比輸入容量值大的第一個二次冪;比如12,擴容後就是16;17擴容後就是32;
- HashMap 的hashCode 就是 entry 的hash值之和,其中Node的哈希值由key和value的哈希值之和;