本文主要讨论重写equals方法的相关注意事项,在每个类中,在重写 equals 方法的时侯,一定要重写 hashcode 方法。 如果不这样做,你的类违反了 hashCode 的通用约定,这会阻止它在 HashMap 和 HashSet 这样的集合中正常工作。根据 Object 规范,以下时具体约定。
- 当在一个应用程序执行过程中,如果在 equals 方法比较中没有修改任何信息,在一个对象上重复调用 hashCode 方法时,它必须始终返回相同的值。从一个应用程序到另一个应用程序的每一次执行返回的值可以是不一致的。
- 如果两个对象根据 equals(Object) 方法比较是相等的,那么在两个对象上调用 hashCode 就必须产生的结果是相同的整数。
- 如果两个对象根据 equals(Object) 方法比较并不相等,则不要求在每个对象上调用 hashCode 都必须产生不同的结果。 但是,程序员应该意识到,为不相等的对象生成不同的结果可能会提高散列表(hash tables)的性能。
Mapm = new HashMap<>();m.put(new PhoneNumber(707, 867, 5309), "Jenny");
你可能期望 m.get(new PhoneNumber(707, 867, 5309))
方法返回 Jenny
字符串,但实际上,返回了 null。注意,这里涉及到两个 PhoneNumber
实例:一个实例插入到 HashMap 中,另一个作为判断相等的实例用来检索。PhoneNumber
类没有重写 hashCode 方法导致两个相等的实例返回了不同的哈希码,违反了 hashCode 约定。put 方法把 PhoneNumber
实例保存在了一个哈希桶( hash bucket)中,但 get 方法却是从不同的哈希桶中去查找,即使恰好两个实例放在同一个哈希桶中,get 方法几乎肯定也会返回 null。因为 HashMap 做了优化,缓存了与每一项(entry)相关的哈希码,如果哈希码不匹配,则不会检查对象是否相等了(还要检查对象是否相等是因为即使对象哈希码一致,都放在了同一个哈希桶中,也可能对象不等)。
一个好的 hash 方法趋向于为不相等的实例生成不相等的哈希码。这也正是 hashCode 约定中第三条的表达。理想情况下,hash 方法为集合中不相等的实例均匀地分配 int 范围内的哈希码。实现这种理想情况可能是困难的。 幸运的是,要获得一个合理的近似的方式并不难。 以下是一个简单的配方:
1.声明一个 int 类型的变量 result,并将其初始化为对象中第一个重要属性 c
的哈希码,如下面步骤 2.a 中所计算的那样。(重要的属性是影响比较相等的领域)
2.对于对象中剩余的重要属性 f
,请执行以下操作:
a. 比较属性 f
与属性 c
的 int 类型的哈希码:
-- i. 如果这个属性是基本类型的,使用 Type.hashCode(f)
方法计算,其中 Type
类是对应属性 f
基本类型的包装类。
-- iii. 如果属性 f
是一个数组,把它看作每个重要的元素都是一个独立的属性。 也就是说,通过递归地应用这些规则计算每个重要元素的哈希码,并且将每个步骤 2.b 的值合并。 如果数组没有重要的元素,则使用一个常量,最好不要为 0。如果所有元素都很重要,则使用 Arrays.hashCode
方法。
b. 将步骤 2.a 中属性 f 计算出的哈希码合并为如下结果:result = 31 * result + f;
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;}
Objects
类有一个静态方法,它接受任意数量的对象并为它们返回一个哈希码。 这个名为 hash 的方法可以让你编写一行 hashCode 方法,其质量与根据这个项目中的上面编写的方法相当。 不幸的是,它们的运行速度更慢,因为它们需要创建数组以传递可变数量的参数,以及如果任何参数是基本类型,则进行装箱和取消装箱。 这种哈希函数的风格建议仅在性能不重要的情况下使用。 以下是使用这种技术编写的 PhoneNumber
的哈希函数:
// One-line hashCode method - mediocre performance@Overridepublic int hashCode() { return Objects.hash(lineNum, prefix, areaCode);}
最后需要注意的是:不要为 hashCode 返回的值提供详细的规范,因此客户端不能合理地依赖它; 你可以改变它的灵活性。 比如在Java 类库中的许多类(例如 String 和 Integer)都将 hashCode 方法返回的确切值指定为实例值的函数(该函数就相当于为 hashCode 返回的值提供详细的规范)。 这不是一个好主意,而是一个我们不得不忍受的错误:因为它妨碍了在未来版本中改进哈希函数的能力。 如果未指定细节(也就不会依赖它)并在散列函数中发现缺陷,或者发现了更好的哈希函数,则可以在后续版本中对其进行更改。
总之,每次重写 equals 方法时都必须重写 hashCode 方法,否则程序将无法正常运行。你的 hashCode 方法必须遵从 Object 类指定的常规约定,并且必须执行合理的工作,将不相等的哈希码分配给不相等的实例。