一个可变对象的 hashCode() 有什么用处?

4
我一直在研究Java集合框架的源码,并注意到AbstractListAbstractMap将其元素的哈希值之和作为它们自己的哈希值返回。这使得List和Map实现将根据集合内容返回不同的哈希值。
我知道hashCode()方法的唯一用途是对使用哈希的数据结构中的元素进行均匀分布,例如HashSetHashMap。然而,SetMap的契约规定如下:
"如果更改对象的值以影响等价比较,则不指定集合的行为"

"如果更改对象的值以影响等价比较,则不指定Map的行为"
这些契约使得可能根据其状态返回不同哈希值的对象在集合中使用危险,并且作为映射中的键使用也存在同样的问题。这让我想到了以下问题:
1. 返回可变哈希值是否有意义? 2. 如果返回每个对象类的常数哈希值不是更好吗? 3. 除了在哈希集合中使用之外,hashCode()方法还有什么用处?

2
一个对象拥有一个不变的哈希码会违反它的契约。例如,如果你将哈希码锁定到一个值上,第一次调用时,两个不同的实例可能会有不同的哈希码,尽管它们是相等的。 - resueman
@resueman,是的,但您可以在实例化时生成它,就像对于String对象一样。 - Robert Mugattarov
1
你仍然会遇到同样的问题。具有不同哈希码的两个对象可能被修改为相等。 - resueman
@resueman,抱歉我没有表达清楚:我的意思是你需要基于最终字段生成它,这样哈希值就不会改变。 - Robert Mugattarov
4个回答

2
返回一个变量哈希值有意义吗?数据结构不知道您是否更改了键或元素的hashCode。这样做会破坏数据结构。但是,如果在将对象添加到哈希集合之前对其进行突变,然后不再更改它,那么这将起作用。
是否返回常量哈希值会更好?是的,但是该常量必须与equals一致,在这种情况下,唯一返回常量的方法是不更改用于创建hashCode()的任何字段。
除了在哈希集合中使用之外,hashCode()方法还有什么用途?它可以用于识别个别对象以进行调试/日志记录,尽管如果您正在使用内置的Object.hashCode()或System.identityHashCode(),则此方法更有用。它是一种快速检查两个对象是否为相同引用的方法。
ByteBuffer bb = ByteBuffer.allocate(1024);
ByteBuffer bb2 = ByteBuffer.allocate(1024);

useBB(bb);
useBB(bb2);

public static void useBB(ByteBuffer bb) {
    System.out.println(Thread.currentThread()+" - Using bb " + System.identityHashCode(bb));
    // do something
}

如果您看到两个线程使用相同的ByteBuffer,可能会出现问题....。
您可以在构造函数中实例化时生成它,就像对String对象所做的那样。String的hashCode是按需生成的,以减少创建不需要hashCode的字符串的开销。注意:拥有hashCode为0的String非常昂贵,除非有人试图创建hashCode为0的字符串。
同样,Object的hashCode也是按需生成的。在Oracle/OpenJDK中,它存储在头部,初始值为0(未设置),但在第一次使用时设置为1 - 2^31-1。
这个程序通过将hashCode重置为0来测试Object.hashCode()的成本。

https://github.com/peter-lawrey/Performance-Examples/blob/master/src/main/java/vanilla/java/unsafe/HashCodePerf.java

这个程序对设置Object.HashCode()的性能进行基准测试,采用了更简单的策略。

https://github.com/peter-lawrey/Performance-Examples/blob/master/src/main/java/vanilla/java/unsafe/FasterHashCodePref.java

我是说您需要根据最终字段生成它,这样哈希值就不会改变。
这并不足以保证hashCode不会改变。
class A {
    final List<String> strings = new ArrayList<>();

    public int hashCode() {
        return strings.hashCode();
    }
}

您的字段需要同时是“final”和不可变的,以确保hashCode不会改变。 但是,如果您不更改它们,它们也不会改变。

我的意思是字符串哈希是基于final字段生成的。 - Robert Mugattarov
@RobertMugattarov 是的,hashCode是基于仅在构造函数中设置的字段。 - Peter Lawrey
这绝对不是一个快速检查两个对象是否相同引用的方法。Object#hashCode()的默认行为甚至没有被API强制规定,即使它通常是地址:这通常是通过将对象的内部地址转换为整数来实现的,但这种实现技术并不是JavaTM编程语言所必需的。 即使如此,您也不能依赖它,因为如果有人覆盖了这个方法,那么它就不再表示地址了,即使您正在使用一个使用地址作为默认返回值的Java实现。 - searchengine27
具有相同hashCode并不意味着您拥有相同的对象的示例 https://twitter.com/PeterLawrey/status/856448017376923648 所有这些字符串的hashCode都为0 - Peter Lawrey
1
即使identityHashCode也不能保证是唯一的。 - Holger

1
有必要返回变量哈希值。一个类的 hashCode() 方法应该与其 equals() 方法保持一致。如果一个类通过可变状态定义实例相等,则它的 hashCode() 也应该基于同样的可变状态定义。
这样的对象很少适用于基于哈希的集合,但如果仔细避免在它们在内部时改变它们,它们可以用于这样的集合。
实际上,Object.hashCode() 做到了这一点,假设你的意思是每个实例都是常量。如果你不重写 equals(),那么没有理由重写那个常量哈希值,但如果你确实重写了 equals(),那么你应该提供一个一致的 hashCode() 实现。
除了用于哈希集合之外,hashCode() 方法还可以有什么用处?
如果一个类缓存了其计算出的哈希码,则比较实例的哈希码可以作为一种便宜的“可能相等”测试,允许在许多情况下绕过更昂贵的精确相等性测试,即使涉及的对象实际上是不相等的。
同样地,如果哈希码以可变状态为基础定义,则哈希码的变化是状态已经改变的积极指示(尽管反之不成立)。

0
  1. 返回变量哈希值有意义吗?

不一定。但是,如果用户在将列表用作哈希表中的键时小心不修改列表及其内容,则哈希值不会变化,哈希表操作将正常工作。这显然很危险,因为用户需要确保在需要时哈希代码不会更改,但这是可能的。

  1. 返回一个常量哈希值不是更好吗?

不一定。虽然这确保哈希码值始终遵守其契约(即2个相等的对象始终返回相同的哈希码),即使列表发生更改...但如果由于常量哈希码而将所有元素放入同一个桶中,那么使用哈希表的性能将非常糟糕,在这种情况下最好使用另一种集合类型。

  1. 除了在哈希集合中使用之外,hashCode()方法还有什么用处?

实际上没有其他用途。有些人创造性地使用哈希码值,但它们通常是无效的,并且基于对哈希码含义的错误理解。


0
如果在对象是集合元素的情况下,以影响等式比较的方式更改对象的值,则不会指定集合的行为。
如果我们将这句话翻译成实际意义,事情就变得更清晰了。简单来说:
java.util.Set 不应用于存储具有可变标识的对象。在此用法中,两个对象 A 和 B 对于目的而言是相同的,如果 A.hashCode() == B.hashCode() 并且 A.equals(B) 和 B.equals(A) 都返回 true。
因此,对于以下问题的答案是:
1. 没有返回变量哈希码的意义。 2. 哈希码必须在相等的对象之间保持恒定。每个类的常数哈希码不会破坏契约,但对性能没有用处。 3. 在哈希集合之外,重写 hashCode() 方法没有用处。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接