【问题标题】:Designing hashCode method Java设计 hashCode 方法 Java
【发布时间】:2016-03-05 00:51:07
【问题描述】:

我正在学习 Item 9, Effective Java [Always override hashcode() when you override equals]。

我对作者提出的观点有一些疑问:

  1. 作者说:

在步骤 1 中使用了一个非零初始值,因此哈希值将受到以下因素的影响 在步骤 2.a 中计算的哈希值为零的初始字段。如果使用零 作为步骤 1 中的初始值,整体哈希值将不受任何影响 这样的初始场,可能会增加碰撞。值 17 是任意的。

步骤 2.a 是:

对于您的对象中的每个重要字段 f(每个字段都纳入 通过 equals 方法计算帐户,即),请执行以下操作:计算 字段的 int 哈希码 c:

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

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

三。如果字段是 long ,则计算 (int) (f^ (f >>> 32)) 。

四。如果字段是 float ,则计算 Float.floatToIntBits(f) 。

v.如果该字段是 double ,则计算 Double.doubleToLongBits(f) ,并且 然后按照步骤 2.a.iii 对结果进行哈希运算。

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

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

假设计算结果为:

result = 31 * result + areaCode;      
result = 31 * result + prefix;
result = 31 * result + lineNumber;

如果结果的初始值为 0 并且上面所有给定的字段都是 0,则结​​果将保持为 0。 但是,即使结果最初不是 0,每次初始字段为 0 时,结果都会等于相同的常数,这将是: 31*(31*(31*17))。该值将如何帮助减少碰撞?

  1. 最后一段指出:

Java 平台库中的许多类,例如 String 、 Integer 和 Date ,在它们的规范中包含返回的确切值 通过他们的 hashCode 方法作为实例值的函数。这是 通常不是一个好主意,因为它严重限制了你的能力 在未来的版本中改进散列函数。如果您留下详细信息 散列函数未指定且发现缺陷或更好的散列 发现函数,您可以在后续的 发布,确信没有客户依赖返回的确切值 通过哈希函数。

他说hashCode返回的确切值是实例值的函数是什么意思?

提前感谢您的帮助。

【问题讨论】:

  • 据我所知,“hashCode 方法作为实例值的函数”意味着生成的哈希码将取决于对象或实例的变量值。因此,具有相同值的两个对象可能会生成相同的 hashCode。这可能会导致碰撞。然后,需要实现一个更好的 hashCode 算法来消除冲突。
  • 如果你查看 String hashcode() 生成方法的文档,他们实现了一个基于字符串中字符的公式。
  • 这完全取决于您对哈希码的实际操作。最终,它是实现工作、运行时成本和您可以容忍的理想冲突数量之间的权衡。没有“更好”。如有疑问,请使用其中一种方便的哈希码构建器,例如番石榴,或者如果你真的不在乎,就返回 -1。或者,可以考虑使用 MD5 或 Murmur 在没有太多计算开销的情况下获得适当的值分布。我发现 String.hashCode() 和 HashMaps 很难扩展到超过几万个条目。

标签: java hashcode effective-java


【解决方案1】:

首先我想说一件很重要的事,但往往没有明确表述:

为大多数情况实施哈希码并不重要。它仅分解为性能问题。因此,如果您对哈希码和对象身份有疑问,只需返回 -1。您的性能会很差,但会实现可靠且正确的实施。但是,除非您拥有数千个使用哈希码的对象,否则您不会意识到性能不佳。顺便说一句:“碰撞”在哈希码的上下文中看起来像是一个重要的词。是的,但前提是性能确实是一个问题。哈希码值的“冲突”并不意味着您的程序无法正常工作。这意味着您的程序可能运行速度较慢。因为使用映射的键访问将导致对具有相同哈希码的对象进行顺序迭代。在高性能环境中,这可能是一个问题。大多数情况下不是。

但是,如果您覆盖哈希码,那么重要的是:您必须正确地实现它。所以定义应该总是满足:如果equals返回true,hashcode应该返回相同的值。

另外一件事:虽然您不小心遇到了问题,但在非不可变值上计算哈希码是个坏主意。这是因为一旦使用了哈希码,对象就会被放置在“地图”中的一个特殊位置。如果哈希码所依赖的值发生变化,则该对象可能会丢失或变得难以访问。这会影响程序的正确性。

结论:仅当您确实需要性能时才使用哈希码。然后你应该确保你正确地应用它。这里很容易出错,但这些错误可能是最难识别的。

【讨论】:

  • 虽然您确实不应该担心创建最先进的哈希函数,除非您需要它,而且哈希函数通常不是特别重要,但您永远不应该只返回一些常量价值。如果你想要一个“无需努力”的哈希函数,只需使用Objects.hash(field1, field2, field3)
  • 至于您关于仅依赖不可变字段的其他建议,我觉得这是错误的。就个人而言,如果我在HashMap 中使用对象作为键,对其进行变异,并且仍然可以从HashMap 中获取值,我会更加困惑/担心;例如,我想知道是否发生了某些事情并且实际上并没有发生突变。
  • 当您的哈希码损坏并且对象在哈希树中的位置依赖于它时,返回 -1 是一个可靠的后备解决方案。如果它使用可变字段,则您的哈希码方法已损坏。自己尝试一下:将对象放入 hashmap(作为键)或 Hashset 中,并对 hashcode 方法中使用的字段进行变异。该对象变得无法访问,因为该对象现在的状态与之前放入的存储桶不对应。顺便说一句,你没有解释为什么返回 -1 永远不应该被返回,因为我明确指出它是一个在遇到问题时的后备。
  • 没错。您可能熟悉的每个对象都将变得无法访问;这是预期的行为。
  • 您不应该使用病态哈希函数,因为根本没有任何好处。变异键通常是一个坏主意,因此破坏基于散列的容器的性能只是为了让自己可能变异它们根本不是一个好的权衡。至于后备评论;满足hashCode() 合约并不难。
