Effective Java 第三版读书笔记——条款11:重写 equals 方法的同时也要重写 hashCode 方法

在每一个重写 equals 方法的类中,都要重写 hashCode 方法。如果不这样做,你的类会违反 hashCode 的通用约定,这会阻止它在 HashMap 和 HashSet 这样的集合中正常工作。下面是根据 Object 源码改编的约定:

  1. 在一个应用程序执行过程中,如果在 equals 方法比较中没有修改任何信息,在一个对象上重复调用 hashCode 方法必须始终返回相同的值。从一个应用程序到另一个应用程序时返回的值可以是不一致的。
  2. 如果两个对象根据 equals(Object) 方法比较是相等的,那么在这两个对象上调用 hashCode 就必须产生相同的整数结果。
  3. 如果两个对象根据 equals(Object) 方法比较并不相等,不要求在每个对象上调用 hashCode 都必须产生不同的结果。 但是,程序员应该意识到,为不相等的对象生成不同的结果可能会提高散列表(hash tables)的性能。

没有重写 hashCode 会违反上述约定的第二条。两个逻辑上相同的对象调用 hashCode 方法,却被 Object 类的 hashCode 方法返回了两个不同的哈希码。

举例说明,假设你使用条款 10 中的 PhoneNumber 类的实例做为 HashMap 的键(key):

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

你可能期望 m.get(new PhoneNumber(707, 867, 5309)) 方法返回 Jenny 字符串,但实际上返回了 null。注意,这里涉及到两个 PhoneNumber 实例:一个实例被用来插入到 HashMap 中,另一个作为判断相等的实例用来检索。PhoneNumber 类没有重写 hashCode 方法导致两个相等的实例返回了不同的哈希码。

解决这个问题很简单,只需要为 PhoneNumber 类重写一个合适的 hashCode 方法。一个好的 hash 方法趋向于为不相等的实例生成不相等的哈希码。理想情况下,hash 方法为集合中不相等的实例均匀地分配 int 范围内的哈希码。实现这种理想情况可能很困难。 幸运的是,要获得一个合理的近似方式并不难。 以下是一个简单的秘诀:

  1. 声明一个 int 类型的变量 result,并将其初始化为对象中第一个重要属性 c 的哈希码,如下面步骤 2.a 中所计算的那样。

  2. 对于对象中剩余的重要属性 f ,执行以下操作:

    a. 为属性 f 与计算一个 int 类型的哈希码 c

    • i. 如果这个属性是基本类型,使用 Type.hashCode(f) 方法计算,其中 Type 类是对应属性 f 的包装类。
    • ii. 如果该属性是一个对象引用,并且该类的 equals 方法通过递归调用 equals 来比较该属性,那么递归地调用 hashCode 方法。 如果需要更复杂的比较,则计算此字段的“范式(canonical representation)”,并在范式上调用 hashCode 。 如果该字段的值为空,则使用 0(也可以使用其他常数,但通常使用 0 表示)。
    • iii. 如果属性 f 是一个数组,把数组中每个重要的元素都看作是一个独立的属性。如果数组没有重要的元素,则使用一个常量,最好不要为0。如果所有元素都很重要,则使用 Arrays.hashCode 方法。

    b. 将步骤 2.a 中计算出的哈希码 c 合并为如下结果:result = 31 * result + c;

  3. 返回 result 值。

让我们把上述办法应用到 PhoneNumber 类中:

// Typical hashCode method
@Override public int hashCode() {
    int result = Short.hashCode(areaCode);
    result = 31 * result + Short.hashCode(prefix);
    result = 31 * result + Short.hashCode(lineNum);
    return result;
}

这个方法返回一个简单的确定性的计算结果,它的唯一的输入是 PhoneNumber 实例中的三个重要的属性,所以相等的 PhoneNumber 实例具有相同的哈希码。实际上,这个方法是一个非常好的 hashCode 实现,与 Java 平台类库中的实现一样。它很简单,速度相当快,并且合理地将不相同的电话号码分散到不同的哈希桶中。

如果一个类是不可变的,并且计算哈希码的代价很大,那么可以考虑在对象中缓存哈希码,而不是在每次请求时重新计算哈希码。如果你认为这种类型的大多数对象将被用作哈希键,那么应该在创建实例时计算哈希码。否则,可以选择在首次调用 hashCode 时延迟初始化(lazily initialize)哈希码。PhoneNumber 类不适合这种情况,下面的代码只是为了展示延迟初始化是如何完成的。请注意,属性 hashCode 的初始值(在本例中为0)不应该是通常创建的实例的哈希码:

// hashCode method with lazily initialized cached hash code
private int hashCode; // Automatically initialized to 0

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

不要试图从哈希码计算中排除重要的属性来提高性能。 由此产生的哈希函数可能运行得更快,但其差劲的质量可能会降低哈希表的性能,甚至使哈希表无法使用。如果发生这种情况,哈希函数将把所有这些实例映射到少许哈希码上,而应该以线性时间运行的程序将会运行平方级的时间。

posted @ 2018-11-24 11:43  LeeFire  阅读(224)  评论(0编辑  收藏  举报