32

Eclipse 源菜单有一个“生成 hashCode / equals 方法”,它生成如下所示的函数。

String name; 
@Override
public int hashCode()
{
    final int prime = 31;
    int result = 1;
    result = prime * result + ((name == null) ? 0 : name.hashCode());
    return result;
}

@Override
public boolean equals(Object obj)
{
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    CompanyRole other = (CompanyRole) obj;
    if (name == null)
    {
        if (other.name != null)
            return false;
    } else if (!name.equals(other.name))
        return false;
    return true;
}

如果我在生成时选择多个字段hashCode()并且equals()Eclipse 使用上面显示的相同模式。

我不是哈希函数方面的专家,我想知道生成的哈希函数有多“好”?什么情况下它会发生故障并导致过多的碰撞?

4

8 回答 8

19

java.util.ArrayList您可以在as中看到 hashCode 函数的实现

public int hashCode() {
    int hashCode = 1;
    Iterator<E> i = iterator();
    while (i.hasNext()) {
        E obj = i.next();
        hashCode = 31*hashCode + (obj==null ? 0 : obj.hashCode());
    }
    return hashCode;
}

这是一个这样的示例,您的 Eclipse 生成的代码遵循类似的实现方式。但是如果你觉得你必须自己实现你的 hashCode,那么 Joshua Bloch 在他的名著Effective Java中给出了一些很好的指导。我将发布该书第 9 项中的重要观点。那些是,

  1. 在名为 result 的 int 变量中存储一些常量非零值,例如 17。
  2. 对于对象中的每个重要字段 f(即 equals 方法考虑的每个字段),请执行以下操作:

    一种。计算字段的 int 哈希码 c:

    一世。如果该字段是布尔值,则计算 (f ? 1 : 0)。

    ii. 如果字段是 byte、char、short 或 int,则计算 (int) f。

    iii. 如果字段是长的,则计算 (int) (f ^ (f >>> 32))。

    iv. 如果该字段是浮点数,则计算 Float.floatToIntBits(f)。

    v. 如果该字段是双精度,则计算 Double.doubleToLongBits(f),然后按照步骤 2.a.iii 对结果进行散列运算。

    六。如果该字段是对象引用,并且此类的 equals 方法通过递归调用 equals 来比较该字段,则在该字段上递归调用 hashCode。如果需要更复杂的比较,请为此字段计算“规范表示”并在规范表示上调用 hashCode。如果该字段的值为 null,则返回 0(或其他一些常量,但 0 是传统的)

    七。如果该字段是一个数组,则将其视为每个元素都是一个单独的字段。也就是说,通过递归应用这些规则来计算每个重要元素的哈希码,并在步骤 2.b 中组合这些值。如果数组字段中的每个元素都很重要,则可以使用 1.5 版中添加的 Arrays.hashCode 方法之一。

    湾。将步骤 2.a 中计算的哈希码 c 合并到结果中,如下所示:

       result = 31 * result + c;
    
  3. 返回结果。

  4. 当你写完 hashCode 方法后,问问自己相等的实例是否有相等的哈希码。编写单元测试来验证你的直觉!如果相等的实例具有不相等的哈希码,找出原因并解决问题。

我想,Java 语言设计者和 Eclipse 似乎遵循了类似的准则。快乐编码。干杯。

于 2012-08-03T12:05:31.137 回答
14

从 Java 7 开始,您可以使用java.util.Objects编写简短而优雅的方法:

class Foo {
  private String name;
  private String id;

  @Override
  public int hashCode() {
    return Objects.hash(name,id);
  }

  @Override
  public boolean equals(Object obj) {
    if (obj instanceof Foo) {
      Foo right = (Foo) obj;
      return Objects.equals(name,right.name) && Objects.equals(id,right.id);
    }
    return false;
  }
}
于 2015-10-14T08:23:54.843 回答
5

通常它是好的,但是:

  1. 番石榴做得更好,我更喜欢它。[编辑:从 JDK7 开始,Java 似乎提供了类似的哈希函数]。
  2. 某些框架在直接访问字段而不是使用 setter/getter 时可能会导致问题,例如Hibernate。对于 Hibernate 创建的一些惰性字段,它会创建一个代理而不是真实对象。只有调用 getter 才能让 Hibernate 获取数据库中的实际值。
于 2012-08-03T11:59:10.227 回答
4

是的,它是完美的 :) 您将在 Java 源代码中几乎无处不在地看到这种方法。

