集合的hashCode方法的最佳实现


Answers:


438

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

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

简短版

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

  2. 对于在方法中测试的每个字段 f,通过以下equals()方式计算哈希码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)和处理返回值,就像每个长值一样;
    • 如果字段f是一个对象:使用hashCode()方法的结果或0 f == null;
    • 如果字段f是一个数组:将每个字段视为单独的元素,并以递归方式计算哈希值,然后组合值,如下所述。
  3. 将哈希值cresult

    result = 37 * result + c
  4. 返回 result

对于大多数使用情况,这应该导致哈希值的正确分配。


45
是的,我特别好奇数字37的来源。
Kip

17
我使用了乔什·布洛赫(Josh Bloch)的“有效Java”一书中的第8条。
dmeister 2010年

39
@dma_k使用质数和此答案中描述的方法的原因是确保所计算的哈希码将是唯一的。使用非质数时,您无法保证。选择哪个素数并不重要,数字37并没有什么神奇的(太糟糕的42并不是素数,是吗?)
Simon Forsberg

34
@SimonAndréForsberg好吧,计算得出的哈希码不能总是唯一的:)是哈希码。但是我有个主意:素数只有一个乘数,而非素数至少有两个。这为乘法运算符创建了一个额外的组合,以产生相同的哈希,即引起冲突。
dma_k 2013年


140

如果您对dmeister推荐的有效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)中的标准库,但工作方式相同。


8
除非有充分的理由不使用它们,否则无论如何都应绝对使用它们。(将其构造得更坚固,因为它应该被IMHO所表述。)使用标准实现/库的典型论点适用(最佳实践,经过良好测试,不太容易出错等)。
Kissaki

7
@ justin.hughey,您似乎很困惑。您应该重写的唯一情况hashCode是,如果您有一个custom equals,这正是这些库方法设计的目的。文档非常明确了它们与equals。库实现并不能声称免除您知道正确hashCode实现的特征是什么-这些库使您可以在大多数被覆盖的情况下更轻松地实现这种一致的实现equals
bacar 2014年

6
对于所有正在研究java.util.Objects类的Android开发人员,它仅是在API 19中引入的,因此请确保您在KitKat或更高版本上运行,否则会出现NoClassDefFoundError。
安德鲁·凯利

3
最佳答案IMO,尽管通过示例的方式,我宁愿选择JDK7 java.util.Objects.hash(...)方法而不是guava com.google.common.base.Objects.hashCode(...)方法。我认为大多数人会选择标准库而不是额外的依赖项。
Malte Skoruppa 2015年

2
如果有两个或两个以上参数,并且其中任何一个为数组,则结果可能不是您所期望的,因为hashCode()对于数组来说,它只是它的java.lang.System.identityHashCode(...)
starikoff 2015年

59

最好使用Eclipse提供的功能,该功能做得很好,您可以投入精力和精力来开发业务逻辑。


4
+1一个好的实用解决方案。dmeister的解决方案更全面,但是当我尝试自己编写哈希码时,我倾向于忘记处理null。
Quantum7 2011年

1
+1同意Quantum7,但我想了解Eclipse生成的实现在做什么以及从何处获取实现细节也非常好。
jwir3 2014年

15
抱歉,但是与“ [某些IDE]提供的功能”有关的答案通常与编程语言的上下文无关。有数十种IDE,但这不能解决问题……就是因为这更多是关于算法确定,并且直接与equals()实现相关联-IDE对此一无所知。
Darrell Teague

57

尽管这与Android文档(Wayback Machine)Github上的我自己的代码链接在一起,但它通常适用于Java。我的答案是dmeister的Answers的扩展,仅提供了易于阅读和理解的代码。

@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,这里是我的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));
}

1
Android文档现在不再包含上述代码,因此这是Wayback Machine
Christopher Rucinski

17

首先,确保equals正确实现。从IBM DeveloperWorks文章中

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

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

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

最后,一个好的哈希函数应该努力接近理想的哈希函数


11

about8.blogspot.com,您说过

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

我不同意你的看法。如果两个对象具有相同的哈希码,则不必表示它们相等。

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

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


3
如果为(A != B) and (A.hashcode() == B.hashcode()),这就是我们所说的哈希函数碰撞。这是因为哈希函数的共域始终是有限的,而其域通常不是。共域越大,冲突发生的频率就越小。良好的散列函数应针对给定的特定共域大小,针对不同的对象返回不同的散列,并且最大可能实现。但是,它很少能得到完全保证。
KrzysztofJabłoński2013年

