在之前的項目需要用到以自定義類型作為HashMap
的key,遇到一個問題:如果修改了已經存儲在HashMap
中的實例,會發生什么情況呢?用一段代碼來試驗:
import java.util.HashMap;
import java.util.Map;
public class TestHashMap {
public static void main(String[] args) {
testObjAsKey();
}
private static void testObjAsKey() {
class Person {
public String familyName;
public String givenName;
public Person(String familyName, String givenName) {
this.familyName = familyName;
this.givenName = givenName;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((familyName == null) ? 0 : familyName.hashCode());
result = prime * result
+ ((givenName == null) ? 0 : givenName.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof Person)) {
return false;
}
Person other = (Person) obj;
if (familyName == null) {
if (other.familyName != null) {
return false;
}
} else if (!familyName.equals(other.familyName)) {
return false;
}
if (givenName == null) {
if (other.givenName != null) {
return false;
}
} else if (!givenName.equals(other.givenName)) {
return false;
}
return true;
}
@Override
public String toString() {
return "Person(" + familyName + ", " + givenName + ")";
}
}
Map<Person, Integer> map = new HashMap<Person, Integer>();
Person person1 = new Person("zhang", "san");
map.put(person1, 1);
System.out.println("Value of " + person1 + " is " + map.get(person1));
person1.givenName = "si";
System.out.println("'zhang san' is changed to 'zhang si'");
System.out.println("Value of 'zhang san' is " + map.get(new Person("zhang", "san")));
System.out.println("Value of 'zhang si' is " + map.get(new Person("zhang", "si")));
System.out.println("Value of `person1` is " + map.get(person1));
}
}
程序的輸出是什么?答案見下
Value of Person(zhang, san) is 1
'zhang san' is changed to 'zhang si'
Value of 'zhang san' is null
Value of 'zhang si' is null
Value of `person1` is null
為什么這樣呢?這要從HashMap
的實現進行分析。HashMap
使用一個Entry
數組保存內部的元素(Entry
是用來保存<key, value="">對的類型)。數組的每個slot保存一個鏈表的頭指針,這個鏈表內的元素都是hashCode相同的entry
Entry[] tables
+---+
| 0 | -> entry_0_0 -> entry_0_1 -> null
+---+
| 1 | -> null
+---+
| |
...
|n-1| -> entry_n-1_0 -> null
+---+
HashMap
的put()
和get()
方法基本原理如下: - put一個元素的時候,根據key的hashCode()
方法計算出hash值,進而算出相應的數組下標,然后將這個新的entry加入到鏈表中。 - get一個key的時候,根據key的hash值找到相應的數組下標,然后遍歷這個鏈表,並查找和當前key相等的entry。判斷兩個元素是否相等時使用使用equals()
方法
上面的例子中,在put
操作之后,map
內部的存儲是這樣的(假設這個元素被存儲在了第i個slot):
| | -> null
+---+
| i | -> entry<person1("zhang", "san"), 1> -> null
+---+
| | -> null
注意,當我們修改了person1
的時候,並沒有修改它存儲在map
中的位置。也就是說,修改之后的map
的內部存儲是這樣的:
| | -> null
+---+
| i | -> entry<person1("zhang", "si"), 1> -> null
+---+
| | -> null
person1
仍然存儲在第i個slot里。
當get(new Person("zhang", "si"))
的時候,HashMap
將先根據hash值計算它應該位於第幾個slot,比如是j。由於根據Person("zhang", "san")
計算出來的下標是i,i和j很可能是不相同的,那么第j個slot是空的(因為之前只put
了一個元素且位於第i個slot),因此get()
方法將返回null
。
當get(new Person("zhang", "san"))
的時候,HashMap
將計算它應該位於第i個slot,然后在這個鏈表中查找。這個鏈表中存儲了一個元素,但是當前的key是("zhang", "si")
,使用equals()
方法判斷這兩個key是否相等時將返回false
,也就是說,HashMap
在第i個slot所維護的鏈表中沒有找到和當前key相等的元素,因此get()
方法將返回null
。
當get(person1)
的時候,因為person1
現在的值是("zhang", "si")
,所以和get(new Person("zhang", "si"))
的情況是完全一樣的。
總結一下,上面的分析說明,如果不小心改變了已經存儲在HashMap
中的key值,那么將引起潛在的錯誤。顯然,避免這個問題比較好的方法是,如果打算將自己定義的一種數據類型作為key,那么將這個類型設計成不可變的(immutable)。比如Integer
,String
等都是不可變的。