设计 hashCode 方法 Java

Designing hashCode method Java

我正在学习第 9 项,有效 Java [当你覆盖 equals 时总是覆盖 hashcode()]。

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

  1. 作者说:

A nonzero initial value is used in step 1 so the hash value will be affected by initial fields whose hash value, as computed in step 2.a, is zero. If zero were used as the initial value in step 1, the overall hash value would be unaffected by any such initial fields, which could increase collisions. The value 17 is arbitrary.

步骤 2.a 是:

For each significant field f in your object (each field taken into account by the equals method, that is), do the following: 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.

假设结果计算为:

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

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

  1. 最后一段指出:

Many classes in the Java platform libraries, such as String , Integer , and Date , include in their specifications the exact value returned by their hashCode method as a function of the instance value. This is generally not a good idea, as it severely limits your ability to improve the hash function in future releases. If you leave the details of a hash function unspecified and a flaw is found or a better hash function discovered, you can change the hash function in a subsequent release, confident that no clients depend on the exact values returned by the hash function.

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

在此先感谢您的帮助。

看这个例子:

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

输出: 65602 65602 80497

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

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

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

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

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

Effective Java 中的 hashCode 实现特别指示您选择一个 non-zero 值作为结果的初始值。至于你的第二个问题,hashCode是应该当用于object的相等比较的内部状态相同时产生相同的值。因此,当实例变量全为零时您将获得相同的值这一事实满足了 hashCode 的约定。请注意,整个副标题是 'Always override hashCode when you override equals'。

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

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

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

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

这个值如何帮助减少碰撞?

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

通过将 0 定义为计算哈希结果的初始值,您可以在一个小范围内限制分布。略有不同的对象——可能仅在某些领域——产生彼此相距不远的散列码。这使得散列冲突更有可能发生。

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

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

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

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

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

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

但是,如果您覆盖哈希码,重要的是:您必须正确地实施它。所以应该始终满足定义:如果等于 return 为真,哈希码应该 return 相同的值。

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

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