一道面试题看 HashMap 的存储方式
我们公司招人喜欢问算法题和一些基础知识。今天我们一个面试官在面试候选人之前在办公室对我们说他准备问一个这样的问题:
在HashMap 中存放的一系列键值对,其中键为某个我们自定义的类型。放入 HashMap 后,我们在外部把某一个 key 的属性进行更改,然后我们再用这个 key 从 HashMap 里取出元素,这时候 HashMap 会返回什么?
我们办公室几个人答案都不一致,有的说返回null,有的说能正常返回value。但不论答案是什么都没有确凿的理由。我觉得这个问题挺有意思的,就写了代码测试。结果是返回null。需要说明的是我们自定义的类重写了 hashCode 方法。我想这个结果还是有点意外的,因为我们知道 HashMap 存放的是引用类型,我们在外面把 key 更新了,那也就是说 HashMap 里面的 key 也更新了,也就是这个 key 的 hashCode 返回值也会发生变化。这个时候 key 的 hashCode 和 HashMap 对于元素的 hashCode 肯定一样,equals也肯定返回true,因为本来就是同一个对象,那为什么不能返回正确的值呢?
先来看看一段测试代码:
先解释一下测试代码做到事。定义了一个person类,就两个属性。重写了 hashCode 方法,还有一套geter和seter,没什么特别。测试类里面先创建了三个person对象作为 key 。打印各个 key 的 hashCode 值。然后三个元素放到 HashMap ,接着更新其中一个 key 的name属性,最后去取这个 key 的value。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
public
class
Person {
private
String name;
private
int
height;
@Override
public
int
hashCode() {
System.out.println(
this
.name +
": HashCode() invoked!"
);
return
this
.name.hashCode() +
this
.height;
}
public
String getName() {
return
name;
}
public
void
setName(String name) {
this
.name = name;
}
public
int
getHeight() {
return
height;
}
public
void
setHeight(
int
height) {
this
.height = height;
}
@Override
public
String toString() {
return
"Name:"
+
this
.name +
"; height:"
+
this
.height;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
import
java.util.HashMap;
import
java.util.Map;
import
java.util.Map.Entry;
public
class
HashmapTest {
public
static
void
main(String[] args) {
Map<Person, String> testMap =
new
HashMap<Person, String>();
Person p1 =
new
Person();
p1.setName(
"Jakie"
);
p1.setHeight(
165
);
Person p2 =
new
Person();
p2.setName(
"Jerry"
);
p2.setHeight(
175
);
Person p3 =
new
Person();
p3.setName(
"Torres"
);
p3.setHeight(
160
);
System.out.println(p1 +
";hashcode:"
+ p1.hashCode() +
"\n"
);
System.out.println(p2 +
";hashcode:"
+ p2.hashCode() +
"\n"
);
System.out.println(p3 +
";hashcode:"
+ p3.hashCode() +
"\n"
);
System.out.println(
"************************"
);
System.out.println(
"putting object into map"
);
testMap.put(p1,
"p1"
);
testMap.put(p2,
"p2"
);
testMap.put(p3,
"p3"
);
System.out.println(
"************************"
);
p2.setName(
"Jerry is now kelly"
);
System.out.println(
"P2 hashcode after update:"
);
System.out.println(p2 +
";hashcode:"
+ p2.hashCode() +
"\n"
);
System.out.println(
"**************************"
);
System.out.println(
"Hash Code of elements in HashMap"
);
for
(Entry<Person, String> entry : testMap.entrySet()) {
System.out.println(entry.getKey() +
":"
+ entry.getValue() +
":"
+ entry.getKey().hashCode());
System.out.println();
if
(entry.getKey().getName().equals(
"Jakie"
)) {
System.out.println(
"Jakie in map is the original jakie "
+ (entry.getKey() == p1));
}
else
if
(entry.getKey().getName().equals(
"Jerry is now kelly"
)) {
System.out
.println(
"Jerry is now kelly in map is the original Jerry "
+ (entry.getKey() == p2));
}
}
System.out.println(
"**********************"
);
String p = testMap.get(p2);
System.out.println(
"Final Result:"
+ p);
}
}
|
输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
Name:Jakie; height:
165
;hashcode:
71336629
Name:Jerry; height:
175
;hashcode:
71462829
Name:Torres; height:
160
;hashcode:-
1784098647
************************
putting object into map
************************
P2 hashcode after update:
Name:Jerry is now kelly; height:
175
;hashcode:-
711681872
**************************
Hash Code of elements in HashMap
Name:Jerry is now kelly; height:
175
:p2:-
711681872
Jerry is now kelly in map is the original Jerry
true
Name:Jakie; height:
165
:p1:
71336629
Jakie in map is the original jakie
true
Name:Torres; height:
160
:p3:-
1784098647
**********************
Final Result:
null
|
从输出我们可以知道, key 更新后 hashCode 确实更新了。而且 HashMap 里面的对象就是我们原来的对象。最后的结果是null。
我们来看一下 HashMap 的get方法源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public
V get(Object key) {
if
(key ==
null
)
return
getForNullKey();
int
hash = hash(key.hashCode());
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.equals(k)))
return
e.value;
}
return
null
;
}
|
可以看到先取得了一个table,这个table实际上是个数组。然后在table里面找对应 key 的value。找的标准就是hash等于传入参数的hash, 并且满足另外两个条件之一:k = e.key,也就是说他们是同一个对象,或者传入的 key 的equal目标的 key 。我们的问题出在那个hash(key.hashCode()),可以看到 HashMap 在存储元素时是把 key 的 hashCode 再做了一次hash。得到的hash将最终作为元素存储位置的依据。对应到我们的情况:第一次存储时,hash函数采用key.hashCode作为参数得到了一个值,然后根据这个值把元素存到了某个位置。
当我们再去取元素的时候,key.hashCode的值已经出现了变化,所以这里的hash函数结果也发生了变化,所以当它尝试去获得这个 key 的存储位置时就不能得到正确的值,导致最终找不到目标元素。要想能正确返回,很简单,把Person类的 hashCode 方法改一下,让它的 hashCode 不依赖我们要修改的属性,但实际开发中肯定不能这么干,我们总是希望当两个对象的属性不完全相同时能返回不同的 hashCode 值。所以结论就是当把对象放到 HashMap 后,不要去修改 key 的属性。
以上都是很基础的东西,但或许我们很多时候都没注意到,了解这些基础可以避免一些很诡异的bug。纯属抛砖引玉,如有谬误请海涵和指出。
本文作者: 伯乐在线 - 梧桐