Objects.hash()与Objects.hashCode(),需要澄清的区别。

77

自Java 7以来,我们拥有了

o.hashCode();
Objects.hashCode(o);
Objects.hash(o);

前两个与空值检查大致相同,但最后一个是什么?

当提供单个对象引用时,返回的值不等于该对象引用的哈希码。

为什么会这样?我的意思是,我们不需要三个做同样事情的方法,我明白这一点,但为什么我们需要Objects.hash()呢?在何时选择使用其中之一?

4个回答

99

请参考hashCodehash 文档。其中,hash 接受参数为 Object... 类型,而 hashCode 接受参数为 Object 类型。以下是示例:

@Override public int hashCode() {
    return Objects.hash(x, y, z);
}
  • 在需要对一系列对象进行哈希的情况下,例如定义自己的hashCode方法,并且想要用于组成对象标识的多个值的简单哈希码时,应使用Objects.hash(Object... values)
  • 当您需要单个对象的哈希值而不是抛出异常时,请使用Objects.hashCode(Object o)
  • 当您需要单个对象的哈希值并且如果对象为null则会引发异常时,请使用Object::hashCode()

请注意,hash(o)hashCode(o)返回的值可能不同!如果只针对单个对象进行操作,则应该使用hashCode


3
hash(o)hashCode(o)不返回相同值的情况是什么?在hash()文档中指出:警告: 如果提供单个对象引用,则返回的值与该对象引用的哈希码不相等。,但我仍在努力弄清原因。 - Craig Otis
13
Objects.hash 只是调用了 Arrays.hashCode 方法 (http://hg.openjdk.java.net/jdk10/jdk10/jdk/file/777356696811/src/java.base/share/classes/java/util/Objects.java#l145),而一个单独对象的 hashCode 值与仅包含该单个对象的数组的 hashCode 值是不同的 (http://hg.openjdk.java.net/jdk10/jdk10/jdk/file/777356696811/src/java.base/share/classes/java/util/Arrays.java#l4677)。 - Peter Hull
谢谢@PeterHull,现在我明白了。 - Craig Otis

46

Objects.hashCode

实用方法Objects.hashCode( Object o )只是在传递的对象上调用hashCode方法。
容忍NULL
那么为什么要发明或使用这种方法呢?为什么不直接调用对象的hashCode方法呢?
这个方法有一个好处:NULL ➙ 0。该实用程序方法容忍空值。
如果您调用Objects.hashCode( myObject ),其中myObjectNULL,则会返回零(0)。
相反,当myObjectNULL时调用myObject.hashCode()会抛出NullPointerException异常。
是否容忍空值取决于您在特定情况下的判断。

Objects.hash

实用方法Objects.hash( Object o , … )有不同的用途。该方法有两个阶段:
  • 对每个传递的对象调用.hashCode,并收集每个结果。
  • 计算收集结果的哈希值。

哈希的哈希

如果您传递单个对象Objects.hash( myObject ),首先会调用myObject.hashCode并收集结果,然后计算单项收集的哈希值。因此,您最终得到一个哈希的哈希。

当对单个对象进行哈希处理时,了解Objects.hashCode( myObject )返回的结果与Objects.hash( myObject )返回的结果不同至关重要。实际上,第二个返回的是对第一个结果的哈希。

在实践中很烦人

这两种Objects方法采取的方法逻辑本身是有意义的。

不幸的是,在实践中,对于我们在日常使用中尝试在POJO上编写代码以覆盖hashCode和相应的equals的人来说,我们必须三思而后行,以决定调用哪个方法。

  • 如果你的hashCode(和equals)重写是基于类的单个成员,请使用Objects.hashCode(member)
  • 如果你的hashCode(和equals)重写是基于类的多个属性,请使用Objects.hash(memberA, memberB, memberC)

单个成员,不容忍NULL

@Override
public int hashCode() {
    return this.member.hashCode() ;  // Throws NullPointerException if member variable is null.
}

单个成员,容忍一个空值

@Override
public int hashCode() {
    return Objects.hashCode( this.member ) ;  // Returns zero (0) if `this.member` is NULL, rather than throwing exception.
}

多成员,容忍NULL

@Override
public int hashCode() {
    return Objects.hash( this.memberA , this.memberB , this.memberC  ) ;  // Hashes the result of all the passed objects’ individual hash codes.  
}

示例

我们可以非常简单地测试这些不同的方法。

UUID

让我们以UUID对象为例。UUID(通用唯一识别码)是一个128位的值,其中某些位具有特定的语义。

OpenJDK实现将UUID内部表示为一对64位long整数数字。

同样的实现覆盖了Object::equalsObject::hashCode,以查看存储在这一对长整型中的数据。以下是这两种方法的源代码

public boolean equals(Object obj) {
    if ((null == obj) || (obj.getClass() != UUID.class))
        return false;
    UUID id = (UUID)obj;
    return (mostSigBits == id.mostSigBits &&
            leastSigBits == id.leastSigBits);
}

public int hashCode() {
    long hilo = mostSigBits ^ leastSigBits;
    return ((int)(hilo >> 32)) ^ (int) hilo;
}

示例代码

实例化我们的 UUID 对象。

UUID uuid = UUID.randomUUID();

计算我们的哈希值。

int hash1 = uuid.hashCode();
int hash2 = Objects.hashCode( uuid );  // Result matches line above.

int hash3 = Objects.hash( uuid );  // Returns a hash of a hash.
int hash4 = Objects.hash( uuid.hashCode() ); // Result matches line above.

输出到控制台。

System.out.println( "uuid.toString(): " + uuid.toString() );
System.out.println( " 1/2 = " + hash1 + " | " + hash2 );
System.out.println( " 3/4 = " + hash3 + " | " + hash4 );

在IdeOne.com上查看此代码的实时运行

uuid.toString(): 401d88ff-c75d-4607-bb89-1f7a2c6963e1

1/2 = 278966883 | 278966883

3/4 = 278966914 | 278966914


15

默认情况下,Object类的hashCode()方法返回对象的内存地址。因此,如果您有以下类:

class Car {
    String make;
    String model;
    int year;

    public Car(String make, String model, int year) {
        this.make = make;
        this.model = model;
        this.year = year;
    }
} 

然后创建两个对象:

Car car1 = new Car("Toyota", "Corolla", 2010);
Car car2 = new Car("Toyota", "Corolla", 2010);

car1.hashCode()将与car2.hashCode()不同,因为每个对象都有不同的内存地址。

如果您希望car1和car2返回相同的哈希码,那么您应该覆盖Car类的默认Object hashCode()方法,如下所示:

@Override
public int hashCode() {
    Object[] x = {model, make, Integer.valueOf(year)};
    int hashArray = Arrays.hashCode(x);
    return hashArray;
}

这将使得car1.hashCode()等于car2.hashCode(),因为String.hashCode()是基于字符串内容计算hashCode的,而Integer.hashCode()将返回整数值本身。

在Java 7中,你可以使用Objects.hash(Object... values)。因此我们的新Car hashCode()将如下所示:

@Override
public int hashCode() {
    return Objects.hash(model, make, year);
}

Objects.hash(Object... values)会为您调用Arrays.hashCode。

最后,Objects.hashCode(Object o)将执行空值检查。如果该对象为空,则返回0。否则,它将调用对象的hashCode()方法。


2
默认的 hashCode() 已经有大约20年没有返回内存地址了。它实际上无法这样做。GC会移动对象。所谓的内存地址是一个不可移动的句柄。 - user207421
1
根据Java文档https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#hashCodehashCode“通常通过将对象的内部地址转换为整数来实现,但是Java™编程语言不要求使用此实现技术。” - ezzadeen
或许更好的方式是将“返回内存地址”改为“返回类似于内存地址的内容”。 - Basil Bourque

0

https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/java/util/Objects.java

public static int hashCode(Object o) {
    return o != null ? o.hashCode() : 0;
}

public static int hash(Object... values) {
    return Arrays.hashCode(values);
}

谜团解开了 :D
为了更清晰,Arrays.hashCode 对不同类型有不同的实现。例如,Object 的实现如下:
public static int hashCode(Object a[]) {
    if (a == null)
        return 0;

    int result = 1;

    for (Object element : a)
        result = 31 * result + (element == null ? 0 : element.hashCode());

    return result;
}

由于`a`是`Object`类型,所以`element.hashCode()`将调用`Object.hashCode`,这与上面使用的`Objects.hashCode()`相同。
但是底层到底是什么呢?很难回答,因为这映射到了`JVM_IHashCode`,它在https://github.com/JetBrains/jdk8u_hotspot/blob/master/src/share/vm/runtime/synchronizer.cpp中定义为`ObjectSynchronizer::FastHashCode`(抱歉,我在Java版本和C++之间跳来跳去)。
现在你知道了这两个JAVA方法的C++实现了,至少对于OpenJDK来说。

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