几周前,我 在 reddit 上遇到了这个故事, 它讨论了在 Map 中使用 URL 类作为键的问题。这归结为 java.net.URL中 hashcode() 方法的实现非常缓慢, 这使得此类在这种情况下无法使用。不幸的是,这是 Java API 规范的一部分,并且在不破坏向后兼容性的情况下不再可以修复。
我们能做的是理解equals和hashcode的问题。今后如何避免此类问题?
URL Hashcode 和 Equals 有什么问题?
为了理解这一点,让我们看一下 hashcode 和 equals 的 JavaDoc:
将此 URL 与另一个对象进行比较是否相等。
如果给定对象不是 URL,则此方法立即返回 false。
如果两个 URL 对象具有相同的协议、 引用等效的 hosts 、在主机上具有相同的端口号以及相同的文件和文件的片段,则它们是相等的。
如果两个主机名都可以解析为相同的IP地址,则认为两个主机是等效的;否则,如果任何一个主机名都无法解析,则主机名必须相等,不区分大小写;或两个主机名都等于 null。
由于主机比较需要名称解析,因此该操作是阻塞操作。
由于主机比较需要名称解析,因此此操作是阻塞操作。”
这可能不清楚。让我们用一个简单的代码块来澄清它:
<span style="color:#444444"><span style="background-color:#f6f6f6">系统。<span style="color:#333333"><strong>out</strong></span> .println( <span style="color:#333333"><strong>new</strong></span> URL( <span style="color:#880000">"http://localhost/"</span> ) <span style="color:#333333"><strong>.equals</strong></span> ( <span style="color:#333333"><strong>new</strong></span> URL ( <span style="color:#880000">"http://127.0.0.1/"</span> )));
系统。<span style="color:#333333"><strong>out</strong></span> .println(<span style="color:#333333"><strong>新</strong></span>URL( <span style="color:#880000">"http://localhost/"</span> ).hashCode() ==<span style="color:#333333"><strong>新</strong></span>URL( <span style="color:#880000">"http://127.0.0.1/"</span> ).hashCode());</span></span>
将打印出:
<span style="color:#444444"><span style="background-color:#f6f6f6"><span style="color:#333333"><strong>真的</strong></span>
<span style="color:#333333"><strong>真的</strong></span></span></span>
使用 localhost 这可能非常简单,但是如果我们比较域并且字符串不相同(它们通常不完全相同),我们需要进行 DNS 查找。我们需要为 hashcode() 调用这样做!
快速解决方法
这种情况的快速解决方法是避免使用 URL。Sun 将该类深深嵌入到原始 JVM 代码中,但我们可以将 URI 用于大多数目的。
例如,如果我们将上面的 hashcode 和 equals 调用更改为使用 URI 而不是 URL,我们将得到以下结果:
<span style="color:#444444"><span style="background-color:#f6f6f6">系统。<span style="color:#333333"><strong>out</strong></span> .println( <span style="color:#333333"><strong>new</strong></span> URI( <span style="color:#880000">"http://localhost/"</span> ) <span style="color:#333333"><strong>.equals</strong></span> ( <span style="color:#333333"><strong>new</strong></span> URI ( <span style="color:#880000">"http://127.0.0.1/"</span> )));
系统。<span style="color:#333333"><strong>out</strong></span> .println( <span style="color:#333333"><strong>new</strong></span> URI( <span style="color:#880000">"http://localhost/"</span> ).hashCode() == <span style="color:#333333"><strong>new</strong></span> URI( <span style="color:#880000">"http://127.0.0.1/"</span> ).hashCode());</span></span>
对于这两种说法,我们都会得到错误的结果。虽然这对于某些用例可能是个问题,但在性能上却存在巨大差异。
更大的陷阱
如果我们曾经用作映射键的只是字符串,我们会很好。这种错误可能会在我们使用这些方法的每个地方袭击我们:
- 套
- 地图
- 贮存
- 商业逻辑
但它变得更深了。当使用我们自己的哈希码和 equals 逻辑编写我们自己的类时,我们经常会成为糟糕代码的牺牲品。哈希码方法的小幅性能损失或过于简单的版本可能会导致难以跟踪的重大性能损失。
例如,由于哈希码方法缓慢或不正确而需要更长时间的流操作可能代表一个长期问题。
最佳哈希码实现
要了解最好的hashcode和equals方法,我们首先需要了解一些平庸的代码。现在我不会展示可怕的或旧的代码。这是很好的代码,但不是最好的:
<span style="color:#444444"><span style="background-color:#f6f6f6"><span style="color:#333333"><strong>public </strong></span> <span style="color:#333333"><strong>int </strong></span> <span style="color:#880000"><strong>hashCode </strong></span>() {
<span style="color:#333333"><strong> return</strong></span> Objects.hash(id, core, setting, values, sets);
}</span></span>
这段代码一开始可能看起来不错,但真的是这样吗?这是理想的代码:
<span style="color:#444444"><span style="background-color:#f6f6f6"><span style="color:#333333"><strong>public </strong></span> <span style="color:#333333"><strong>int </strong></span> <span style="color:#880000"><strong>hashCode </strong></span>() {
<span style="color:#333333"><strong>返回</strong></span>id;
}</span></span>
这是快速、100% 独特且正确的。除此以外,没有理由做任何事情。id 有一个例外,它是一个对象。在这种情况下,我们可能想做 Objects.hashCode(id) 代替它也适用于 null 等。
哈希码不等于
好吧,显然……这是在编写哈希码实现时需要牢记的最重要的事情之一。此方法必须执行快速,并且必须与 false 情况下的 equals 一致。对于 true 的情况,这将是不正确的。
澄清一下,哈希码必须始终遵守这条定律:
<span style="color:#444444"><span style="background-color:#f6f6f6">断言(<span style="color:#333333"><strong>obj1.hashCode</strong></span>()!= obj2.hashCode()<span style="color:#bc6060">&&</span>!obj1.equals(<span style="color:#333333"><strong>obj2</strong></span>))<span style="color:#888888">;</span></span></span>
这意味着如果哈希码结果不同,则对象必须不同,并且必须从 equals 返回 false。但情况并非如此:
<span style="color:#444444"><span style="background-color:#f6f6f6"><span style="color:#333333"><strong>if</strong></span> (obj1.hashCode() == obj2.hashCode()) {
<span style="color:#333333"><strong>if</strong></span> (obj1.equals(obj2)) {
<span style="color:#bc6060">//</span>这可能是<span style="color:#78a960">假</span>的 ...
}
}</span></span>
这里的价值在于性能。hashcode 方法的执行速度应该比 equals 快得多。它应该让我们快速跳过潜在的昂贵的 equals 计算和索引元素。
JPA 的特例
JPA 开发人员通常只对 hashcode 使用硬编码值或使用 Class 对象来生成 hashCode()。在您考虑到这一点之前,这似乎很奇怪。
如果您让数据库为您生成 ID,您将保存一个对象,它将不再等于源对象……一种解决方案是使用 @NaturalId
注释和数据类型。但这需要更改数据模型。不幸的是,实体类没有合适的解决方法。
事实上,我认为 JPA 开发人员在使用 Lombok 时遇到的许多问题都是因为它为您生成了 hashcode 和 equals 方法。这些可能是有问题的。
这是一个关于调试的博客吗?
很抱歉设置了这么长的时间,但是是的,这该死的很好。所以我需要所有这些前言以更通用的调试意义来讨论这个问题。请注意,对于使用类似范式的通用接口的其他语言也是如此。
这个博客从一个性能问题开始,我想从调试的角度讨论这个方面。在许多分析器中,哈希码方法的开销几乎是不明显的。但是因为它被如此频繁地调用并且具有广泛的影响,所以你最终可能会感受到影响并将责任归咎于其他地方。
下意识的反应将是实现一个“虚拟”哈希码方法并查看由此产生的性能差异。只需返回硬编码数字而不是有效数字。
这在某些情况下很有价值,甚至可以解决顶部提到的哈希码方法执行不佳的问题。但是,它对地图没有帮助。如果哈希码将返回相同的值,则将其用作映射中的键将有效地禁用哈希码可以提供的所有性能优势。
我们如何知道哈希码方法是否良好?
嗯……我们可以使用调试器来解决这个问题。只需检查您的地图并查看各个存储桶之间的对象分布,即可了解哈希码方法的真实价值。
如果您在提交时有代码验证过程,我强烈建议您在哈希码方法的复杂性级别上定义规则。这应该设置得非常低,以防止慢代码渗入。
但问题是嵌套。例如,想想我们之前讨论过的代码:
<span style="color:#444444"><span style="background-color:#f6f6f6"><span style="color:#333333"><strong>public </strong></span> <span style="color:#333333"><strong>int </strong></span> <span style="color:#880000"><strong>hashCode </strong></span>() {
<span style="color:#333333"><strong> return</strong></span> Objects.hash(id, core, setting, values, sets);
}</span></span>
它简短而简单。然而,这段代码的性能可能无处不在。该方法将调用所有内部对象的 hashcode 方法。这些方法在性能方面可能要差得多。我们需要对此保持警惕。即使对于 JDK 类,如我们前面讨论的 URL,也是有问题的。
TL;博士
我们经常自动生成 hashcode 和 equals 方法。IDE 通常在这方面做得很好。他们为我们提供了选择我们希望比较的字段的选项。不幸的是,他们随后将两组字段应用于哈希码和等于。
有时,这无关紧要。通常我们不会“看到”重要的地方,因为方法太小而无法在分析器中产生凹痕。但它们具有我们应该优化的广泛影响。
调试让我们检查地图并查看桶分布,以便我们了解哈希码方法的执行情况以及是否应该调整它以从地图和类似 API 中获得更一致的结果。