Effective Java:Ch3_Methods:Item9_重写equals时总要重写hashCode (一)

2014-11-24 11:27:37 · 作者: · 浏览: 11

一个常见的bug原因是没有覆盖hashCode方法。在每个覆盖了equals的类中,都必须覆盖hashCode。如果不这样,则会导致违反Object.hashCode()的通用约定,导致在与所有基于哈希码的集合无法一起正常工作,包括HashMap、HashSet、Hashtable。

如下是Object规范中的通用约定:

在程序的一次执行中只要equals方法所用到的信息没有改变,则多次调用同一个对象的hashCode(),都能始终返回相同的整数。在同一程序的多次执行中,每次的返回结果可以不同。
如果equals方法判定两个对象相同,则这两个对象的hashCode必须产生相同的整数。
如果equals方法判定两个对象不同,则这两个对象的hashCode可以不用;然而,程序员必须要清楚,给不相等的对象生成不同的hashCode可以提供哈希表的性能。

关键的约定是第二条:相等的对象必须有相同的hashCode。如果未覆盖hashCode,则两个相等对象返回的哈希码是Object.hashCode()产生的两个随机数。

【例】下面这个PhoneNumber类,其equals方法是根据Item8的诀窍构造出来的:


[java]
public final class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(int areaCode, int prefix, int lineNumber) {
rangeCheck(areaCode, 999, "area code");
rangeCheck(prefix, 999, "prefix");
rangeCheck(lineNumber, 9999, "line number");
this.areaCode = (short) areaCode;
this.prefix = (short) prefix;
this.lineNumber = (short) lineNumber;
}
private static void rangeCheck(int arg, int max, String name) {
if (arg < 0 || arg > max)
throw new IllegalArgumentException(name +": " + arg);
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNumber == lineNumber
&& pn.prefix == prefix
&& pn.areaCode == areaCode;
}
// Broken - no hashCode method!
... // Remainder omitted
}

public final class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(int areaCode, int prefix, int lineNumber) {
rangeCheck(areaCode, 999, "area code");
rangeCheck(prefix, 999, "prefix");
rangeCheck(lineNumber, 9999, "line number");
this.areaCode = (short) areaCode;
this.prefix = (short) prefix;
this.lineNumber = (short) lineNumber;
}
private static void rangeCheck(int arg, int max, String name) {
if (arg < 0 || arg > max)
throw new IllegalArgumentException(name +": " + arg);
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNumber == lineNumber
&& pn.prefix == prefix
&& pn.areaCode == areaCode;
}
// Broken - no hashCode method!
... // Remainder omitted
}


假设你打算在HashMap中使用这个类:


[java]
Map m
= HashMap();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");

Map m
= HashMap();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");
这时候你可能期望m.get(new PhoneNumber(707, 867, 5309))返回"Jenny",可它实际上却返回null。本例中两个PhoneNumber对象是equals的,但hashCode不同;导致在put的时候将对象A放到一个哈希桶A中,但get的时候却根据对象B的哈希码 到哈希桶B中却查找对象。

即使碰巧对象AB都指向相同的哈希桶,get方法也会返回null;因为HashMap有一项优化,它会将每个项关联的哈希码缓存起来,当哈希码不同时,就不再去检查对象等同性了。


修正这个问题非常简单,只要给PhoneNumber类提供一个合适的hashCode方法即可。那么hashCode()应该怎么写呢?编写一个合法但是不好用的hashCode是没有价值的,【例】如下hashCode()是合法的,但永远不应该这么写:


[java]
@Override
public int hashCode() {
return 42;
}

@Override
public int hashCode() {
return 42;
}
它是合法的,因为保证了相等的对象拥有相同的哈希码。但他也是很恶劣的,因为所有对象都拥有相同的哈希码。因此,每个对象都被放到相同的哈希桶中,使得哈希表(hash table)退化为链表(linked list)。本来应该线性时间运行的程序变成平方时间运行了,对于大型的哈希表,这会关系到能否正常工作。