集合的 hashCode 方法的最佳实现

Posted

技术标签:

【中文标题】集合的 hashCode 方法的最佳实现【英文标题】:Best implementation for hashCode method for a collection 【发布时间】:2010-09-11 22:15:14 【问题描述】:

我们如何确定集合的hashCode() 方法的最佳实现(假设equals 方法已被正确覆盖)?

【问题讨论】:

用Java 7+,我猜Objects.hashCode(collection)应该是一个完美的解决方案! @Diablo 我认为这根本不能回答问题 - 该方法只返回 collection.hashCode() (hg.openjdk.java.net/jdk7/jdk7/jdk/file/9b8c96f96a0f/src/share/…) 【参考方案1】:

对于一个简单的类,通常最容易根据 equals() 实现检查的类字段来实现 hashCode()。

public class Zam 
    private String foo;
    private String bar;
    private String somethingElse;

    public boolean equals(Object obj) 
        if (this == obj) 
            return true;
        

        if (obj == null) 
            return false;
        

        if (getClass() != obj.getClass()) 
            return false;
        

        Zam otherObj = (Zam)obj;

        if ((getFoo() == null && otherObj.getFoo() == null) || (getFoo() != null && getFoo().equals(otherObj.getFoo()))) 
            if ((getBar() == null && otherObj. getBar() == null) || (getBar() != null && getBar().equals(otherObj. getBar()))) 
                return true;
            
        

        return false;
    

    public int hashCode() 
        return (getFoo() + getBar()).hashCode();
    

    public String getFoo() 
        return foo;
    

    public String getBar() 
        return bar;
    

最重要的是保持hashCode() 和equals() 一致:如果equals() 对两个对象返回true,那么hashCode() 应该返回相同的值。如果 equals() 返回 false,则 hashCode() 应该返回不同的值。

【讨论】:

像 SquareCog 已经注意到了。如果从两个字符串的连接中生成一次哈希码,则极容易产生大量冲突:("abc"+""=="ab"+"c"=="a"+"bc"==""+"abc")。这是严重的缺陷。最好先评估两个字段的哈希码,然后计算它们的线性组合(最好使用素数作为系数)。 @KrzysztofJabłoński 对。此外,交换foobar 也会产生不必要的冲突。【参考方案2】:

@about8 :那里有一个非常严重的错误。

Zam obj1 = new Zam("foo", "bar", "baz");
Zam obj2 = new Zam("fo", "obar", "baz");

相同的哈希码

你可能想要类似的东西

public int hashCode() 
    return (getFoo().hashCode() + getBar().hashCode()).toString().hashCode();

(这些天你能直接从 Java 中的 int 获取 hashCode 吗?我认为它会进行一些自动转换。如果是这种情况,请跳过 toString,它很难看。)

【讨论】:

bug 出现在 about8.blogspot.com 的长答案中——从字符串的串联中获取哈希码会为您提供一个哈希函数,该哈希函数对于加起来为的任何字符串组合都是相同的相同的字符串。 所以这是元讨论,根本与问题无关? ;-) 这是对存在相当严重缺陷的提议答案的更正。 这是一个非常有限的实现 你的实现避免了这个问题并引入了另一个问题;交换foobar 会导致相同的hashCode。您的toStringAFAIK 无法编译,如果编译,则效率极低。像109 * getFoo().hashCode() + 57 * getBar().hashCode() 这样的东西更快、更简单并且不会产生不必要的冲突。【参考方案3】:

首先确保equals被正确实现。来自an IBM DeveloperWorks article:

对称性:对于两个引用,a 和 b,a.equals(b) 当且仅当 b.equals(a) 自反性:对于所有非空引用,a.equals(a) 传递性:如果 a.equals(b) 和 b.equals(c),则 a.equals(c)

然后确保他们与 hashCode 的关系尊重联系人(来自同一篇文章):

与 hashCode() 的一致性:两个相等的对象必须具有相同的 hashCode() 值

最后一个好的哈希函数应该努力接近ideal hash function。

