如果equals方法被覆盖,hashCode方法也必须被覆盖

Java语言规范中为Object.hashCode方法制定了须遵守的合约:

1.在程序的同一次运行(execution)中,给定某个对象,如果此对象用于计算equals返回值的信息没有改变,那么多次执行其hashCode方法,都必须返回同样的哈希值。当然,不同的运行之间,哈希值可以不同。

2.如果两个对象,依据equals方法的判定,是相等的,那么他们的哈希值必须相同。

3.如果两个对象,依据equals方法的判定,是不相等的,那么他们的哈希值不一定不等。如果此情况下可以保证两哈希值不等,那么所有依赖哈希值的数据结构(HashMap,HashSet和Hashtable)的性能将被优化。

如果上述合约被破坏,在依赖哈希值的数据结构中,程序的行为将会出错。

以上三条合约中,最容易被破坏的是第二条:相等的对象必须有相同的哈希值。比如下面的例子:

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
}

这段代码覆盖了equals方法,而未覆盖hashCode方法,所以它将继承Object类中默认的hashCode:由对象的内存地址计算而来的一个整数。显然,对于由equals方法(已覆盖)判定相等的两个对象,hashCode很难相等,这将严重破坏Hash类数据结构的行为。比如:

Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");
String nameOfJenny = m.get(new PhoneNumber(707, 867, 5309));

nameOfJenny将得到一个空值,而非预期的"Jenny"。这是因为jvm为HashMap做了优化:比较被搜索的key和表中存储的key时,如果二者哈希值不等,则直接判定不吻合,不会再用equals方法对二者进行比较。解决方法就是为上面的类覆盖hashCode方法:

@Override public int hashCode() {
	int result = 17;
	result = 31 * result + areaCode;
	result = 31 * result + prefix;
	result = 31 * result + lineNumber;
	return result;
}

推荐的一套计算哈希值的方法:

第一步:选定一个正整数作为初始值

第二步:为每个在equals方法中参与计算的属性f,定义一个hash:

a.

i.    f类型为boolean,计算(f?1:0)

ii.   类型为byte,char,short或者int,计算(int)f

iii.  类型为long,计算(int)(f^(f>>>32))

iv.  类型为float,计算Float.floatToIntBits(f)

v.   类型为double,计算Double.doubleToLongBits(f),然后继以上面的步骤iii

vi.  f是对象,而且在equals方法中递归调用了该属性的equals方法,那么调用此属性的hashCode方法,以其结果作为此属性的哈希值

vii. f是数组,将其每个元素视作单独的属性分别计算,最后用下面的步骤b计算数组属性的hash。如果数组中每个元素都参与equals计算,还可以用Arrays.hashCode方法(Java 1.5以上版本)。

b.

将初始值和a步骤中计算出的哈希值用下面的算法合并:

result = 31 * result + c;

第三步:返回result

第四部:再次检验合约第二条,看equals判定相等的对象是否有相同的hash。并以unit test来检验。

写hashCode方法时,冗余属性(值可以由其他属性计算出来的属性)可以被忽略。如果hashCode方法开销巨大,可以考虑将哈希结果缓存起来,并采用lazy方式初始化:

// Lazily initialized, cached hashCode
private volatile int hashCode; // (See Item 71)

@Override public int hashCode() {
	int result = hashCode;
	if (result == 0) {
		result = 17;
		result = 31 * result + areaCode;
		result = 31 * result + prefix;
		result = 31 * result + lineNumber;
		hashCode = result;
	}
	return result;
}