参考资料
ref 1-阿里《Java开发手册》,「集合处理」章节
ref 2-《Effective Java》,第3章节,「第11条 覆盖equals时总要覆盖hashcode」
ref 3-为什么重写equals必须重写hashCode | Segmentfault
前言
根据阿里《Java开发手册》,对 Java 对象的 hashCode
和 equals
方法,有如下强制约定。
[强制] 关于 hashCode
和 equals
的处理,遵循如下规则
1)只要覆写 equals,就必须覆写 hashCode。
2)因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须覆写这两个方法。
3)如果自定义对象作为 Map 的键,那么必须覆写 hashCode 和 equals。
说明:String 已经覆写 hashCode 和 equals 方法,所以我们可以愉快地使用 String 对象作为 key 来使用。
下面进行必要的补充分析。
equals保证可靠性,hashCode保证性能
equals
保证可靠性,hashCode
保证性能。
equals
和 hashCode
都可用来判断两个对象是否相等,但是二者有区别
equals
可以保证比较对象是否是绝对相等,即「equals
保证可靠性」
hashCode
用来在最快的时间内判断两个对象是否相等,可能有「误判」,即「hashCode
保证性能」
两个对象 equals
为 true 时,要求 hashCode
也必须相等
两个对象 hashCode
为 true 时,equals
可以不等(如发生哈希碰撞时)
hashCode
的「误判」指的是
同一个对象的 hashCode
一定相等。
不同对象的 hashCode
也可能相等,这是因为 hashCode
是根据地址 hash
出来的一个 int 32
位的整型数字,相等是在所难免。
此处以向 HashMap 中插入数据(调用 put
方法,put
方法会调用内部的 putVal
方法)为例,对「equals
保证可靠性,hashCode
保证性能」这句话加以说明,putVal
方法中,判断两个 Key 是否相同的代码如下所示。
// putVal 方法if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))...
在判断两个 Key 是否相同时,
先比较 hash
(通过 hashCode
的高 16 位和低 16 位进行异或运算得出)。这可以在最快的时间内判断两个对象是否相等,保证性能。
但是不同对象的 hashCode
也可能相等。所以对满足 p.hash == hash
的条件,需要进一步判断。
继续,比较两个对象的地址是否相同,==
判断是否绝对相等,equals
判断是否客观相等。
自定义对象作为Set元素时
ref 1-自定义对象作为Map的键或Set元素,需要重写equals和hashCode方法 | CSDN
class Dog { String color; public Dog(String s) { color = s; }}public class SetAndHashCode { public static void main(String[] args) { HashSet<Dog> dogSet = new HashSet<Dog>(); dogSet.add(new Dog("white")); dogSet.add(new Dog("white")); System.out.println("We have " + dogSet.size() + " white dogs!"); if (dogSet.contains(new Dog("white"))) { System.out.println("We have a white dog!"); } else { System.out.println("No white dog!"); } }}
运行程序,输出结果如下。
We have 2 white dogs!No white dog!
根据阿里《Java开发手册》可知,「因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须覆写这两个方法」。将 Dog
代码修改为如下。
class Dog { String color; public Dog(String s) { color = s; } //重写equals方法, 最佳实践就是如下这种判断顺序: public boolean equals(Object obj) { if (!(obj instanceof Dog)) return false; if (obj == this) return true; return this.color == ((Dog) obj).color; } public int hashCode() { return color.length();//简单原则 }}
此时,再运行程序,输出结果如下。
We have 1 white dogs!We have a white dog!
自定义对象作为Map的键和内存溢出
如下代码,自定义 KeylessEntry
对象,作为 Map 的键。
class KeylessEntry { static class Key { Integer id; Key(Integer id) { this.id = id; } @Override public int hashCode() { return id.hashCode(); } } public static void main(String[] args) { Map m = new HashMap(); while (true){ for (int i = 0; i < 10000; i++){ if (!m.containsKey(new Key(i))){ m.put(new Key(i), "Number:" + i); } } System.out.println("m.size()=" + m.size()); } }}
上述代码中,使用 containsKey(keyElement)
判断 Map 是否已经包含 keyElement
键值。containsKey
的关键代码如下所示,使用了 hashCode
和 equals
方法进行判断。
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))...
执行上述代码,因没有重写 equals
方法,导致 m.containsKey(new Key(i))
判断总是 false,导致程序不断向 Map 中插入新的 key-value
,造成死循环,最终将导致内存溢出。