【讨论】:

【参考方案4】:

只是一个快速说明,用于完成其他更详细的答案(就代码而言):

如果我考虑问题how-do-i-create-a-hash-table-in-java,尤其是jGuru FAQ entry,我认为可以判断哈希码的其他一些标准是:

同步(算法是否支持并发访问)? 失败安全迭代(算法是否检测到在迭代期间发生变化的集合) 空值(哈希码是否支持集合中的空值)

【讨论】:

【参考方案5】:

正如您特别要求的集合,我想添加一个其他答案尚未提到的方面:HashMap 不希望它们的键在添加到集合后更改其哈希码。会破坏整个目的......

【讨论】:

【参考方案6】:

如果我正确理解您的问题,您有一个自定义集合类(即从 Collection 接口扩展的新类)并且您想要实现 hashCode() 方法。

如果您的集合类扩展了 AbstractList,那么您不必担心,已经有一个 equals() 和 hashCode() 的实现,它通过遍历所有对象并将它们的 hashCodes() 相加来工作。

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

现在,如果您想要计算特定类的哈希码的最佳方法,我通常使用 ^(按位异或)运算符来处理我在 equals 方法中使用的所有字段:

public int hashCode()
   return intMember ^ (stringField != null ? stringField.hashCode() : 0);

【讨论】:

【参考方案7】:

在可能的范围内均匀分布散列值的任何散列方法都是一个很好的实现。请参阅有效的 java (http://books.google.com.au/books?id=ZZOiqZQIbRMC&dq=effective+java&pg=PP1&ots=UZMZ2siN25&sig=kR0n73DHJOn-D77qGj0wOxAxiZw&hl=en&sa=X&oi=book_result&resnum=1&ct=result),那里有一个很好的哈希码实现技巧(我认为第 9 项......)。

【讨论】:

【参考方案8】:

最好的实现?这是一个难题,因为它取决于使用模式。

Josh BlochEffective Java 在第 8 条(第二版)中提出了几乎所有情况下合理的良好实现。最好的办法是在那里查找,因为作者在那里解释了为什么这种方法很好。

一个简短的版本

    创建一个int result 并分配一个非零值。

    对于在equals() 方法中测试的每个字段 f,通过以下方式计算c 的哈希码:

    如果字段 f 是 boolean: 计算(f ? 0 : 1); 如果字段f是bytecharshortint:计算(int)f; 如果字段f是long:计算(int)(f ^ (f >>> 32)); 如果字段f是float:计算Float.floatToIntBits(f); 如果字段 f 是 double:计算 Double.doubleToLongBits(f) 并像处理每个 long 值一样处理返回值; 如果字段 f 是对象:使用hashCode() 方法的结果,如果f == null 则使用0; 如果字段 f 是 数组:将每个字段视为单独的元素,并以 递归方式计算哈希值,然后将这些值组合起来,如下所述。李>

    将哈希值cresult结合起来:

    result = 37 * result + c
    

    返回result

这应该会导致哈希值在大多数使用情况下得到正确分布。

【讨论】:

是的,我特别好奇数字 37 是从哪里来的。 我使用了 Josh Bloch 的“Effective Java”一书的第 8 项。 @dma_k 使用素数的原因和此答案中描述的方法是为了确保计算的哈希码是唯一的。使用非质数时,您不能保证这一点。选择哪个素数并不重要,数字 37 并没有什么神奇之处(可惜 42 不是素数,嗯?) @SimonAndréForsberg 好吧,计算出来的哈希码不能总是唯一的 :) 是一个哈希码。但是我明白了:素数只有一个乘数,而非素数至少有两个。这为乘法运算符创建了一个额外的组合以产生相同的哈希,即导致冲突。 我认为Bloch multiplies by 31, not 37,对于its ease of optimization。【参考方案9】:

在Apache Commons Lang 中有一个很好的Effective Javahashcode()equals() 逻辑实现。结帐HashCodeBuilder 和EqualsBuilder。

