IntelliJ默认的hashCode()实现说明

4

我看了一下IntelliJ默认的hashCode()实现,想知道他们为什么要这样实现。由于我对哈希概念很陌生,找到了一些矛盾的说法,需要澄清:

public int hashCode(){
  // creationDate is of type Date
  int result = this.creationDate != null ? this.creationDate.hashCode() : 0;
  // id is of type Long (wrapper class)
  result = 31 * result + (this.id != null ? this.id.hashCode() : 0);
  // code is of type String
  result = 31 * result + (this.code != null ? this.code.hashCode() : 0);
  // revision is of type int
  result = 31 * result + this.revision;
  return result;
}

在我看来,关于这个主题最好的资源似乎是这篇Java World文章,因为我发现他们的论点最具有说服力。所以我想知道:

  1. 上述资源中提到的其中一个论点是,乘法是较慢的操作之一。那么,当我调用引用类型的hashCode()方法时,跳过与质数的乘法运算是否更好呢?因为大多数情况下,这已经包括了这样的乘法运算。
  2. Java World指出,位异或^也可以改善计算,但没有提到原因:(相比普通加法,确切地说可能有什么优势呢?
  3. 如果返回的类字段为null,返回不同的值是否更好?这将使结果更易区分,对于使用非零值是否有巨大的不利影响?

老实说,他们的示例代码更吸引人:

  public boolean hashCode() {
    return
     (name  == null ? 17 : name.hashCode()) ^ 
     (birth == null ? 31 : name.hashCode());
  }  

但我不确定这是否客观真实。我也有点怀疑IntelliJ,因为它们默认的equals(Object)代码是通过instanceof进行比较,而不是直接比较实例类。我同意这篇Java world文章的观点,这似乎无法正确履行合同。

1个回答

4
关于hashCode(),我认为最重要的是尽量减少碰撞(两个不同的对象具有相同的hashCode())而不是hashCode()计算的速度。是的,hashCode()应该快(如果可能的话是恒定时间),但对于使用hashCode()(maps, sets等)的大型数据结构来说,碰撞更重要。
如果您的hashCode()函数在常数时间内执行(与数据和输入大小无关)并产生良好的哈希函数(少碰撞),则映射上的操作(get、contains、put)将渐近地在常数时间内执行。
如果您的hashCode()函数产生了很多碰撞,则性能会受到影响。在极端情况下,您可以始终从hashCode()返回0——函数本身将非常快,但映射操作将随着映射大小呈线性增长。
在添加另一个字段的子哈希码之前乘以hashCode()通常会提供更少的碰撞——这是一种基于字段通常包含类似数据/小数字的启发式方法。
考虑一个Person类的例子:
class Person {
    int age;
    int heightCm;
    int weightKg;
}

如果您只是将数字相加以计算hashCode,则所有人的结果都将介于60和500之间。如果按照Idea的方式进行乘法,您将获得2000到超过100,000之间的hashCode——更大的空间,因此发生碰撞的几率更小。
使用XOR不是一个很好的主意,例如,如果您有一个带有“高度”和“宽度”字段的“矩形”类,则所有正方形的hashCode都相同为0。
至于使用instanceof vs.getClass().equals()在equals()中,我从来没有见过一个决定性的辩论。两者都有其优点和缺点,并且如果您不小心,两种方法都可能会引起麻烦:
- 如果您使用instanceof,任何覆盖您的equals()的子类都可能会破坏对称要求 - 如果您使用getClass().equals(),这将无法与某些框架(如Hibernate)很好地配合使用,因为它们会生成自己的子类来存储自己的技术信息。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接