于 2012-08-03T11:47:37.880 回答
0

这是编写散列函数的标准方法。但是,如果您对这些领域有一些了解,您可以改进/简化它。例如,如果您的类保证该字段永远不会为空(也适用于 equals()),您可以省略空检查。或者,如果只使用一个字段,您可以委托该字段的哈希码。

于 2012-08-03T12:39:58.970 回答
0

我还想添加对 Joshua Bloch 在 Effective Java 2nd Edition 中的 Item 9 的引用。

这是一个食谱Item 9 : ALWAYS OVERRIDE HASHCODE WHEN YOU OVERRIDE EQUALS

  1. 在名为 result 的 int 变量中存储一些常量非零值,例如 17。
  2. 对于对象中的每个重要字段 f(即 equals 方法考虑的每个字段),请执行以下操作:
    a. Compute an int hash code c for the field:            
        i.   If the field is a boolean, compute (f ? 1 : 0).
        ii.  If the field is a byte, char, short, or int, compute (int) f.
        iii. If the field is a long,compute(int)(f^(f>>>32)).
        iv.  If the field is a float, compute Float.floatToIntBits(f).
        v.   If the field is a double, compute Double.doubleToLongBits(f), and then hash the resulting long as in step 2.a.iii.
        vi.  If the field is an object reference and this class’s equals method compares the field by recursively invoking equals, recursively invoke hashCode on the field. If a more complex comparison is required, compute a “canonical representation” for this field and invoke hashCode on the canonical representation. If the value of the field is null, return 0 (or some other constant, but 0 is traditional).
        vii. If the field is an array, treat it as if each element were a separate field. That is, compute a hash code for each significant element by applying these rules recursively, and combine these values per step 2.b. If every element in an array field is significant, you can use one of the Arrays.hashCode methods added in release 1.5. 
   b. Combine the hash code c computed in step 2.a into result as follows: result = 31 * result + c;
3. Return result.
4. When you are finished writing the hashCode method, ask yourself whether equal instances have equal hash codes. Write unit tests to verify your intuition! If equal instances have unequal hash codes, figure out why and fix the problem.
于 2012-08-03T20:23:46.860 回答
0

如果您使用的是 Apache Software Foundation(commons-lang 库),那么下面的类将帮助您使用反射生成 hashcode/equals/toString 方法。

添加/删除实例变量时,您无需担心重新生成 hashcode/equals/toString 方法。

EqualsBuilder - 这个类提供了为任何类构建好的equals方法的方法。它遵循 Joshua Bloch 在 Effective Java 中制定的规则。特别是比较双精度、浮点数和数组的规则可能很棘手。此外,确保 equals() 和 hashCode() 一致可能很困难。

HashCodeBuilder - 这个类可以为任何类构建一个好的 hashCode 方法。它遵循 Joshua Bloch 的《Effective Java》一书中规定的规则。编写一个好的 hashCode 方法实际上是相当困难的。此类旨在简化流程。

ReflectionToStringBuilder - 此类使用反射来确定要附加的字段。因为这些字段通常是私有的,所以该类使用 AccessibleObject.setAccessible(java.lang.reflect.AccessibleObject[], boolean) 来更改字段的可见性。除非正确设置了适当的权限,否则这将在安全管理器下失败。

Maven依赖:

<dependency>
        <groupId>commons-lang</groupId>
        <artifactId>commons-lang</artifactId>
        <version>${commons.lang.version}</version>
</dependency>

示例代码:

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ReflectionToStringBuilder;

public class Test{

    instance variables...
    ....

    getter/setter methods...
    ....

    @Override
    public String toString() {
        return ReflectionToStringBuilder.toString(this);
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this);
    }

    @Override
    public boolean equals(Object obj) {
        return EqualsBuilder.reflectionEquals(this, obj);
    }
}
于 2018-03-29T21:46:57.593 回答
0

一个潜在的缺点是所有具有空字段的对象都将具有 31 的哈希码,因此仅包含空字段的对象之间可能存在许多潜在的冲突。这将使Maps.

当您Map的键类型具有多个子类时,可能会发生这种情况。例如,如果您有一个HashMap<Object, Object>,您可能有许多哈希码为 31 的键值。诚然,这种情况不会经常发生。如果您愿意,您可以将素数的值随机更改为 31 以外的值,并减少碰撞的可能性。

于 2018-08-20T12:14:39.987 回答