【讨论】:

这个 API 的缺点是每次调用 equals 和 hashcode 时都要支付对象构造的成本(除非您的对象是不可变的并且您预先计算了哈希),这在某些情况下可能很多。 这是我最喜欢的方法,直到最近。我在使用 SharedKey OneToOne 关联的条件时遇到了 ***Error。此外,Objects 类提供了从 Java7 开始的 hash(Object ..args)equals() 方法。这些推荐用于使用 jdk 1.7+ 的任何应用程序 @Diablo 我猜,你的问题是对象图中的一个循环,然后你对大多数实现都不走运,因为你需要忽略一些参考或打破循环(强制@987654329 @)。 FWIW 我对所有实体使用基于 id 的 hashCode 和 equals。【参考方案10】:

about8.blogspot.com,你说

如果 equals() 对两个对象返回 true,则 hashCode() 应该返回相同的值。如果equals()返回false,那么hashCode()应该返回不同的值

我不能同意你的看法。如果两个对象具有相同的哈希码,则不一定意味着它们相等。

如果 A 等于 B 则 A.hashcode 必须等于 B.hascode

但是

如果 A.hashcode 等于 B.hascode 并不意味着 A 必须等于 B

【讨论】:

如果(A != B) and (A.hashcode() == B.hashcode()),这就是我们所说的哈希函数碰撞。这是因为哈希函数的域总是有限的,而它的域通常不是。共域越大,碰撞发生的频率就越低。好的散列函数应该为不同的对象返回不同的散列,在给定特定的共域大小的情况下具有最大的可能性。但是,它很少能得到完全保证。 这应该只是对上述 Grey 帖子的评论。很好的信息,但它并没有真正回答问题 好的 cmets 但要小心使用术语“不同对象”...因为 equals() 和 hashCode() 实现不一定与 OO 上下文中的不同对象有关,但通常更多关于他们的领域模型表示(例如,如果两个人共享一个国家代码和国家 ID,则可以认为他们是相同的——尽管它们在 JVM 中可能是两个不同的“对象”——他们被认为是“相等的”并具有给定的 hashCode)。 ..【参考方案11】:

我更喜欢使用 Google Collections lib 中的 Objects 类中的实用方法,这有助于我保持代码整洁。很多时候equalshashcode 方法是由IDE 的模板制作的,所以它们读起来不干净。

【讨论】:

【参考方案12】:

在 Apache Commons EqualsBuilder 和 HashCodeBuilder 上使用反射方法。

【讨论】:

如果你打算使用它,请注意反射是昂贵的。老实说,除了扔掉代码之外,我不会将它用于任何事情。【参考方案13】:

如果你使用eclipse,你可以生成equals()hashCode()使用:

Source -> 生成 hashCode() 和 equals()。

使用这个函数,您可以决定哪些字段要用于相等性和哈希码计算,Eclipse 会生成相应的方法。

【讨论】:

【参考方案14】:

最好使用Eclipse提供的功能,它做得很好,你可以把你的精力和精力放在开发业务逻辑上。

【讨论】:

+1 一个很好的实用解决方案。 dmeister 的解决方案比较全面,但是我自己写hashcode的时候往往会忘记处理空值。 +1 同意 Quantum7,但我想说了解 Eclipse 生成的实现在做什么以及它从哪里获取实现细节也非常好。 抱歉,但涉及“[某些 IDE] 提供的功能”的答案在一般编程语言的上下文中并不真正相关。有几十个 IDE,这并不能回答这个问题......也就是说,因为这更多是关于算法确定并直接与 equals() 实现相关 - IDE 对此一无所知。【参考方案15】:

在组合hash值时,我通常使用boost c++库中的组合方法,即:

seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);

这在确保均匀分布方面做得相当好。有关此公式如何工作的一些讨论,请参阅 *** 帖子:Magic number in boost::hash_combine

在http://burtleburtle.net/bob/hash/doobs.html

上对不同的哈希函数进行了很好的讨论

【讨论】:

这是一个关于 Java 的问题,而不是 C++。【参考方案16】:

如果您对 dmeister 推荐的 Effective Java 实现感到满意,您可以使用库调用而不是自己滚动:

@Override
public int hashCode() 
    return Objects.hashCode(this.firstName, this.lastName);

这需要 Guava (com.google.common.base.Objects.hashCode) 或 Java 7 中的标准库 (java.util.Objects.hash),但工作方式相同。

【讨论】:

除非有充分的理由不使用这些,否则在任何情况下都绝对应该使用这些。 (公式化它更强大,因为它应该被公式化。)使用标准实现/库的典型论点适用(最佳实践,经过良好测试,不易出错等)。 @justin.hughey 你似乎很困惑。您应该覆盖hashCode 的唯一情况是您有一个自定义equals,而这正是这些库方法的设计目的。文档非常清楚他们与equals 相关的行为。库实现并不声称可以免除您了解正确 hashCode 实现的特征的责任 - 这些库使您在大多数情况下更容易实现这种符合 @ 的实现987654328@ 被覆盖。 对于任何查看 java.util.Objects 类的 android 开发人员,它仅在 API 19 中引入,因此请确保您在 KitKat 或更高版本上运行,否则您将收到 NoClassDefFoundError。跨度> 最佳答案 IMO,尽管作为示例,我宁愿选择 JDK7 java.util.Objects.hash(...) 方法而不是 guava com.google.common.base.Objects.hashCode(...) 方法。我认为大多数人会选择标准库而不是额外的依赖项。 如果有两个或更多参数并且其中任何一个是数组,则结果可能不是您所期望的,因为数组的hashCode() 只是它的java.lang.System.identityHashCode(...)【参考方案17】:

虽然这与Android documentation (Wayback Machine) 和My own code on Github 相关联,但它通常适用于Java。我的答案是对dmeister's Answer 的扩展,代码更易于阅读和理解。

@Override 
public int hashCode() 

    // Start with a non-zero constant. Prime is preferred
    int result = 17;

    // Include a hash for each field.

    // Primatives

    result = 31 * result + (booleanField ? 1 : 0);                   // 1 bit   » 32-bit

    result = 31 * result + byteField;                                // 8 bits  » 32-bit 
    result = 31 * result + charField;                                // 16 bits » 32-bit
    result = 31 * result + shortField;                               // 16 bits » 32-bit
    result = 31 * result + intField;                                 // 32 bits » 32-bit

    result = 31 * result + (int)(longField ^ (longField >>> 32));    // 64 bits » 32-bit

    result = 31 * result + Float.floatToIntBits(floatField);         // 32 bits » 32-bit

    long doubleFieldBits = Double.doubleToLongBits(doubleField);     // 64 bits (double) » 64-bit (long) » 32-bit (int)
    result = 31 * result + (int)(doubleFieldBits ^ (doubleFieldBits >>> 32));

    // Objects

    result = 31 * result + Arrays.hashCode(arrayField);              // var bits » 32-bit

    result = 31 * result + referenceField.hashCode();                // var bits » 32-bit (non-nullable)   
    result = 31 * result +                                           // var bits » 32-bit (nullable)   
        (nullableReferenceField == null
            ? 0
            : nullableReferenceField.hashCode());

    return result;


编辑

通常,当您覆盖hashcode(...) 时,您还希望覆盖equals(...)。所以对于那些将要或已经实现equals 的人来说,这里有一个很好的参考from my Github...

@Override
public boolean equals(Object o) 

    // Optimization (not required).
    if (this == o) 
        return true;
    

    // Return false if the other object has the wrong type, interface, or is null.
    if (!(o instanceof MyType)) 
        return false;
    

    MyType lhs = (MyType) o; // lhs means "left hand side"

            // Primitive fields
    return     booleanField == lhs.booleanField
            && byteField    == lhs.byteField
            && charField    == lhs.charField
            && shortField   == lhs.shortField
            && intField     == lhs.intField
            && longField    == lhs.longField
            && floatField   == lhs.floatField
            && doubleField  == lhs.doubleField

            // Arrays

            && Arrays.equals(arrayField, lhs.arrayField)

            // Objects

            && referenceField.equals(lhs.referenceField)
            && (nullableReferenceField == null
                        ? lhs.nullableReferenceField == null
                        : nullableReferenceField.equals(lhs.nullableReferenceField));