【解决方案2】:

该值如何有助于减少碰撞?

哈希冲突主要是通过在整个哈希范围(这里是整数类型)上的良好分布来实现的。

通过将 0 定义为计算散列结果的初始值,您可以在小范围内获得某种限制分布。有细微差别的对象(可能仅在某些领域)会产生彼此相距不远的哈希码。这使得哈希冲突的可能性更大。

通过定义一个非零的初始值,您可以简单地增加计算哈希码之间的差距,这些对象之间的差异只有很小的差异。因此,您可以更好地利用哈希范围并有效地降低哈希冲突的可能性。

他说hashCode返回的确切值是实例值的函数是什么意思?

这只是意味着您应该使用对象的值(即其字段的值)来计算哈希码。你已经在你的例子中做到了,我认为你已经隐含地理解了它。

但是:Joshua Bloch 打算在这段话中说点别的:他想警告您没有记录如何计算哈希码的确切函数。如果您这样做,您将限制自己在未来版本中无法再更改实现,因为某些用户可能期望特定的实现,并且您会根据您的情况破坏一些代码。

【讨论】:

  • 当你说'通过定义一个非零初始值,你只是增加了计算哈希码之间的差距,这些对象只有很小的差异......'你有没有机会编辑你的问题是一个简单的例子,什么时候会出现这种情况?我正在努力看看它是如何工作的,谢谢:-)
【解决方案3】:

对于您的第一个问题,如果 2 个对象相等,则它们应该返回相同的哈希值,这就是为什么在重写 equals 方法时重写 hash 方法是个好主意的原因。它不能避免相等对象的碰撞,但它确实降低了当对象相等时发生碰撞的可能性,这一点更重要,因为我们希望能够尽快找到唯一的对象。

关于你的第二个问题,我不假装在设计哈希码方面有太多经验,但我相信他的意思是某些对象可能只返回一个哈希值(例如单例)。

他是说将该值放入文档中是一种不好的做法,因为您可能希望稍后更改散列函数,或者散列函数中的其他变量可能会在以后更改以更改返回值。

无论是指定返回值还是依赖指定的返回值都是一个坏主意。

【讨论】:

  • 我的第一个问题是关于结果的初始值不是 0?这将如何帮助减少碰撞?我知道当我覆盖equals时我必须覆盖hashCode。这更多是关于哈希方法的设计。
  • 啊好吧我误会了。不应该使用的不仅仅是 0,它是任何非素数。 (大素数是最好的散列算法。)这是因为对素数的操作不太可能最终得到类似的结果。 0 只是最差的非素数,因为每个数字的结果通常都是相同的(即 10 * 0 是 0,15 * 0 也是)。
【解决方案4】:

Effective Java 中的 hashCode 实现特别指示您为 result 的初始值选择一个非零值。至于您的第二个问题,当用于对象的相等比较的内部状态相同时,hashCode supposed 会产生相同的值。因此,当实例变量全为零时,您将获得相同的值这一事实满足了 hashCode 的约定。请注意,整个子标题是“覆盖等于时始终覆盖 hashCode”。

【讨论】:

    【解决方案5】:

    看这个例子:

        String a = "Abc";
        String b = "Abc";
        String c = "Pqr";
        System.out.println(" "+a.hashCode()+" "+b.hashCode()+" "+c.hashCode());
    

    输出: 65602 65602 80497

    这清楚地表明字符串的 hashCode() 取决于值。

    从 hashCode() 文档中摘录:
    int java.lang.String.hashCode()

    返回此字符串的哈希码。 String 对象的哈希码计算为

    s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

    使用int算术,其中s[i]是字符串的第i个字符,n是字符串的长度,^表示求幂。 (空字符串的哈希值为零。)

    【讨论】:

    • 你的观察对我来说很有意义。我的推断是,在上述类的情况下,如果 hashCode 方法依赖于实例变量,那么在未来版本中重写该 hash 方法可能很难而不破坏以前版本的代码。例如,如果将来的 hashCode 定义考虑了其他一些参数,则逻辑上相等的 2 个字符串(当它们的哈希码使用其中包含的字符计算时)可能会变得不相等。在某种程度上,hashCode 和实例之间存在紧密耦合,这总是不好的。
    • 但我还是不清楚第一部分。您能否说明“结果”的初始值不被用作“0”的原因?它将如何帮助减少碰撞?
    猜你喜欢
    • 2012-03-27
    • 2014-10-17
    • 1970-01-01
    • 2020-06-09
    • 1970-01-01
    • 1970-01-01
    • 2013-07-28
    • 2018-03-27
    • 2021-05-20
    相关资源
    最近更新 更多