问题描述
一道来自Java
官方twitter
的问题。风格很像目前国内各大互联网公司的笔试题。
public class MapEqualsChallenge {
public static void main(String[] args) {
Map map = new LinkedHashMap<>();
map.put(new Stark("Arya"), "1");
map.put(new Stark("Ned"), "2");
map.put(new Stark("Sansa"), "3");
map.put(new Stark("Bran"), "4");
map.put(new Stark("Jaime"), "5");
map.forEach((key, value) -> System.out.println(value));
}
static class Stark {
String name;
public Stark(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
return ((Stark) obj).name.length() == this.name.length();
}
@Override
public int hashCode() {
return 4000 << 2 * 2000 / 10000;
}
}
}
挺有意义的一道题,绝对是学习equals
和hashcode
的经典案例,为官方社区点个赞。
分析
Java 集合
这是一个有关Java
集合的关系图,最初觉得这个好复杂,但是现在再去看看,其实也很简单。
Collection
和Map
接口都是我们常用的,这个图有一点问题,在JDK
的源码中,Map
和Collection
毫无关系。
List
:强调数据在集合中的位置。
Set
:强调集合中元素不允许重复。
Map
:强调集合中的元素根据key
来查询。
如果你不明白三者的区别与联系,请看《Head First Java 第二版》557页,书中用图示对三者进行了详细的阐述。
LinkedHashMap
之前研究过HashMap
和ConcurrentHashMap
(线程安全的HashMap
),这个LinkedHashMap
倒是第一次见。
LinkedHashMap
继承自HashMap
,二者区别不大。
存放数据,根据key
计算哈希值,然后根据哈希值计算该元素应当存储在数组中的位置,如果hash
冲突,并且不是一个对象,则用链表处理。
HashMap
实现的是单向链表,LinkedHashMap
实现的是双向链表。
hashCode
这是源码中计算哈希值的方法,先取对象的哈希值,然后再进行相应的处理。
所以再去看put
过程:key
是一个对象,然后LinkedHashMap
会调用key
的hashCode
方法。
map.put(new Stark("Arya"), "1");
我们可能一看这个hashCode
方法就吓蒙了,这怎么算啊?其实我们完全没必要去计算这个表达式,这是个常量表达式,所以不管new
出来多少个对象,哈希值都是一样的。再去调用LinkedHashMap
的hash
方法,返回值也是一样。
@Override
public int hashCode() {
return 4000 << 2 * 2000 / 10000;
}
先不去管到底是双向链表还是单向链表,反正哈希值相同,这些个键值对肯定在一个链表上。
map.put(new Stark("Arya"), "1");
执行第一行代码,插入key
为Arya
对象,value
为1
的节点对象。
equals
map.put(new Stark("Ned"), "2");
放入第二个元素,再计算哈希值,发现与第一个节点key
的哈希值相同。
如果两个对象的哈希值相等,则这两个对象有可能相等;如果两个对象的哈希值不相等,那这两个对象肯定不相等。
哈希值相等,然后用equals
方法判断两个对象是否相等。
@Override
public boolean equals(Object obj) {
return ((Stark) obj).name.length() == this.name.length();
}
重写过的equals
是根据对象名字的长度来判等的,如果两个对象名字长度相等,那就认为是同一对象。
Arya
与Ned
长度不相等,所以LinkedHashMap
认为这是两个对象,再去新建一个Node
,指过去。
注意:这里有一个插入的位置问题,到底是在链表头部插新节点还是在尾部插新节点?据说面试官还考过?
答案是:在Java8
之前,是在头部插入节点;在Java8
中,是在尾部插入节点。
文章链接:HashMap到底是插入链表头部还是尾部
map.put(new Stark("Sansa"), "3");
又来一个,哈希值相等,名字长度为5
,没有一样的key
,再插一个新节点。
覆盖
map.put(new Stark("Bran"), "4");
又来一个,哈希值相等,长度为4
,与Arya 1
是相等的,直接将原节点的值覆盖。由1
修改为4
。
map.put(new Stark("Jaime"), "5");
第五个,哈希值相等,长度为5
,与Sansa 3
相等,覆盖,由3
修改为5
。
结果
分析了一大堆,切换到IDE
中运行,与预期结果一致。
总结
学然后知不足,教然后知困!
之前觉得自己对HashMap
还是听明白的,但当自己去画各种示意图时,发现还有很多细节去需要学习。