HaspMap
文章目錄
- 一、理論
- 二、實踐:Java7的HashMap源碼分析
- (一)包路徑
- (二)導入包
- (三)接口和實現類
- (四)成員變量
- (五)構造函數:HashMap初始化
- (六)散列方法:hash
- (七)擴容方法:resize
- (八)put方法
- (九)get方法
- (十)size方法
- (十一)其它方法
- (十二)內部類
- 三、實踐:Java8的HashMap源碼分析
- (一)包路徑
- (二)導入包
- (三)接口和實現類
- (四)成員變量
- (五)構造函數
- (六)散列方法:hash
- (七)擴容方法:resize
- (八)put方法
- (九)get方法
- (十)其它方法
- (十一)內部類
- (十二)樹化方法
- (十三)LinkedHashMap支持的方法
- 四、總結
- 五、參考
本文準備從以下幾個方面去講解HashMap:
一、理論:基本概念、數據結構、關鍵算法
二、實踐:HashMap源碼詳細分析(以Java7和Java8爲例)
重要成員變量、構造函數、hash方法、resize方法、put方法、get方法
三、擴展:主要是一些查漏補缺和擴展學習的東西。該部分內容暫時在另一個文檔,整理中。
四 、總結
說明:該文章不是絕對原創文章,只是個人學習HashMap過程中的摘錄的筆記,大部分摘錄自網絡,包括百度百科和一些博客。
一、理論
(一)Hashing
散列法(Hashing)或哈希法是一種將字符組成的字符串轉換爲固定長度(一般是更短長度)的數值或索引值的方法,稱爲散列法,也叫哈希法。
由於通過更短的哈希值比用原始值進行數據庫搜索更快,這種方法一般用來在數據庫中建立索引並進行搜索,同時還用在各種解密算法中。
Hash是散列的意思,就是把任意長度的輸入,通過散列算法變換成固定長度的輸出,該輸出就是散列值。關於散列值,有以下幾個關鍵結論:
1、如果散列表中存在和散列原始輸入K相等的記錄,那麼K必定在f(K)的存儲位置上
2、不同關鍵字經過散列算法變換後可能得到同一個散列地址,這種現象稱爲碰撞
3、如果兩個Hash值不同(前提是同一Hash算法),那麼這兩個Hash值對應的原始輸入必定不同
(二)HashCode
1、HashCode的特點
(1)HashCode的存在主要是爲了查找的快捷性,HashCode是用來在散列存儲結構中確定對象的存儲地址的
(2)如果兩個對象equals相等,那麼這兩個對象的HashCode一定也相同
(3)如果對象的equals方法被重寫,那麼對象的HashCode方法也儘量重寫
(4)如果兩個對象的HashCode相同,不代表兩個對象就相同,只能說明這兩個對象在散列存儲結構中,存放於同一個位置
2、HashCode有什麼用
HashCode有什麼用?舉個例子:
1、假設內存中有0 1 2 3 4 5 6 7 8這8個位置,如果我有個字段叫做ID,那麼我要把這個字段存放在以上8個位置之一,如果不用HashCode而任意存放,那麼當查找時就需要到8個位置中去挨個查找
2、使用HashCode則效率會快很多,把ID的HashCode%8,然後把ID存放在取得餘數的那個位置,然後每次查找該類的時候都可以通過ID的HashCode%8求餘數直接找到存放的位置了
3、如果ID的HashCode%8算出來的位置上本身已經有數據了怎麼辦?這就取決於算法的實現了,比如ThreadLocal中的做法就是從算出來的位置向後查找第一個爲空的位置,放置數據;HashMap的做法就是通過鏈式結構連起來。反正,只要保證放的時候和取的時候的算法一致就行了。
4、如果ID的HashCode%8相等怎麼辦(這種對應的是第三點說的鏈式結構的場景)?這時候就需要定義equals了。先通過HashCode%8來判斷類在哪一個位置,再通過equals來在這個位置上尋找需要的類。對比兩個類的時候也差不多,先通過HashCode比較,假如HashCode相等再判斷equals。如果兩個類的HashCode都不相同,那麼這兩個類必定是不同的。
舉個實際的例子Set。我們知道Set裏面的元素是不可以重複的,那麼如何做到?Set是根據equals()方法來判斷兩個元素是否相等的。比方說Set裏面已經有1000個元素了,那麼第1001個元素進來的時候,最多可能調用1000次equals方法,如果equals方法寫得複雜,對比的東西特別多,那麼效率會大大降低。使用HashCode就不一樣了,比方說HashSet,底層是基於HashMap實現的,先通過HashCode取一個模,這樣一下子就固定到某個位置了,如果這個位置上沒有元素,那麼就可以肯定HashSet中必定沒有和新添加的元素equals的元素,就可以直接存放了,都不需要比較;如果這個位置上有元素了,逐一比較,比較的時候先比較HashCode,HashCode都不同接下去都不用比了,肯定不一樣,HashCode相等,再equals比較,沒有相同的元素就存,有相同的元素就不存。如果原來的Set裏面有相同的元素,只要HashCode的生成方式定義得好(不重複),不管Set裏面原來有多少元素,只需要執行一次的equals就可以了。這樣一來,實際調用equals方法的次數大大降低,提高了效率。
3、爲什麼重寫Object的equals(Object obj)方法後,也要儘量要重寫Object的hashCode()方法
在重寫Object的equals(Object obj)方法的時候,應該儘量重寫hashCode()方法,這是有原因的,下面詳細解釋下:
HashCodeClass.java:
public class HashCodeClass
{
private String str0;
private double dou0;
private int int0;
public boolean equals(Object obj)
{
if (obj instanceof HashCodeClass)
{
HashCodeClass hcc = (HashCodeClass)obj;
if (hcc.str0.equals(this.str0) &&
hcc.dou0 == this.dou0 &&
hcc.int0 == this.int0)
{
return true;
}
return false;
}
return false;
}
}
TestMain.java:
public class TestMain
{
public static void main(String[] args)
{
System.out.println(new HashCodeClass().hashCode());
System.out.println(new HashCodeClass().hashCode());
System.out.println(new HashCodeClass().hashCode());
System.out.println(new HashCodeClass().hashCode());
System.out.println(new HashCodeClass().hashCode());
System.out.println(new HashCodeClass().hashCode());
}
}
打印出來的值是:
1901116749
1807500377
355165777
1414159026
1569228633
778966024
我們希望兩個HashCodeClass類equals的前提是兩個HashCodeClass的str0、dou0、int0分別相等。那麼這個類不重寫hashCode()方法是有問題的。
現在我的HashCodeClass都沒有賦初值,那麼這6個HashCodeClass應該是全部equals的。如果以HashSet爲例,HashSet內部的HashMap的table本身的大小是16,那麼6個HashCode對16取模分別爲13、9、1、2、9、8。第一個放入table[13]的位置、第二個放入table[9]的位置、第三個放入table[1]的位置。。。但是明明是全部equals的6個HashCodeClass,怎麼能這麼做呢?HashSet本身要求的就是equals的對象不重複,現在6個equals的對象在集合中卻有5份(因爲有兩個計算出來的模都是9)。
那麼我們該怎麼做呢?重寫hashCode方法,根據str0、dou0、int0搞一個算法生成一個儘量唯一的hashCode,這樣就保證了str0、dou0、int0都相等的兩個HashCodeClass它們的HashCode是相等的,這就是重寫equals方法必須儘量要重寫hashCode方法的原因。看下JDK中的一些類,都有這麼做:
Integer.java:
public int hashCode() {
return value;
}
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
String.java:
public int hashCode() {
int h = hash;
if (h == 0) {
int off = offset;
char val[] = value;
int len = count;
for (int i = 0; i < len; i++) {
h = 31*h + val[off++];
}
hash = h;
}
return h;
}
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = count;
if (n == anotherString.count) {
char v1[] = value;
char v2[] = anotherString.value;
int i = offset;
int j = anotherString.offset;
while (n-- != 0) {
if (v1[i++] != v2[j++])
return false;
}
return true;
}
}
return false;
}
Double.java的:
public static int hashCode(double value) {
long bits = doubleToLongBits(value);
return (int)(bits ^ (bits >>> 32));
}
HashMap中的實體類Entry:
public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^ (value==null ? 0 : value.hashCode());
}
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
(三)HashMap
1、基本概念
HashMap是基於哈希表的Map接口的非同步實現。
此實現提供所有可選的映射操作,並允許使用null值和null鍵。
HashMap儲存的是鍵值對,HashMap很快。
此類不保證映射的順序,特別是它不保證該順序恆久不變。
關鍵字:非同步、允許null值和null鍵、鍵值對、不保證排序
2、數據結構
HashMap 可以看作是數組和鏈表結合組成的複合結構:
(1)數組被分爲一個個桶(bucket),
(2)每個桶存儲有一個或多個Entry對象(每個Entry對象包含三部分key、value,next),
(3)通過哈希值決定了Entry對象(鍵值對)在這個數組的尋址;
(4)哈希值相同的Entry對象(鍵值對),則以鏈表形式存儲。
(5)如果鏈表大小超過樹形轉換的閾值(TREEIFY_THRESHOLD= 8),鏈表就會被改造爲樹形結構(Java8及以後)。
問題:爲什麼要使用數組和鏈表結合的方式作爲數據結構?
先來看看數組和鏈表有什麼區別:
數組的存儲方式在內存的地址是連續的,大小是固定的,一旦分配就不能被其他引用佔用。其特點是查詢快,時間複雜度是O(1),插入和刪除的操作比較慢,時間複雜度是O(n);鏈表的存儲方式是非連續的,大小是不固定的,特點與數組相反,插入和刪除快,查詢速度慢。簡單來說,就是
數組:存儲區間連續,佔用內存嚴重,尋址容易,插入刪除困難;
鏈表:存儲區間離散,佔用內存比較寬鬆,尋址困難,插入刪除容易;
所以,Hashmap綜合使用了這兩種數據結構,實現了尋址容易,插入刪除也容易。
hashMap(Java7爲例)的結構示意圖如下:數組和鏈表
hashMap(Java8爲例)的結構示意圖如下:
圖2:數組+鏈表+紅黑樹
可以看出,HashMap底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個HashMap時,就會初始化一個數組。
當發生衝突時,相同hash值的鍵值對會組成鏈表。
這種數組+鏈表的組合形式大部分情況下都能有不錯的性能效果,Java6、7就是這樣設計的。
然而,在極端情況下,一組(比如經過精心設計的)鍵值對都發生了衝突,這時的哈希結構就會退化成一個鏈表,使HashMap性能急劇下降。
所以在Java8中,HashMap的結構實現變爲數組+鏈表+紅黑樹
3、工作原理
(1)Java7
HashMap先聲明一個下標範圍比較大的數組來存儲元素,數組存儲的元素是一個Map.Entry類,這個類有幾個重要的數據域,hash值、key、value(鍵值對),next(指向下一個Entry),並不是僅僅只在Entry類中存儲值。
使用put(key, value)存儲對象到HashMap中,使用get(key)從HashMap中獲取對象。
其主要工作原理總結爲以下三步:
1、首先判斷Key是否爲Null,如果爲null,直接查找Enrty[0],如果不是Null,先計算Key的HashCode,然後經過二次Hash。得到Hash值,這裏的Hash特徵值是一個int值。
2、根據Hash值,要找到對應的數組,所以對Entry[]的長度length求餘,得到的就是Entry數組的index。
3、找到對應的數組,就是找到了所在的鏈表,然後按照鏈表的操作對Value進行插入、刪除和查詢操作。
(2)java8
Java8中存儲元素的類改爲了Node,但基本思想和Java7是基本一致的。
4、查詢時間複雜度
HashMap的本質可以認爲是一個數組,每一個索引對應的每一個數組節點被稱爲桶,每個桶裏放着一個單鏈表,一個節點連着一個節點。
通過下標來檢索數組元素時間複雜度爲O(1),而遍歷鏈表的時間複雜度是O(n),所以在鏈表長度儘可能短的前提下,HashMap的查詢複雜度接近O(1)
5、hash衝突問題
哈希表直接定址可能存在衝突,舉一個簡單的例子。例如:
第一個鍵值對A進來。通過計算其key的hash得到的index=0。記做:Entry[0] = A。
第二個鍵值對B,通過計算其index也等於0, HashMap會將B.next =A,Entry[0] =B,
第三個鍵值對 C,index也等於0,那麼C.next = B,Entry[0] = C;
這樣我們發現index=0的地方事實上存取了A,B,C三個鍵值對,它們通過next這個屬性鏈接在一起。 對於不同的元素,可能計算出了相同的函數值,這樣就產生了“衝突”。
這就需要解決衝突,“直接定址”與“解決衝突”是哈希表的兩大特點。
解決Hash衝突有以下幾種方法:
a. 鏈地址法:將哈希表的每個單元作爲鏈表的頭結點,所有哈希地址爲 i 的元素構成一個同義詞鏈表。即發生衝突時就把該關鍵字鏈在以該單元爲頭結點的鏈表的尾部。
b. 開放定址法:即發生衝突時,去尋找下一個空的哈希地址。只要哈希表足夠大,總能找到空的哈希地址。
c. 再哈希法:即發生衝突時,由其他的函數再計算一次哈希值。
d. 建立公共溢出區:將哈希表分爲基本表和溢出表,發生衝突時,將衝突的元素放入溢出表。
HashMap 使用鏈地址法來解決衝突(jdk8中採用平衡樹來替代鏈表存儲衝突的元素,但hash() 方法原理相同)。當兩個對象的hashcode相同時,它們的bucket位置相同,碰撞就會發生。此時,可以將 put 進來的 K- V 對象插入到鏈表的尾部。對於儲存在同一個bucket位置的鏈表對象,可通過鍵對象的equals()方法用來找到鍵值對。
6、死鎖問題
getEntry(Object key)方法中的for 循環容易出現死鎖。
爲什麼呢?因爲當你查找一個key的hash存在的時候,進入了循環,恰恰這個時候,另外一個線程將這個Entry刪除了,那麼你就一直因爲找不到Entry而出現死循環,最後導致的結果就是代碼效率很低,CPU特別高。
二、實踐:Java7的HashMap源碼分析
作者和參考類。
* @author Doug Lea
* @author Josh Bloch // Java集合框架的作者
* @author Arthur van Hoff
* @author Neal Gafter
* @see Object#hashCode() // Object類的hashCode()方法
* @see Collection
* @see Map
* @see TreeMap
* @see Hashtable
* @since 1.2
(一)包路徑
說明:打包的路徑爲util包。
package java.util;
(二)導入包
說明:需要導入io包。
import java.io.*;
(三)接口和實現類
說明:繼承抽象類AbstractMap和實現Map、Cloneable、Serializable接口。
問題:問什麼繼承了抽象類AbstractMap,卻還要實現Map接口?AbstractMap已經實現了Map接口。
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
}
(四)成員變量
// hashmap(即變量table)默認的初始化大小,必須是2的冪。(構造函數中未指定時使用的初始化大小。)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// hashmap(即變量table)默認的最大容量。
static final int MAXIMUM_CAPACITY = 1 << 30;
// hashmap(即變量table)默認的負載因子。(構造函數中未指定時使用的加載因子。)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// TODO 可繼續深入瞭解
// HashMap內部的存儲結構是一個數組,此處數組爲空,即沒有初始化之前的狀態
static final Entry<?,?>[] EMPTY_TABLE = {};
// hashmap的容器
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
// hashmap(即變量table)實際的大小,即對象數量
transient int size;
// hashmap(即變量table)實際的容量臨界閾值,公式爲(threshold = 實際capacity * 實際loadFactor)
// 下一個要調整大小的大小值,hashmap的實際大小達到該容量臨界值,會觸發resize
int threshold;
// hashmap(即變量table)實際的負載因子
final float loadFactor;
// TODO 可繼續深入瞭解
// HashMap的結構被修改或者刪除的次數總數,用於迭代器。
// 用於快速失敗,由於HashMap非線程安全,在對HashMap進行迭代時,如果期間其他線程的參與導致HashMap的結構發生變化了(比如put,remove等操作),需要拋出異常ConcurrentModificationException
transient int modCount;
// TODO 可繼續深入瞭解
// 表示在對字符串鍵(即key爲String類型)的HashMap應用替代哈希函數時HashMap的條目數量的默認閾值。
// 替代哈希函數的使用可以減少由於對字符串鍵進行弱哈希碼計算時的碰撞概率
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
// TODO 可繼續深入瞭解
// 與實例關聯的隨機值,用於哈希代碼中,以減少衝突
transient int hashSeed = 0;
// TODO 可繼續深入瞭解
// 保存所有map的Set容器 可以用來遍歷、查詢HashMap等
private transient Set<Map.Entry<K,V>> entrySet = null;
// 序列化號
private static final long serialVersionUID = 362498820763181265L;
(五)構造函數:HashMap初始化
HashMap有四個構造方法。
1、無參數構造方法
使用默認的初始化大小,默認的加載因子初始化HashMap。此時初始化大小爲16,負載因子爲0.75f。
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
2、一個參數:int
使用入參指定的初始化大小和默認的加載因子初始化HashMap。此時初始化大小爲入參,負載因子爲0.75f。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
3、兩個參數:int,float
使用入參指定的初始化大小和入參指定的加載因子初始化HashMap。此時初始化大小和負載因子都爲入參。
public HashMap(int initialCapacity, float loadFactor) {
// 判斷設置的容量和負載因子是否合法,提高代碼魯棒性。
// 初始大小不能小於0
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 初始大小大於MAXIMUM_CAPACITY,則只能取到MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 負載因子不能小於0,且要求爲數值
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 根據入參設置實際負載因子
this.loadFactor = loadFactor;
// threshold臨界值此時爲初始化大小,後面第一次put時由inflateTable(int toSize)計算設置
threshold = initialCapacity;
// init方法在HashMap中沒有實際實現,不過在其子類如:linkedHashMap中就會有對應實現
init();
}
4、一個參數:Map
使用入參指定的map初始化HashMap。此時初始化大小根據入參map大小指定,負載因子爲默認的0.75f。
舉些例子:
(1)如果入參map的大小爲90,則90/0.75+1 = 121,和默認的初始化大小16比較,取最大值,即初始化大小取121,負載因子取默認的0.75f。
(2)如果入參map的大小爲9,則90/0.75+1 = 13,和默認的初始化大小16比較,取最大值,即初始化大小取默認的16,負載因子取默認的0.75f。
public HashMap(Map<? extends K, ? extends V> m) {
// 調用HashMap(int initialCapacity)構造方法
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
// 思考:爲什麼上面已經調用構造方法創建了hashmap,這裏還要調用該方法inflateTable(threshold)?
// 初始化HashMap底層的數組結構
inflateTable(threshold);
// 調用私有方法,將入參map的元素插入到hash中。該方法不會進行resize。
putAllForCreate(m);
}
(六)散列方法:hash
1、源碼
/**
* Retrieve object hash code and applies a supplemental hash function to the
* result hash, which defends against poor quality hash functions. This is
* critical because HashMap uses power-of-two length hash tables, that
* otherwise encounter collisions for hashCodes that do not differ
* in lower bits. Note: Null keys always map to hash 0, thus index 0.
檢索對象哈希代碼,並對結果哈希應用補充哈希函數,以抵禦質量差的哈希函數。
這一點很關鍵,因爲hashmap使用了兩個長度散列表的冪,否則會遇到在低位沒有差異的hashcode的衝突。
注意:空鍵總是映射到散列0,因此索引0。
*/
final int hash(Object k) {
// 哈希種子一個隨機值,在計算key的哈希碼時會用到這個種子,目的是爲了進一步減少哈希碰撞。
// 如果hashSeed=0表示禁用備用哈希。
int h = hashSeed;
// 字符串和非字符串算哈希值的方法是不一樣的。直接返回stringHash32方法處理的hash值。
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
// 一次hash:k.hashCode()方法,是Object中取hashCode值的方法,
// ^= 表示異或賦值。h ^= k.hashCode()等價於 h = h ^ k.hashCode(),
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
// 二次hash。這裏就是解決Hash衝突的函數
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
2、分析
Hash算法處理步驟如下:
(1)首先判斷key的類型是否爲String,如果是String類型,就單獨調用stringHash32((String) k)處理。
(2)接着對key進行一次hash
(3)然後hashSeed和第(2)步計算出的hash進行一次異或運算
(4)最後對第(3)步的結果再進行二次hash
3、何時使用hash?
在增加、刪除、查找鍵值對的操作中,定位到哈希桶數組的位置都是很關鍵的第一步。
無論是get方法還是put方法,都是先傳入key,然後對key進行兩次hash,首先是調用Object類的hash方法獲得一個hash值a,然後再調用HashMap類hash進行第二次hash獲得hash值b,然後才調用indexFor(hash, table.length)方法計算獲得最終的數組索引。
// 計算二次Hash
int hash = hash(key.hashCode());
// 通過Hash找數組索引
int i = indexFor(hash, table.length);
HashMap爲什麼還要做二次hash呢?
final int hash(Object k) {
......
......
// 二次hash。這裏就是解決Hash衝突的函數。進行移位,防止hash值得低位都是一樣的。
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
Q:回答這個問題之前,我們先來看看HashMap是怎麼通過Hash查找數組的索引的。
A:通過indexFor(int h, int length)方法:
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
其中h是hash值,length是數組的長度,這個h & (length-1)按位與的算法,其實就是h%length求餘。
Q:一般什麼情況下利用該算法?
A:分組。例如怎麼將100個數分組16組中。
既然知道了分組的原理,那我們看看幾個例子,代碼如下:
public class Main {
public static void main(String[] args){
int h=15,length=16;
System.out.println(h & (length-1));
h=15+16;
System.out.println(h & (length-1));
h=15+16+16;
System.out.println(h & (length-1));
h=15+16+16+16;
System.out.println(h & (length-1));
}
}
運行結果都是15,爲什麼呢?
我們換算成二進制來看看。
public class Main {
public static void main(String[] args){
System.out.println(Integer.parseInt("0001111", 2) & Integer.parseInt("0001111", 2));
System.out.println(Integer.parseInt("0011111", 2) & Integer.parseInt("0001111", 2));
System.out.println(Integer.parseInt("0111111", 2) & Integer.parseInt("0001111", 2));
System.out.println(Integer.parseInt("1111111", 2) & Integer.parseInt("0001111", 2));
}
}
這裏你就發現了,在做按位與操作的時候,後面的始終是低位在做計算,高位不參與計算,因爲高位都是0。這樣導致的結果就是只要是低位是一樣的,高位無論是什麼,最後結果是一樣的,如果這樣依賴,hash碰撞始終在一個數組上,導致這個數組開始的鏈表無限長,那麼在查詢的時候就速度很慢,又怎麼算得上高性能的啊。
所以hashmap必須解決這樣的問題,儘量讓key儘可能均勻的分配到數組上去。避免造成Hash堆積。
回到正題,HashMap爲了減少hash碰撞衝突,所以進行了二次hash。
4、String的Hash算法
/**
* Returns a hash code for this string. The hash code for a
* <code>String</code> object is computed as
* <blockquote><pre>
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
* </pre></blockquote>
* using <code>int</code> arithmetic, where <code>s[i]</code> is the
* <i>i</i>th character of the string, <code>n</code> is the length of
* the string, and <code>^</code> indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return a hash code value for this object.
*/
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
它的算法等式就是s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1],其中s[i]就是索引爲i的字符,n爲字符串的長度。這裏爲什麼有一個固定常量31呢,關於這個31的討論很多,基本就是優化的數字,主要參考Joshua Bloch’s Effective Java的引用如下:
The value 31 was chosen because it is an odd prime. If it were even and the multiplication overflowed, information would be lost, as multiplication by 2 is equivalent to shifting. The advantage of using a prime is less clear, but it is traditional. A nice property of 31 is that the multiplication can be replaced by a shift and a subtraction for better performance: 31 * i == (i << 5) - i. Modern VMs do this sort of optimization automatically.
大體意思是說選擇31是因爲它是一個奇素數,如果它做乘法溢出的時候,信息會丟失,而且當和2做乘法的時候相當於移位,在使用它的時候優點還是不清楚,但是它已經成爲了傳統的選擇,31的一個很好的特性就是做乘法的時候可以被移位和減法代替的時候有更好的性能體現。例如31i相當於是i左移5位減去i,即31i == (i<<5)-i。現代的虛擬內存系統都使用這種自動優化。
5、String的hash32算法
/**
* TODO 可深入研究
* Calculates a 32-bit hash value for this string.
*
* @return a 32-bit hash value for this string.
*/
int hash32() {
int h = hash32;
if (0 == h) {
// harmless data race on hash32 here.
h = sun.misc.Hashing.murmur3_32(HASHING_SEED, value, 0, value.length);
// ensure result is not zero to avoid recalcing
h = (0 != h) ? h : 1;
hash32 = h;
}
return h;
}
(七)擴容方法:resize
1、源碼
/**
* 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.
*
* If current capacity is MAXIMUM_CAPACITY, this method does not
* resize the map, but sets threshold to Integer.MAX_VALUE.
* This has the effect of preventing future calls.
*
* @param newCapacity the new capacity, MUST be a power of two;
* must be greater than current capacity unless current
* capacity is MAXIMUM_CAPACITY (in which case value
* is irrelevant). // 傳入新的容量
*/
void resize(int newCapacity) {
// 引用擴容前的Entry數組
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
// 擴容前的數組大小如果已經達到最大(2^30)了
if (oldCapacity == MAXIMUM_CAPACITY) {
// 修改閾值爲int的最大值(2^31-1)
threshold = Integer.MAX_VALUE;
// 不擴容,直接返回
return;
}
// 初始化一個新的Entry數組
Entry[] newTable = new Entry[newCapacity];
// 將數據轉移到新的Entry數組裏
transfer(newTable, initHashSeedAsNeeded(newCapacity));
// HashMap的table屬性引用新的Entry數組
table = newTable;
// 修改閾值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
2、分析
(1)如果新的容量大小newCapacity超過最大容量就返回,不進行擴容。
(2)否則就new 一個新的Entry數組,長度爲舊的Entry數組長度的兩倍。
(3)然後將舊的Entry[]複製到新的Entry[]。在複製的時候,數組的索引重新參與計算,關鍵代碼爲:int i = indexFor(e.hash, newCapacity);
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
// 遍歷舊的Entry數組
for (Entry<K,V> e : table) {
while(null != e) {
// 暫存當前節點e的下一個節點
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
// 重新計算每個元素在數組中的位置。重點
int i = indexFor(e.hash, newCapacity);
// 標記[1]
e.next = newTable[i];
// 將元素放在數組上
newTable[i] = e;
// 訪問下一個Entry鏈上的元素
e = next;
}
}
}
(4)HashMap的table屬性引用新的Entry數組
(5)修改閾值threshold
(八)put方法
1、源碼
public V put(K key, V value) {
// 如果table爲{},則先調用inflateTable(threshold)處理。
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 如果是null,就單獨調用putForNullKey(value)處理。調用後return返回。
if (key == null)
return putForNullKey(value);
// 對key的hashcode進一步計算,確保散列均勻
int hash = hash(key);
// 獲取在table中的存放位置
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 如果該數據已存在,執行覆蓋操作。更新value,並返回舊value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
// 更新value
e.value = value;
e.recordAccess(this);
// 並返回舊value
return oldValue;
}
}
modCount++;
// 如果該數據不存在,執行添加操作。更新value,並返回null
addEntry(hash, key, value, i);
return null;
}
2、分析
put方法共8個步驟,分別如下:
(1)首先判斷table是否爲{},如果是,則先調用inflateTable(threshold)處理。
/**
* Inflates the table.
這個方法只是在第一次對數組進行操作的時候,需要對數組進行增加來存儲元素,
因爲table什麼元素都沒有,就調用該方法。
*/
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
// 返回一個大於等於 最接近toSize的2的冪數。
// 2的3次方的冪數就是8.2的冪數就是每次都市2的幾次方,2的冪數有可能是1,2,4,8,16等。
// 比如toSize=16,則16的2的冪數就是2的4次方還是16,
// 比如toSize=17,那麼最接近他的2的冪數就是2的5次方,也就是32.
int capacity = roundUpToPowerOf2(toSize);
// 設置下一個要調整大小的大小值,hashmap的實際大小達到該容量臨界值,會觸發resize
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 將table數組大小改變爲capacity。
table = new Entry[capacity];
// 重新計算賦值給hashSeed,返回值是boolean。
initHashSeedAsNeeded(capacity);
}
/**
* Initialize the hashing mask value. We defer initialization until we
* really need it.
*/
final boolean initHashSeedAsNeeded(int capacity) {
// 當我們初始化的時候hashSeed爲0,0!=0,所以這時爲false.
boolean currentAltHashing = hashSeed != 0;
// isBooted()這個方法裏面返回了一個boolean值
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
sun.misc.VM類的代碼如下:
private static volatile boolean booted = false;
public static boolean isBooted() {
return booted;
}
這裏返回的是booted 的值,但是booted 默認爲false,但是我們不知道VM啓動的時候是否對它又賦了新值,怎麼辦呢?
詳見:https://blog.csdn.net/qq_30447037/article/details/78985216
(2)接着判斷key是否爲null,如果是null,就單獨調用putForNullKey(value)處理。
/**
* Offloaded version of put for null keys
遍歷table[0]的鏈表的每個節點Entry,如果發現其中存在節點Entry的key爲null,
就替換新的value,然後返回舊的value,如果沒發現key等於null的節點Entry,就增加新的節點。
也就是說,如果key爲null的值,默認就存儲到table[0]開頭的鏈表了。
*/
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;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
(3)計算key的hashcode,再用計算的結果二次hash,
(4)通過indexFor(hash, table.length);找到Entry數組的索引i。
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
(5)然後遍歷以table[i]爲頭節點的鏈表,如果發現有節點的hash,key都相同的節點時,就替換爲新的value,然後返回舊的value。
(6)modCount累加1。原因是因爲HashMap不是線程安全的,但在某些容錯能力較好的應用中,如果你不想僅僅因爲1%的可能性而去承受hashTable的同步開銷,HashMap使用了Fail-Fast機制來處理這個問題,你會發現modCount在源碼中是這樣聲明的。
transient volatile int modCount;
volatile關鍵字聲明瞭modCount,代表了多線程環境下訪問modCount,根據JVM規範,只要modCount改變了,其他線程將讀到最新的值。其實在Hashmap中modCount只是在迭代的時候起到關鍵作用。
private abstract class HashIterator<E> implements Iterator<E> {
Entry<K,V> next; // next entry to return
int expectedModCount; // For fast-fail
int index; // current slot
Entry<K,V> current; // current entry
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
}
public final boolean hasNext() {
return next != null;
}
final Entry<K,V> nextEntry() {
// 關鍵代碼
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
current = e;
return e;
}
public void remove() {
if (current == null)
throw new IllegalStateException();
// 關鍵代碼
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Object k = current.key;
current = null;
HashMap.this.removeEntryForKey(k);
// 關鍵代碼
expectedModCount = modCount;
}
}
使用Iterator開始迭代時,會將modCount的賦值給expectedModCount,在迭代過程中,通過每次比較兩者是否相等來判斷HashMap是否在內部或被其它線程修改,如果modCount和expectedModCount值不一樣,證明有其他線程在修改HashMap的結構,會拋出異常。
所以HashMap的put、remove等操作都有modCount++的計算。
(7)如果沒有找到key的hash相同的節點,就增加新的節點addEntry()。代碼如下:
/**
* Adds a new entry with the specified key, value and hash code to
* the specified bucket. It is the responsibility of this
* method to resize the table if appropriate.
*
* Subclass overrides this to alter the behavior of put method.
*/
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);
}
(8)如果HashMap大小超過臨界值,就要重新設置大小,擴容。
(九)get方法
1、源碼
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
public V get(Object key) {
// key爲空
if (key == null)
return getForNullKey();
// key不爲空
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
2、分析
get方法步驟如下:
(1)首先判斷key是否爲null,如果是null,就單獨調用getForNullKey()處理。
/**
* Offloaded version of get() to look up null keys. Null keys map
* to index 0. This null case is split out into separate methods
* for the sake of performance in the two most commonly used
* operations (get and put), but incorporated with conditionals in
* others.
*/
private V getForNullKey() {
if (size == 0) {
return null;
}
// 當key爲null時,只找鏈表的第一個節點(table[0])及其上面的鏈節點?
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
(2)如果不爲null,則調用getEntry(key)方法進行處理
/**
* Returns the entry associated with the specified key in the
* HashMap. Returns null if the HashMap contains no mapping
* for the key.
*/
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
// key經過hash算法計算得到對應的索引位置
int hash = (key == null) ? 0 : hash(key);
// 遍歷該位置的鏈表,通過equal方法比對,若空則返回空,若比對成功則返回對應數值
// 這裏容易出現死循環
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;
}
這段代碼帶來的問題是巨大的。因爲HashMap是非線程安全的,所以這裏的循環會導致死循環的。
爲什麼呢?因爲當你查找一個key的hash存在的時候,進入了循環,恰恰這個時候,另外一個線程將這個Entry刪除了,那麼你就一直因爲找不到Entry而出現死循環,最後導致的結果就是代碼效率很低,CPU特別高。
(十)size方法
1、源碼
/**
* Returns the number of key-value mappings in this map.
*
* @return the number of key-value mappings in this map
*/
public int size() {
return size;
}
2、分析
HashMap的大小很簡單,不是實時計算的,而是每次新增加Entry的時候,size就遞增;刪除的時候就遞減。
因爲HashMap設計的意圖不是線程安全的,使用的場景也不需要考慮線程安全的(如只在方法中使用,不在成員變量中使用),所以可以這樣獲取size,效率高。是一種空間換時間的做法。
(十一)其它方法
1、源碼
//
private static int roundUpToPowerOf2(int number) {}
private void inflateTable(int toSize) {}
void init() {}
final boolean initHashSeedAsNeeded(int capacity){}
// 取模運算
static int indexFor(int h, int length) {}
// public方法:獲取hash的大小
public int size() {}
// public方法:判斷是否爲空
public boolean isEmpty() {}
// public方法:根據key獲取節點
public V get(Object key) {}
private V getForNullKey() {}
// public方法:是否存在入參的key
public boolean containsKey(Object key) {}
final Entry<K,V> getEntry(Object key) {}
// public方法:put方法,設值
public V put(K key, V value) {}
private V putForNullKey(V value) {}
private void putForCreate(K key, V value) {}
private void putAllForCreate(Map<? extends K, ? extends V> m) {}
void transfer(Entry[] newTable, boolean rehash) {}
// public方法:設值
public void putAll(Map<? extends K, ? extends V> m) {}
// public方法:移除
public V remove(Object key) {}
final Entry<K,V> removeEntryForKey(Object key) {}
final Entry<K,V> removeMapping(Object o) {}
// public方法:清空
public void clear() {}
// public方法:是否存在入參的value
public boolean containsValue(Object value) {}
private boolean containsNullValue() {}
// public方法:克隆
public Object clone() {}
void addEntry(int hash, K key, V value, int bucketIndex) {}
void createEntry(int hash, K key, V value, int bucketIndex) {}
Iterator<K> newKeyIterator() {}
Iterator<V> newValueIterator() {}
Iterator<Map.Entry<K,V>> newEntryIterator() {}
// public方法:通過迭代器獲取value的集合
public Collection<V> values() {}
// public方法:通過迭代器獲取key的集合
public Set<K> keySet() {}
// public方法:通過迭代器獲取節點的集合
public Set<Map.Entry<K,V>> entrySet() {}
private Set<Map.Entry<K,V>> entrySet0() {}
private void writeObject(java.io.ObjectOutputStream s) throws IOException{}
private void readObject(java.io.ObjectInputStream s){}
int capacity() {}
float loadFactor() {}
2、分析
(十二)內部類
三大集合與迭代
1、源碼
private static class Holder {}
// HashMap存儲對象的實際實體,由Key,value,hash,next組成。
static class Entry<K,V> implements Map.Entry<K,V> {}
private abstract class HashIterator<E> implements Iterator<E> {}
private final class ValueIterator extends HashIterator<V> {}
private final class KeyIterator extends HashIterator<K> {}
private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {}
private final class KeySet extends AbstractSet<K> {}
private final class Values extends AbstractCollection<V> {}
private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {}
Entry
static class Entry<K,V> implements Map.Entry<K,V> {
// 鍵
final K key;
// 值
V value;
// 指向下一個Entry
Entry<K,V> next;
// hash值
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
/**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already
* in the HashMap.
*/
void recordAccess(HashMap<K,V> m) {
}
/**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(HashMap<K,V> m) {
}
}
2、分析
三、實踐:Java8的HashMap源碼分析
說明:作者和參考類。和java7一樣。
* @author Doug Lea
* @author Josh Bloch
* @author Arthur van Hoff
* @author Neal Gafter
* @see Object#hashCode()
* @see Collection
* @see Map
* @see TreeMap
* @see Hashtable
* @since 1.2
(一)包路徑
說明:打包的路徑爲util包。和java7一樣。
package java.util;
(二)導入包
說明:Java7只導入了io包,Java8會導入其它包。
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import sun.misc.SharedSecrets;
(三)接口和實現類
說明:繼承抽象類AbstractMap和實現Map、Cloneable、Serializable接口。和java7一樣。
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {}
(四)成員變量
private static final long serialVersionUID = 362498820763181265L;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 桶的樹化閾值:即 鏈表轉成紅黑樹的閾值,在存儲數據時,當鏈表長度 > 該值時,則將鏈表轉換成紅黑樹
static final int TREEIFY_THRESHOLD = 8;
// 桶的鏈表還原閾值:即 紅黑樹轉爲鏈表的閾值,當在擴容(resize())時(此時HashMap的數據存儲位置會重新計算),在重新計算存儲位置後,當原有的紅黑樹內數量 < 6時,則將 紅黑樹轉換成鏈表
static final int UNTREEIFY_THRESHOLD = 6;
// 最小樹形化容量閾值:即 當哈希表中的容量 > 該值時,才允許樹形化鏈表 (即 將鏈表 轉換成紅黑樹)
// 否則,若桶內元素太多時,則直接擴容,而不是樹形化
// 爲了避免進行擴容、樹形化選擇的衝突,這個值不能小於 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;
// hashmap的容器
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
transient int modCount;
int threshold;
final float loadFactor;
(五)構造函數
說明:先從構造函數說起,HashMap有四個構造方法,
1、無參數構造方法
和java7一樣。
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
2、一個參數:int
和java7一樣。
/**
* 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);
}
3、兩個參數:int,float
Java8中的HashMap的初始化,不同於Java7中的構造方法,Java8對於數組table的初始化,並沒有直接放在構造器中完成,而是將table數組的構造延遲到了resize中完成。
/**
* 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;
// 這裏和java7不同
this.threshold = tableSizeFor(initialCapacity);
}
如上所示,首先對傳入參數進行基本的檢測,其中需注意,當需要構造的容量大於HashMap所支持的最大容量時,容量會被置爲MAXIMUM_CAPACITY,同時對裝載因子進行初始化(裝載因子決定數組何時進行擴容)。如上所示,不同於Java7,在構造函數中並沒有對table數組進行初始化,具體的構造,將後在面的resize函數解析中做具體的分析。
4、一個參數:Map
/**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 這裏和java7不同
putMapEntries(m, false);
}
(六)散列方法:hash
1、源碼
/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
// 第一步:h = key.hashCode() // 取hashCode值
// 第二步:h ^ (h >>> 16) // 對hashCode進行16位的無符號右移。能夠把高位的變化影響到了低位的變化,大大減少了hash衝突。
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2、分析
(七)擴容方法:resize
1、源碼
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) {
// 若數組長度已經達到HashMap所設計的最大長度,則不能進行擴容,只將閥值設置爲最大
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 檢測將數組長度擴大一倍後是否在HashMap所設計的最大長度範圍內
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;
// 將HashMap進行重置
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;
}
2、分析
整個HashMap中,最爲精髓和核心的函數即爲resize函數了,通過上文的分析可知,很多函數都將依賴put操作的執行,同時很多HashMap的設計,也都集中在resize函數當中。對於resize函數的分析,將着重的從兩點並行出發,首先會分析resize函數的代碼邏輯,其次會對上文提出的問題進行回答。
首先來看看在Java8中,擴容的具體邏輯是怎樣的。我們在上文中提出了一個問題,即HashMap的數組大小爲什麼必須爲2的n次冪,我們對其作出了一個層次的回答,而其第二個層次的回答就在resize函數的執行邏輯中。在Java8的擴容中,不是簡單的將原數組中的每一個元素取出進行重新hash映射,而是做移位檢測。所謂移位檢測的含義具體是針對HashMap做映射時的&運算所提出的,通過上文對&元算的分析可知,映射的本質即看hash值的某一位是0還是1,當擴容以後,會相比於原數組多出一位做比較,由多出來的這一位是0還是1來決定是否進行移位,而具體的移位距離,也是可知的,及位原數組的大小,我們來看下錶的分析,假定原表大小爲16:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-rHaqWMG7-1573367712076)(1572681207396.png)]
由上表可知,是否移位,由擴容後表示的最高位是否爲所決定,並且移動的方向只有一個,即向高位移動。因此,可以根據對最高位進行檢測的結果來決定是否移位,從而可以優化性能,不用每一個元素都進行移位。
代碼中最核心的部分即爲對擴容後的數組的重建,不同於Java7中數組的構建,Java8採用了尾插法,保證了新構建的數組與原數組在碰撞後的元素組織次序的一致。同時還需要注意的是,HashMap不通過線程安全的支持,因此在數組擴容的過程中會造成死循環,具體出現死循環的情況可參見相關博客。
(八)put方法
1、源碼
public V put(K key, V value) {
// hash(key) : 對key的hashCode()做hash
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 暫存當前的散列表
Node<K,V>[] tab;
//
Node<K,V> p;
int n;
int i;
// 如果當前的散列表,即tab爲空
if ((tab = table) == null || (n = tab.length) == 0)
// TODO 爲什麼要這樣寫?
n = (tab = resize()).length;
// 如果i位置的節點爲空,則插入新元素
// i = (n - 1) & hash // 計算index,並對null做處理
if ((p = tab[i = (n - 1) & hash]) == null)
// 根據入參哈希值、鍵和值,構造新節點,並在i的位置插入
tab[i] = newNode(hash, key, value, null);
// 如果 tab[i]不爲空,需要依次判斷:是否第一個值相等、是否爲紅黑樹、是否爲鏈表
else {
//
Node<K,V> e;
// 暫存tab[i]的key
K k;
// 如果tab[i]不爲空,且hash值等於入參哈希值、或者key的引用或者值等於入參key
// 即判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 暫存tab[i],即直接覆蓋value
e = p;
// 如果tab[i]是紅黑樹的節點,即判斷該鏈爲紅黑樹
else if (p instanceof TreeNode)
// TODO
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);
// 如果桶中元素個數超過這個值(8-1=7)時,即鏈表長度大於8轉換爲紅黑樹進行處理
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 轉爲紅黑樹
treeifyBin(tab, hash);
break;
}
// key已經存在直接覆蓋value
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果該數據已存在,執行覆蓋操作。更新value,並返回舊value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 超過threshold 就擴容
if (++size > threshold)
resize();
// HashMap中該方法爲空,只有LinkedHashMap實現這個方法
afterNodeInsertion(evict);
return null;
}
2、分析
put函數的實現,具體依賴於putNode方法的實現。put鍵值對的方法,過程如下:
①.判斷鍵值對數組table[i]是否爲空或爲null,否則執行resize()進行擴容;
②.根據鍵值key計算hash值得到插入的數組索引i,如果table[i]==null,直接新建節點添加,轉向⑥,如果table[i]不爲空,轉向③;
③.判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value,否則轉向④,這裏的相同指的是hashCode以及equals;
④.判斷table[i] 是否爲treeNode,即table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉向⑤;
⑤.遍歷table[i],判斷鏈表長度是否大於8,大於8的話把鏈表轉換爲紅黑樹,在紅黑樹中執行插入操作,否則進行鏈表的插入操作;遍歷過程中若發現key已經存在直接覆蓋value即可;
⑥.插入成功後,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,如果超過,進行擴容。
如上圖所示,爲整個put操作的邏輯,其中需要注意以下幾點:
put操作會進行進行判空的檢測,如果當前的散列表爲空時,則進行resize擴容
當put的key存在時,會分別在數組頭、紅黑樹或者鏈表中進行節點匹配,匹配成功後進行value值的更新
(九)get方法
1、源碼
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
// 變量說明:
// tab用於存儲table引用,
// first用於存儲數組中第一個節點,
// e爲目標節點
Node<K,V>[] tab;
Node<K,V> first, e;
int n; K k;
// 首先確保當前HashMap不爲空,同時通過映射後Key所對應的數組位置節點不爲空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 當前if框架內,是當前位置發生hash碰撞後的節點,框架內是對節點的匹配
// hash值相等的情況下,進行key值的匹配
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
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、分析
通過傳入的key,返回其對應的value,在Java8的源碼中可以看到,該函數的執行是依賴於getNode函數的,具體代碼及註釋分析過程如下:
1、指定key 通過hash函數得到key的hash值:int hash=key.hashCode();
2、調用內部方法 getNode(),得到桶號(一般爲hash值對桶數求模)
int index =hash%Entry[].length;
jdk1.6版本後使用位運算替代模運算,int index=hash&( Entry[].length - 1);
3、比較桶的內部元素是否與key相等,若都不相等,則沒有找到。相等,則取出相等記錄的value。
4、如果得到 key 所在的桶的頭結點恰好是紅黑樹節點,就調用紅黑樹節點的 getTreeNode() 方法,否則就遍歷鏈表節點。getTreeNode 方法使通過調用樹形節點的 find()方法進行查找。由於之前添加時已經保證這個樹是有序的,因此查找時基本就是折半查找,效率很高。
5、如果對比節點的哈希值和要查找的哈希值相等,就會判斷 key 是否相等,相等就直接返回;不相等就從子樹中遞歸查找。
代碼中除了對基礎邏輯的分析以外,還需要注意的是,進行key匹配的時候,同時需要hash值相等和key的值相等纔算匹配成功,
因此這裏進一步的證明了需要要求hashcode與equal保持一致(即可回答一個經典的面試問題:當hashcode進行重寫了也必須重寫equals方法,反之亦然)。
同時可以看到,Java8中引入的紅黑樹,在進行節點匹配時,需要進行節點的類型檢測,在鏈表和紅黑樹之間調用不同的檢測接口進行匹配。
另一方面還需要注意,對於值匹配的判斷中,只要引用相等或值相等的其中之一成立即成立。
(十)其它方法
1、源碼
static Class<?> comparableClassFor(Object x) {
static int compareComparables(Class<?> kc, Object k, Object x) {
static final int tableSizeFor(int cap) {
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
public int size() {
public boolean isEmpty() {
public V get(Object key) {
final Node<K,V> getNode(int hash, Object key) {
public boolean containsKey(Object key) {
public V put(K key, V value) {
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
public void putAll(Map<? extends K, ? extends V> m) {
public V remove(Object key) {
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
public void clear() {
public boolean containsValue(Object value) {
public Set<K> keySet() {
public Collection<V> values() {
public Set<Map.Entry<K,V>> entrySet() {
@Override
public V getOrDefault(Object key, V defaultValue) {
@Override
public V putIfAbsent(K key, V value) {
@Override
public boolean remove(Object key, Object value) {
@Override
public boolean replace(K key, V oldValue, V newValue) {
@Override
public V replace(K key, V value) {
@Override
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
public V computeIfPresent(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
@Override
public V compute(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
@Override
public V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
@Override
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
@SuppressWarnings("unchecked")
@Override
public Object clone() {
final float loadFactor() {
final int capacity() {
private void writeObject(java.io.ObjectOutputStream s) throws IOException {
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
void reinitialize() {
2、分析
(十一)內部類
1、源碼
static class Node<K,V> implements Map.Entry<K,V> {}
final class KeySet extends AbstractSet<K> {}
final class Values extends AbstractCollection<V> {}
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {}
abstract class HashIterator {}
final class KeyIterator extends HashIterator implements Iterator<K> {}
final class ValueIterator extends HashIterator implements Iterator<V> {}
final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>> {}
static class HashMapSpliterator<K,V> {}
static final class KeySpliterator<K,V>
extends HashMapSpliterator<K,V>
implements Spliterator<K> {}
static final class ValueSpliterator<K,V>
extends HashMapSpliterator<K,V>
implements Spliterator<V> {}
static final class EntrySpliterator<K,V>
extends HashMapSpliterator<K,V>
implements Spliterator<Map.Entry<K,V>> {}
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {}
2、分析
(十二)樹化方法
1、源碼
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
* tab:元素數組,
* hash:hash值(要增加的鍵值對的key的hash值)
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
/*
* 如果元素數組爲空 或者 數組長度小於 樹結構化的最小限制
* MIN_TREEIFY_CAPACITY 默認值64,對於這個值可以理解爲:如果元素數組長度小於這個值,沒有必要去進行結構轉換
* 當一個數組位置上集中了多個鍵值對,那是因爲這些key的hash值和數組長度取模之後結果相同。(並不是因爲這些key的hash值相同)
* 因爲hash值相同的概率不高,所以可以通過擴容的方式,來使得最終這些key的hash值在和新的數組長度取模之後,拆分到多個數組位置上。
*/
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// 擴容
resize();
// 如果元素數組長度已經大於等於了 MIN_TREEIFY_CAPACITY,那麼就有必要進行結構轉換了
// 根據hash值和數組長度進行取模運算後,得到鏈表的首節點
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 定義首、尾節點
TreeNode<K,V> hd = null, tl = null;
// 遍歷鏈表
do {
// 將該節點轉換爲 樹節點
TreeNode<K,V> p = replacementTreeNode(e, null);
// 如果尾節點爲空,說明還沒有根節點
if (tl == null)
// 首節點(根節點)指向 當前節點
hd = p;
// 尾節點不爲空,以下兩行是一個雙向鏈表結構
else {
// 當前樹節點的 前一個節點指向 尾節點
p.prev = tl;
// 尾節點的 後一個節點指向 當前節點
tl.next = p;
}
// 把當前節點設爲尾節點
tl = p;
} while ((e = e.next) != null);
// 到目前爲止 也只是把Node對象轉換成了TreeNode對象,把單向鏈表轉換成了雙向鏈表
// 把轉換後的雙向鏈表,替換原來位置上的單向鏈表
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
2、分析
把容器裏的元素變成樹結構。當HashMap的內部元素數組中某個位置上存在多個hash值相同的鍵值對,這些Node已經形成了一個鏈表,當該鏈表的長度大於等於8的時候,會調用該方法來進行一個特殊處理。
(十三)LinkedHashMap支持的方法
1、源碼
// Create a regular (non-tree) node
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
// For conversion from TreeNodes to plain nodes
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
return new Node<>(p.hash, p.key, p.value, next);
}
// Create a tree bin node
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
return new TreeNode<>(hash, key, value, next);
}
// For treeifyBin
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
/**
* Reset to initial default state. Called by clone and readObject.
*/
void reinitialize() {
table = null;
entrySet = null;
keySet = null;
values = null;
modCount = 0;
threshold = 0;
size = 0;
}
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
// Called only from writeObject, to ensure compatible ordering.
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
Node<K,V>[] tab;
if (size > 0 && (tab = table) != null) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
s.writeObject(e.key);
s.writeObject(e.value);
}
}
}
}
2、分析
四、總結
1、HashMap工作原理:數組+鏈表+紅黑樹的數據結構、初始化、put方法、get方法、resize方法、hash方法。
2、hash衝突問題:hash值一致。解決方案:鏈地址法、開放定址法、再哈希法、建立公共溢出區。
3、性能問題:resize擴容時非常耗性能,所以在初始化HashMap的時候給一個大致的數值,可以避免頻繁擴容。
4、線程問題:HashMap線程不安全,多線程環境中容易產生死循環。可用ConcurrentHashMap代替。
5、Java8新特性:引入紅黑樹,很大程度優化了HashMap的性能。
五、參考
java集合之----HashMap源碼分析(基於JDK1.7與1.8)
HashMap看這篇就夠了~
一文讀懂HashMap
一文搞定HashMap的實現原理和麪試
Java基礎系列之(三) - HashMap深度分析
Java8 HashMap源碼分析
談談HashMap的hash()方法巧妙之處
談談HashCode的作用