hashMap發(fā)生死鎖的問題圃泡,在以下文章中:
1.7hashmap發(fā)生死鎖颇蜡,線程非安全
看懂這篇需要參考以下內(nèi)容:
一风秤、HashMap簡介
哈希表(hash table)也叫散列表,是一種非常重要的數(shù)據(jù)結(jié)構(gòu)彻磁,應用場景及其豐富,許多緩存技術(shù)(比如memcached)的核心其實就是在內(nèi)存中維護一張大的哈希表尘喝,而HashMap的實現(xiàn)原理就是基于此朽褪。那么什么是哈希表呢?
在討論哈希表之前骑科,我們先大概了解下其他數(shù)據(jù)結(jié)構(gòu)在新增咆爽,查找等基礎(chǔ)操作執(zhí)行性能
數(shù)組:采用一段連續(xù)的存儲單元來存儲數(shù)據(jù)斗埂。對于指定下標的查找男娄,時間復雜度為O(1)模闲;通過給定值進行查找,需要遍歷數(shù)組实夹,逐一比對給定關(guān)鍵字和數(shù)組元素亮航,時間復雜度為O(n),當然宴猾,對于有序數(shù)組仇哆,則可采用二分查找油讯,插值查找陌兑,斐波那契查找等方式,可將查找復雜度提高為O(logn)狞玛;對于一般的插入刪除操作锭亏,涉及到數(shù)組元素的移動慧瘤,其平均復雜度也為O(n)。對應到集合實現(xiàn)怔匣,代表就是ArrayList。
線性鏈表:對于鏈表的新增永部,刪除等操作(在找到指定操作位置后),僅需處理結(jié)點間的引用即可组橄,時間復雜度為O(1)羽资,而查找操作需要遍歷鏈表逐一進行比對屠升,復雜度為O(n)腹暖。對應的集合類是LinkedList脏答。
二叉樹:對一棵相對平衡的有序二叉樹,對其進行插入亩鬼,查找殖告,刪除等操作,平均復雜度均為O(logn)辛孵。對應的集合類有TreeSet和TreeMap丛肮。
哈希表:相比上述幾種數(shù)據(jù)結(jié)構(gòu),在哈希表中進行添加魄缚,刪除宝与,查找等操作嚼隘,性能十分之高卧檐,不考慮哈希沖突的情況下榜跌,僅需一次定位即可完成笼吟,時間復雜度為O(1)撵枢。對應的集合類就是HashMap佛吓。
哈希表的主干就是數(shù)組逸贾。我們要新增或查找某個元素,我們通過把當前元素的關(guān)鍵字 通過某個函數(shù)映射到數(shù)組中的某個位置,通過數(shù)組下標一次定位就可完成操作崔慧。即:
存儲位置 = F(關(guān)鍵字)
其中悼泌,這個函數(shù)f一般稱為哈希函數(shù)复斥,這個函數(shù)的設(shè)計好壞會直接影響到哈希表的優(yōu)劣。這會涉及到哈希沖突臭埋。
當我們對某個元素進行哈希運算累贤,得到一個存儲地址,然后要進行插入的時候医清,發(fā)現(xiàn)已經(jīng)被其他元素占用了,其實這就是所謂的哈希沖突贫导,也叫哈希碰撞欢顷。前面我們提到過墩莫,哈希函數(shù)的設(shè)計至關(guān)重要椭更,好的哈希函數(shù)會盡可能地保證計算簡單和散列地址分布均勻。但是痛侍,我們需要清楚的是誓竿,數(shù)組是一塊連續(xù)的固定長度的內(nèi)存空間燎潮,再好的哈希函數(shù)也不能保證得到的存儲地址絕對不發(fā)生沖突纠拔。那么哈希沖突如何解決呢?哈希沖突的解決方案有多種:開放定址法(發(fā)生沖突嘱蛋,繼續(xù)尋找下一塊未被占用的存儲地址)蚯姆、再散列函數(shù)法、鏈地址法洒敏。而HashMap即是采用了鏈地址法龄恋,也就是數(shù)組+鏈表的方式。
簡單來說凶伙,HashMap由數(shù)組+鏈表組成的郭毕,數(shù)組是HashMap的主體,鏈表則是主要為了解決哈希沖突而存在的函荣,如果定位到的數(shù)組位置不含鏈表(當前entry的next指向null),那么對于查找显押,添加等操作很快扳肛,僅需一次尋址即可;如果定位到的數(shù)組包含鏈表乘碑,對于添加操作挖息,其時間復雜度依然為O(1),因為最新的Entry會插入鏈表頭部兽肤,急需要簡單改變引用鏈即可套腹,而對于查找操作來講,此時就需要遍歷鏈表资铡,然后通過key對象的equals方法逐一比對查找电禀。所以,性能考慮害驹,HashMap中的鏈表出現(xiàn)越少,性能才會越好蛤育。
二宛官、HashMap的源碼實現(xiàn)
1、存儲結(jié)構(gòu)
HashMap的內(nèi)部存儲結(jié)構(gòu)其實是數(shù)組和鏈表的結(jié)合瓦糕。當實例化一個HashMap時底洗,系統(tǒng)會創(chuàng)建一個長度為Capacity的Entry數(shù)組,這個長度被稱為容量(Capacity)咕娄,在這個數(shù)組中可以存放元素的位置我們稱之為“桶”(bucket)亥揖,每個bucket都有自己的索引,系統(tǒng)可以根據(jù)索引快速的查找bucket中的元素圣勒。 每個bucket中存儲一個元素费变,即一個Entry對象,但每一個Entry對象可以帶一個引用變量圣贸,用于指向下一個元素挚歧,因此,在一個桶中吁峻,就有可能生成一個Entry鏈滑负。 Entry是HashMap的基本組成單元,每一個Entry包含一個key-value鍵值對用含。 Entry是HashMap中的一個靜態(tài)內(nèi)部類矮慕。代碼如下:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結(jié)構(gòu)
int hash;//對key的hashcode值進行hash運算后得到的值啄骇,存儲在Entry痴鳄,避免重復計算
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
經(jīng)過以上分析,HashMap的存儲結(jié)構(gòu)圖如下:
一個長度為16的數(shù)組中缸夹,每個元素存儲的是一個鏈表的頭結(jié)點夏跷。那么這些元素是按照什么樣的規(guī)則存儲到數(shù)組中呢哼转。一般情況是通過hash(key)%len獲得,也就是元素的key的哈希值對數(shù)組長度取模得到槽华。比如上述哈希表中壹蔓,12%16=12,28%16=12,108%16=12,140%16=12。所以12猫态、28佣蓉、108以及140都存儲在數(shù)組下標為12的位置。
在存儲一對值時(Key—->Value對)亲雪,實際上是存儲在一個Entry的對象e中勇凭,程序通過key計算出Entry對象的存儲位置。換句話說义辕,Key—->Value的對應關(guān)系是通過key—-Entry—-value這個過程實現(xiàn)的虾标,所以就有我們表面上知道的key存在哪里,value就存在哪里灌砖。
2璧函、構(gòu)造方法
先看HashMap中的幾個重要屬性:
//默認初始化化容量,即16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量,即2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默認裝載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//HashMap內(nèi)部的存儲結(jié)構(gòu)是一個數(shù)組基显,此處數(shù)組為空蘸吓,即沒有初始化之前的狀態(tài)
static final Entry<?,?>[] EMPTY_TABLE = {};
//空的存儲實體
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//實際存儲的key-value鍵值對的個數(shù)
transient int size;
//閾值,當table == {}時撩幽,該值為初始容量(初始容量默認為16)库继;當table被填充了,也就是為table分配內(nèi)存空間后窜醉,threshold一般為 capacity*loadFactory宪萄。HashMap在進行擴容時需要參考threshold
int threshold;
//負載因子,代表了table的填充度有多少榨惰,默認是0.75
final float loadFactor;
//用于快速失敗雨膨,由于HashMap非線程安全,在對HashMap進行迭代時读串,如果期間其他線程的參與導致HashMap的結(jié)構(gòu)發(fā)生變化了(比如put聊记,remove等操作),需要拋出異常ConcurrentModificationException
transient int modCount;
//默認的threshold值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
HashMap有4個構(gòu)造器恢暖,其他構(gòu)造器如果用戶沒有傳入initialCapacity 和loadFactor這兩個參數(shù)排监,會使用默認值。initialCapacity默認為16杰捂,loadFactory默認為0.75舆床。
//計算Hash值時的key
transient int hashSeed = 0;
//通過初始容量和狀態(tài)因子構(gòu)造HashMap
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)//參數(shù)有效性檢查
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)//參數(shù)有效性檢查
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))//參數(shù)有效性檢查
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();//init方法在HashMap中沒有實際實現(xiàn),不過在其子類如 linkedHashMap中就會有對應實現(xiàn)
}
//通過擴容因子構(gòu)造HashMap,容量去默認值,即16
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//裝載因子取0.75挨队,容量取16谷暮,構(gòu)造HashMap
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//通過其他Map來初始化HashMap,容量通過其他Map的size來計算,裝載因子取0.75
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
inflateTable(threshold);//初始化HashMap底層的數(shù)組結(jié)構(gòu)
putAllForCreate(m);//添加m中的元素
}
從上面這段代碼我們可以看出盛垦,在常規(guī)構(gòu)造器中湿弦,并沒有馬上為數(shù)組table分配內(nèi)存空間(有一個入?yún)橹付∕ap的構(gòu)造器例外),事實上是在執(zhí)行第一次put操作的時候才真正構(gòu)建table數(shù)組腾夯。
3颊埃、put操作
如果兩個key通過hash%Entry[].length得到的index相同,會不會有覆蓋的危險蝶俱?為了解決這個問題班利,HashMap里面用到鏈式數(shù)據(jù)結(jié)構(gòu)的一個概念。上面我們提到過Entry類里面有一個next屬性榨呆,作用是指向下一個Entry罗标。打個比方, 第一個鍵值對A進來积蜻,通過計算其key的hash得到的index=0闯割,記做:Entry[0] = A。一會后又進來一個鍵值對B浅侨,通過計算其index也等于0纽谒,現(xiàn)在怎么辦证膨?HashMap會這樣做:B.next = A,Entry[0] = B,如果又進來C,index也等于0,那么C.next = B,Entry[0] = C如输;這樣我們發(fā)現(xiàn)index=0的地方其實存取了A,B,C三個鍵值對,他們通過next這個屬性鏈接在一起。所以疑問不用擔心央勒。也就是說數(shù)組中存儲的是最后插入的元素不见。到這里為止,HashMap的大致實現(xiàn)崔步,我們應該已經(jīng)清楚了稳吮。
public V put(K key, V value) {
//如果table數(shù)組為空數(shù)組{},進行數(shù)組填充(為table分配實際內(nèi)存空間)井濒,入?yún)閠hreshold灶似,此時threshold為initialCapacity 默認是1<<4(=16)
if (table == EMPTY_TABLE) {
inflateTable(threshold);//分配數(shù)組空間
}
//如果key為null,存儲位置為table[0]或table[0]的沖突鏈上
if (key == null)
return putForNullKey(value);
int hash = hash(key);//對key的hashcode進一步計算瑞你,確保散列均勻
int i = indexFor(hash, table.length);//獲取在table中的實際位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果該對應數(shù)據(jù)已存在酪惭,執(zhí)行覆蓋操作。用新value替換舊value者甲,并返回舊value
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);//調(diào)用value的回調(diào)函數(shù)春感,其實這個函數(shù)也為空實現(xiàn)
return oldValue;
}
}
modCount++;//保證并發(fā)訪問時,若HashMap內(nèi)部結(jié)構(gòu)發(fā)生變化,快速響應失敗
addEntry(hash, key, value, i);//新增一個entry
return null;
}
inflateTable的源碼如下:
private void inflateTable(int toSize) {
int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次冪
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//此處為threshold賦值鲫懒,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值嫩实,capaticy一定不會超過MAXIMUM_CAPACITY,除非loadFactor大于1
table = new Entry[capacity];//分配空間
initHashSeedAsNeeded(capacity);//選擇合適的Hash因子
}
inflateTable這個方法用于為主干數(shù)組table在內(nèi)存中分配存儲空間窥岩,通過roundUpToPowerOf2(toSize)可以確保capacity為大于或等于toSize的最接近toSize的二次冪甲献,比如toSize=13,則capacity=16;to_size=16谦秧,capacity=16竟纳;to_size=17,capacity=32。其實現(xiàn)如下:
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
roundUpToPowerOf2中的這段處理使得數(shù)組長度一定為2的次冪疚鲤,Integer.highestOneBit是用來獲取最左邊的bit(其他bit位為0)所代表的數(shù)值锥累。
在對數(shù)組進行空間分配后,會根據(jù)hash函數(shù)計算散列值集歇,其實現(xiàn)如下:
//用了很多的異或桶略,移位等運算,對key的hashcode進一步進行計算以及二進制位的調(diào)整等來保證最終獲取的存儲位置盡量分布均勻
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {//這里針對String優(yōu)化了Hash函數(shù)诲宇,是否使用新的Hash函數(shù)和Hash因子有關(guān)
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
從上面的操作看以看出际歼,影響HashMap元素的存儲位置的只有key的值,與value值無關(guān)姑蓝。
通過hash函數(shù)得到散列值后鹅心,再通過indexFor進一步處理來獲取實際的存儲位置,其實現(xiàn)如下:
//返回數(shù)組下標
static int indexFor(int h, int length) {
return h & (length-1);
}
h&(length-1)保證獲取的index一定在數(shù)組范圍內(nèi)纺荧,舉個例子旭愧,默認容量16,length-1=15宙暇,h=18,轉(zhuǎn)換成二進制計算為
這里寫圖片描述
最終計算出的index=2输枯。有些版本的對于此處的計算會使用 取模運算,也能保證index一定在數(shù)組范圍內(nèi)占贫,不過位運算對計算機來說桃熄,性能更高一些(HashMap中有大量位運算)。
通過以上分析型奥,我們看到瞳收,要得到一個元素的存儲位置,需要如下幾步:
1厢汹、獲取該元素的key值
2螟深、通過hash方法得到key的散列值,這其中需要用到key的hashcode值坑匠。
3血崭、通過indexFor計算得到存儲的下標位置。
最后,得到存儲的下標位置后夹纫,我們就可以將元素放入HashMap中咽瓷,具體通過addEntry實現(xiàn):
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);//當size超過臨界閾值threshold,并且即將發(fā)生哈希沖突時進行擴容舰讹,新容量為舊容量的2倍
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);//擴容后重新計算插入的位置下標
}
//把元素放入HashMap的桶的對應位置
createEntry(hash, key, value, bucketIndex);
}
//創(chuàng)建元素
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex]; //獲取待插入位置元素
table[bucketIndex] = new Entry<>(hash, key, value, e);//這里執(zhí)行鏈接操作茅姜,使得新插入的元素指向原有元素。
//這保證了新插入的元素總是在鏈表的頭
size++;//元素個數(shù)+1
}
通過以上代碼能夠得知月匣,當發(fā)生哈希沖突并且size大于閾值的時候钻洒,需要進行數(shù)組擴容,擴容時锄开,需要新建一個長度為之前數(shù)組2倍的新的數(shù)組素标,然后將當前的Entry數(shù)組中的元素全部傳輸過去,擴容后的新數(shù)組長度為之前的2倍萍悴,所以擴容相對來說是個耗資源的操作头遭。
4、擴容操作
擴容操作通過resize操作實現(xiàn):
//按新的容量擴容Hash表
void resize(int newCapacity) {
Entry[] oldTable = table;//老的數(shù)據(jù)
int oldCapacity = oldTable.length;//獲取老的容量值
if (oldCapacity == MAXIMUM_CAPACITY) {//老的容量值已經(jīng)到了最大容量值
threshold = Integer.MAX_VALUE;//修改擴容閥值
return;
}
//新的結(jié)構(gòu)
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));//將老的表中的數(shù)據(jù)拷貝到新的結(jié)構(gòu)中
table = newTable;//修改HashMap的底層數(shù)組
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//修改閥值
}
如果數(shù)組進行擴容癣诱,數(shù)組長度發(fā)生變化计维,而存儲位置 index = h&(length-1),index也可能會發(fā)生變化,需要重新計算index撕予,我們先來看看transfer這個方法:
//將老的表中的數(shù)據(jù)拷貝到新的結(jié)構(gòu)中
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;//容量
for (Entry<K,V> e : table) { //遍歷所有桶
while(null != e) { //遍歷桶中所有元素(是一個鏈表)
Entry<K,V> next = e.next;
if (rehash) {//如果是重新Hash鲫惶,則需要重新計算hash值
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);//定位Hash桶
e.next = newTable[i];//元素連接到桶中,這里相當于單鏈表的插入,總是插入在最前面
newTable[i] = e;//newTable[i]的值總是最新插入的值
e = next;//繼續(xù)下一個元素
}
}
}
這個方法將老數(shù)組中的數(shù)據(jù)逐個鏈表地遍歷实抡,重新計算后放入新的擴容后的數(shù)組中欠母,我們的數(shù)組索引位置的計算是通過 對key值的hashcode進行hash擾亂運算后,再通過和 length-1進行位運算得到最終數(shù)組索引位置澜术。
注意:HashMap數(shù)組元素長度的設(shè)計
通過源碼可以發(fā)現(xiàn)艺蝴,hashMap的數(shù)組長度一定保持2的次冪猬腰,這樣做有什么好處呢鸟废?
//根據(jù)Hash值和Hash表的大小選擇合適的Hash桶
static int indexFor(int h, int length) {
return h & (length-1);
}
如果length為2的次冪,其二進制表示就是100….0000姑荷;則length-1 轉(zhuǎn)化為二進制必定是0111….11的形式盒延,在于h的二進制與操作效率會非常的快,而且空間不浪費鼠冕;如果length不是2的次冪添寺,比如length為15,則length-1為14懈费,對應的二進制為1110计露,再于h與操作,
最后一位都為0,所以0001票罐,0011叉趣,0101,1001该押,1011疗杉,0111,1101這幾個位置永遠都不會存放元素了蚕礼,空間浪費相當大烟具,更糟的是這種情況中,數(shù)組可以使用的位置比數(shù)組長度小了很多奠蹬,這意味著進一步增加了碰撞的幾率朝聋,減慢了查詢的效率!這樣就會造成空間的浪費囤躁。
5玖翅、get操作
//獲取key值為key的元素值
public V get(Object key) {
if (key == null)//如果Key值為空,則獲取對應的值割以,這里也可以看到金度,HashMap允許null的key,其內(nèi)部針對null的key有特殊的邏輯
return getForNullKey();
Entry<K,V> entry = getEntry(key);//獲取實體
return null == entry ? null : entry.getValue();//判斷是否為空严沥,不為空秘症,則獲取對應的值
}
//獲取key為null的實體
private V getForNullKey() {
if (size == 0) {//如果元素個數(shù)為0,則直接返回null
return null;
}
//key為null的元素存儲在table的第0個位置
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)//判斷是否為null
return e.value;//返回其值
}
return null;
}
get方法通過key值返回對應value剪勿,如果key為null撞芍,直接去table[0]處檢索。我們再看一下getEntry這個方法:
//獲取鍵值為key的元素
final Entry<K,V> getEntry(Object key) {
if (size == 0) {//元素個數(shù)為0
return null;//直接返回null
}
int hash = (key == null) ? 0 : hash(key);//獲取key的Hash值
for (Entry<K,V> e = table[indexFor(hash, table.length)];//根據(jù)key和表的長度翩瓜,定位到Hash桶
e != null;
e = e.next) {//進行遍歷
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//判斷Hash值和對應的key受扳,合適則返回值
return e;
}
return null;
}
可以看出,get方法的實現(xiàn)相對簡單兔跌,key(hashcode)–>hash–>indexFor–>最終索引位置勘高,找到對應位置table[i],再查看是否有鏈表坟桅,遍歷鏈表华望,通過key的equals方法比對查找對應的記錄。要注意的是仅乓,有人覺得上面在定位到數(shù)組位置之后然后遍歷鏈表的時候赖舟,e.hash == hash這個判斷沒必要,僅通過equals判斷就可以夸楣。其實不然宾抓,試想一下子漩,如果傳入的key對象重寫了equals方法卻沒有重寫hashCode,而恰巧此對象定位到這個數(shù)組位置石洗,如果僅僅用equals判斷可能是相等的痛单,但其hashCode和當前對象不一致,這種情況劲腿,根據(jù)Object的hashCode的約定旭绒,不能返回當前對象,而應該返回null焦人。
下面來舉一個例子看一下:
package com.kang.test;
import java.util.HashMap;
public class Test {
private static class Person {
int ID;
String name;
public Person(int idCard, String name) {
this.ID = 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;
// 兩個對象是否等值挥吵,通過ID來確定
return this.ID == person.ID;
}
}
public static void main(String[] args) {
HashMap<Person, String> map = new HashMap<>();
Person person = new Person(1234, "kang");
map.put(person, "25歲");
// get取出,從邏輯上講應該能輸出“25歲”
System.out.println("結(jié)果:" + map.get(new Person(1234, "kang")));//注意這里是new一個對象
}
}
程序運行結(jié)果是:結(jié)果:null
為什么會出現(xiàn)這樣的結(jié)果呢花椭?我們只重寫了Person的equal方法忽匈,盡管我們在進行g(shù)et和put操作的時候,使用的key從邏輯上講是等值的(通過equals比較是相等的)矿辽,但由于沒有重寫hashCode方法丹允,而我們在進行g(shù)et操作時使用的是map.get(new Person(1234, "kang"))),注意是new操作新建了一個對象袋倔。因為沒有重寫hashcode方法雕蔽,所以兩者的hashcode是不同的。所以put操作時得到的hashcode1和get操作時得到的hashcode2是不同的宾娜。由于hashcode1不等于hashcode2批狐,導致沒有定位到一個數(shù)組位置而返回邏輯上錯誤的值null(也有可能碰巧定位到一個數(shù)組位置,但是也會判斷其entry的hash值是否相等前塔,上面get方法中有提到嚣艇。)。
解決方法很簡單华弓,只需要重寫hashcode方法即可食零。如下:
@Override
public int hashCode() {
return this.ID;//這里為了簡單起見,直接將ID值作為hashcode的值
}
}
如此一來寂屏,輸出結(jié)果便是正確的贰谣。所以,在重寫equals的方法的時候凑保,必須注意重寫hashCode方法冈爹,同時還要保證通過equals判斷相等的兩個對象涌攻,調(diào)用hashCode方法要返回同樣的整數(shù)值欧引。而如果equals判斷不相等的兩個對象,其hashCode可以相同(只不過會發(fā)生哈希沖突恳谎,應盡量避免)芝此。
三憋肖、HashMap的使用總結(jié)
HashMap是基于哈希表的 Map 接口的實現(xiàn)。此實現(xiàn)提供所有可選的映射操作婚苹,并允許使用 null 值和 null 鍵岸更。(除了非同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同膊升。)此類不保證映射的順序怎炊,特別是它不保證該順序恒久不變(發(fā)生擴容時,元素位置會重新分配)廓译。
迭代 collection 視圖所需的時間與 HashMap 實例的“容量”(桶的數(shù)量)及其大衅浪痢(鍵-值映射關(guān)系數(shù))成比例。所以非区,如果迭代性能很重要瓜挽,則不要將初始容量設(shè)置得太高(或?qū)⒓虞d因子設(shè)置得太低)。
HashMap 的實例有兩個參數(shù)影響其性能:初始容量 和加載因子征绸。容量 是哈希表中桶的數(shù)量久橙,初始容量只是哈希表在創(chuàng)建時的容量。加載因子 是哈希表在其容量自動增加之前可以達到多滿的一種尺度管怠。當哈希表中的條目數(shù)超出了加載因子與當前容量的乘積時淆衷,則要對該哈希表進行 rehash 操作(即重建內(nèi)部數(shù)據(jù)結(jié)構(gòu)),從而哈希表將具有大約兩倍的桶數(shù)渤弛。通常吭敢,默認加載因子 (.75) 在時間和空間成本上尋求一種折衷。加載因子過高雖然減少了空間開銷暮芭,但同時也增加了查詢成本(在大多數(shù) HashMap 類的操作中鹿驼,包括 get 和 put 操作,都反映了這一點)辕宏。在設(shè)置初始容量時應該考慮到映射中所需的條目數(shù)及其加載因子畜晰,以便最大限度地減少 rehash 操作次數(shù)。如果初始容量大于最大條目數(shù)除以加載因子瑞筐,則不會發(fā)生 rehash 操作凄鼻。
如果很多映射關(guān)系要存儲在 HashMap 實例中,則相對于按需執(zhí)行自動的 rehash 操作以增大表的容量來說聚假,使用足夠大的初始容量創(chuàng)建它將使得映射關(guān)系能更有效地存儲块蚌。
HashMap的實現(xiàn)不是同步的。如果在多線程操作下膘格,應該使用 Collections.synchronizedMap 方法來“包裝”該映射峭范。最好在創(chuàng)建時完成這一操作,以防止對映射進行意外的非同步訪問瘪贱,如下所示:
Map m = Collections.synchronizedMap(new HashMap(…));
由所有此類的“collection 視圖方法”所返回的迭代器都是快速失敗 的:在迭代器創(chuàng)建之后纱控,如果從結(jié)構(gòu)上對映射進行修改辆毡,除非通過迭代器本身的 remove 方法,其他任何時間任何方式的修改甜害,迭代器都將拋出 ConcurrentModificationException