Java中hashcode和equals性能注意点 - Shai


几周前,我 在 reddit 上遇到了这个故事, 它讨论了在 Map 中使用 URL 类作为键的问题。这归结为java.net.URL中 hashcode() 方法的实现非常缓慢,这使得此类在这种情况下无法使用。不幸的是,这是 Java API 规范的一部分,并且在不破坏向后兼容性的情况下不再可以修复。
我们能做的是理解equals和hashcode的问题。今后如何避免此类问题?

URLs Hashcode和Equals的问题是什么?
为了理解这个问题,我们来看看JavaDoc定义。

  • 比较这个URL与另一个对象是否相等。
  • 如果给定的对象不是一个URL,那么这个方法立即返回错误。
  • 如果两个URL对象具有相同的协议,引用相等的主机,在主机上具有相同的端口号,以及相同的文件和文件片段,则它们是相等的。
  • 如果两个主机的名字都能被解析为相同的IP地址,则认为是相等的;否则,如果任何一个主机的名字都不能被解析,则主机的名字必须相等,不考虑大小写;或者两个主机的名字都等于空。
  • 由于主机比较需要名称解析,因此该操作是一个阻塞性操作。
  • 因为主机比较需要名称解析,所以这个操作是一个阻塞性操作。

这可能是不清楚的。让我们用一个简单的代码块来澄清它:

System.out.println(new URL("http://localhost/").equals(new URL("http://127.0.0.1/")));
System.out.println(new URL(
"http://localhost/").hashCode() == new URL("http://127.0.0.1/").hashCode());

输出:

true
true

对于localhost来说,这可能很简单;
但如果我们比较带有域名的URL,则结果就不是相等的,因为需要进行DNS查询,我们需要做的只是调用hashcode()!

快速的变通方法
在这种情况下,一个快速的变通方法是避免使用URL。Sun公司在原始的JVM代码中深深地嵌入了这个类,但我们可以使用URI来达到大多数目的。

例如,如果我们改变上面的hashcode和equals调用,使用URI而不是URL,我们将得到这个结果。

System.out.println(new URI("http://localhost/").equals(new URI("http://127.0.0.1/")));
System.out.println(new URI(
"http://localhost/").hashCode() == new URI("http://127.0.0.1/").hashCode());

我们将得到两个语句的错误。虽然这对某些用例来说可能是有问题的,但在性能上是有很大差别的。

一个更大的陷阱
如果我们所使用的map键都是字符串,我们就不会有事。这种错误会在我们使用这些方法的所有地方发生。

  • Sets
  • Maps
  • Storage
  • 业务逻辑

但它更深层次。当我们用自己的Hashcode和Equals逻辑来编写自己的类时,我们经常会被糟糕的代码所欺骗。

Hashcode方法的一个小的性能缺陷或一个过于简单的版本可能会导致重大的性能缺陷,而这是很难追踪的。
例如,由于Hashcode方法太慢或不正确,一个流操作需要更长的时间,这可能是一个长期的问题。

最佳Hashcode的实现
我们首先需要理解一些平常的实现代码。现在我不会展示可怕的或古老的代码。这是好的代码,但它不是最好的。

public int hashCode() {
    return Objects.hash(id, core, setting, values, sets);
}

这段代码一开始可能看起来没问题,但是。。。下面才是理想的代码。

public int hashCode() {
    return id;
}

这段代码才是快速、100%独特和正确的。除了这一点,简直没有理由做任何事情。
有一个例外,那就是id是一个对象。
在这种情况下,我们可能想用Objects.hashCode(id)来代替,这对null也有效。

Hashcode不是Equals
嗯,很明显......这是你在编写Hashcode实现时需要牢记的最重要的事情之一:
Hashcode方法必须快速执行,而且对于假的情况必须与equals一致;当然对于真的情况,这个方法可能不一定适合。

hashcode必须始终遵守这一规律:

assert(obj1.hashCode() != obj2.hashCode() && !obj1.equals(obj2));

这意味着如果Hashcode的结果是不同的,那么这些对象一定是不同的,并且一定会从equals返回false。

但是反过来就不是这样了。

if(obj1.hashCode() == obj2.hashCode()) {
    if(obj1.equals(obj2)) {
       // this can be false...
    }
}

因此:从性能角度看:一个Hashcode方法应该比equals法快得多。
它应该让我们跳过潜在的昂贵的equals计算,快速索引元素。

JPA问题
JPA开发者通常只是使用一个硬编码的hashcode 值或使用类对象hashCode().来生成哈希码()。

如果你让数据库为你生成ID,当你保存一个对象时,它将不再等同于源对象......
一个解决方案是使用@NaturalId注解和数据类型。但这需要改变数据模型。
不幸的是,对于实体类来说,没有像样的解决方法。

事实上,我推测JPA开发者在使用Lombok时遇到的很多问题都是因为它为你生成了hashcode 和 equals 方法。这些可能是有问题的。