【讨论】:

Android 文档现在不再包含上述代码,所以这里是来自Wayback Machine - Android Documentation (Feb 07, 2015)的缓存版本【参考方案18】:

我在 Arrays.deepHashCode(...) 周围使用了一个小型包装器,因为它可以正确处理作为参数提供的数组

public static int hash(final Object... objects) 
    return Arrays.deepHashCode(objects);

【讨论】:

【参考方案19】:

这是另一个包含超类逻辑的 JDK 1.7+ 方法演示。我认为它非常方便使用 Object class hashCode() 计算,纯 JDK 依赖并且没有额外的手动工作。请注意Objects.hash() 可以容忍空值。

我没有包含任何 equals() 实现,但实际上你当然会需要它。

import java.util.Objects;

public class Demo 

    public static class A 

        private final String param1;

        public A(final String param1) 
            this.param1 = param1;
        

        @Override
        public int hashCode() 
            return Objects.hash(
                super.hashCode(),
                this.param1);
        

    

    public static class B extends A 

        private final String param2;
        private final String param3;

        public B(
            final String param1,
            final String param2,
            final String param3) 

            super(param1);
            this.param2 = param2;
            this.param3 = param3;
        

        @Override
        public final int hashCode() 
            return Objects.hash(
                super.hashCode(),
                this.param2,
                this.param3);
        
    

    public static void main(String [] args) 

        A a = new A("A");
        B b = new B("A", "B", "C");

        System.out.println("A: " + a.hashCode());
        System.out.println("B: " + b.hashCode());
    


【讨论】:

【参考方案20】:

标准实现很弱,使用它会导致不必要的冲突。想象一个

class ListPair 
    List<Integer> first;
    List<Integer> second;

    ListPair(List<Integer> first, List<Integer> second) 
        this.first = first;
        this.second = second;
    

    public int hashCode() 
        return Objects.hashCode(first, second);
    

    ...

现在,

new ListPair(List.of(a), List.of(b, c))

new ListPair(List.of(b), List.of(a, c))

具有相同的hashCode,即31*(a+b) + c,因为这里重用了用于List.hashCode 的乘数。显然,碰撞是不可避免的,但产生不必要的碰撞只是......不必要的。

使用31 并没有什么特别聪明的地方。乘数必须为奇数以避免丢失信息(任何偶数乘数至少会丢失最高有效位,四的倍数会丢失二,等等)。任何奇数乘数都是可用的。小的乘法器可能会导致更快的计算(JIT 可以使用移位和加法),但考虑到乘法在现代 Intel/AMD 上只有三个周期的延迟,这无关紧要。小的乘数也会导致小输入的更多冲突,这有时可能是个问题。

使用素数是没有意义的,因为素数在环 Z/(2**32) 中没有意义。

所以,我建议使用随机选择的大奇数(随意取质数)。由于 i86/amd64 CPU 可以对适合单个有符号字节的操作数使用更短的指令,因此对于像 109 这样的乘法器有微小的速度优势。为了最大限度地减少冲突,请使用 0x58a54cf5 之类的东西。

在不同的地方使用不同的乘数是有帮助的,但可能不足以证明额外的工作是合理的。

【讨论】:

以上是关于集合的 hashCode 方法的最佳实现的主要内容,如果未能解决你的问题,请参考以下文章

hashCode花式卖萌

HashCode计算

整数 hashCode 的最佳实现是啥?

java 集合中重写hashCode方法和重写equals方法啥关系?

集合框架比较两个对象是否相同(equals和hashCode方法)

Java Set集合通过重写hashCode和equals实现去重