这只是对上述Grey帖子的评论。良好的信息,但它并没有真正回答这个问题
克里斯托弗Rucinski

好的评论,但请谨慎使用术语“不同的对象”……因为equals()以及hashCode()实现不一定与OO上下文中的不同对象有关,而是通常与它们的域模型表示有关(例如,两个如果人们共享一个国家/地区代码和国家/地区ID,则可以认为他们是相同的-尽管它们在JVM中可能是两个不同的“对象”-被认为是“相等”并具有给定的hashCode)...
Darrell Teague

7

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

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

使用此函数,您可以确定要用于相等性和哈希码计算的字段,然后Eclipse生成相应的方法。


7

有一个很好的贯彻实施有效的Javahashcode()equals()逻辑的Apache Commons Lang中。检出HashCodeBuilderEqualsBuilder


1
该API的缺点是,每次调用equals和hashcode时(除非对象是不可变的并且您预先计算了哈希值),您都要付出对象构造的费用,在某些情况下这可能很多。
詹姆斯·麦克马洪

直到最近,这是我最喜欢的方法。我在使用SharedKey OneToOne关联条件时遇到了StackOverFlowError。此外,Objects类从Java7开始提供hash(Object ..args)equals()方法。对于使用jdk 1.7+的任何应用程序,建议使用这些工具
Diablo

@Diablo我想,您的问题是对象图中的一个循环,然后您对大多数实现都不满意,因为您需要忽略一些引用或中断循环(强制IdentityHashMap)。FWIW我使用基于id的hashCode并等于所有实体。
maaartinus

6

简要说明一下完成其他更详细的答案(根据代码):

如果我考虑如何在Java中创建哈希表(尤其是jGuru FAQ条目)问题,我认为可以判断哈希码的其他一些标准是:

  • 同步(算法是否支持并发访问)?
  • 故障安全迭代(算法是否检测到在迭代过程中发生更改的集合)
  • 空值(哈希码是否支持集合中的空值)

4

如果我正确理解了您的问题,则您有一个自定义集合类(即从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);
}

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,这很丑陋。)


3
该错误在about8.blogspot.com的较长答案中-从字符串的连接中获取哈希码会给您提供哈希函数,该哈希函数对于加总相同字符串的任何字符串组合都是相同的。
SquareCog

1
因此,这是元讨论,根本与问题无关吗?;-)
嬉皮

1
这是对提出的答案的更正,它具有相当大的缺陷。
SquareCog

这是一个非常有限的实施方式
Christopher Rucinski 2015年

您的实现避免了该问题,并引入了另一个问题;交换foobar导致相同hashCode。您的toStringAFAIK无法编译,如果可以编译,那么效率很低。诸如此类的东西109 * getFoo().hashCode() + 57 * getBar().hashCode()更快,更简单,并且不会产生不必要的冲突。
maaartinus

2

正如您特别要求的集合一样,我想添加一个其他答案尚未提及的方面:HashMap不会期望将其键添加到集合后更改其哈希码。会打败整个目标...



2

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

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


1

我更喜欢使用Objects类的Google收藏库中的 fromm Google实用工具方法来帮助我保持代码的清洁。通常equalshashcode方法都是从IDE的模板中制作出来的,因此它们不干净。


1

这是另一个介绍了超类逻辑的JDK 1.7+方法演示。我认为,使用Object类的hashCode(),纯JDK依赖关系和无需任何额外的手工工作,这非常方便。请注意,它Objects.hash()是null容忍的。

我没有任何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());
    }

}

1

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

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之类的方法。

在不同的地方使用不同的乘法器会有所帮助,但可能不足以证明其他工作的合理性。



-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()应该返回不同的值。


1
像SquareCog一样已经注意到。如果从两个字符串连接产生一次哈希码这是非常容易产生冲突的群众:("abc"+""=="ab"+"c"=="a"+"bc"==""+"abc")。这是严重的缺陷。最好评估两个字段的哈希码,然后计算它们的线性组合(最好使用素数作为系数)。
克日什托夫·雅布隆斯基

@KrzysztofJabłoński对。此外,交换foo以及bar产生不必要的碰撞,太。
maaartinus
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.