×

消息

EU e-Privacy Directive

This website uses cookies to manage authentication, navigation, and other functions. By using our website, you agree that we can place these types of cookies on your device.

View e-Privacy Directive Documents

You have declined cookies. This decision can be reversed.

如果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;
}

提交评论


安全码
刷新