你为什么要关心equals和hashcode

等于和哈希码是每个Java对象的基本元素。 它们的正确性和性能对于您的应用程序至关重要。 但是,我们经常看到甚至有经验的程序员都忽略了类开发的这一部分。 在本文中,我将介绍一些与这两种非常基本的方法有关的常见错误和问题。

合同

提到的方法至关重要的是所谓的“合同”。 有大约的hashCode三个规则和五个约等于 (你可以找到他们在Java文档的Object类),但我们将讨论三个重要的。 让我们从hashCode()开始:

“只要在Java应用程序执行期间在同一个对象上多次调用它, hashCode方法就必须一致地返回相同的整数,只要没有 修改该对象的equals比较中使用的 信息即可 。”
这意味着对象的哈希码不必是不变的。 因此,让我们看一下真正简单的Java对象的代码:

public class Customer {private UUID id;private String email;public UUID getId() {return id;}public void setId(final UUID id) {this.id = id;}public String getEmail() {return email;}public void setEmail(final String email) {this.email = email;}@Overridepublic boolean equals(final Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;final Customer customer = (Customer) o;return Objects.equals(id, customer.id) &&Objects.equals(email, customer.email);}@Overridepublic int hashCode() {return Objects.hash(id, email);}
}

您可能已经注意到, equalshashCode是由我们的IDE自动生成的。 我们确信这些方法不是一成不变的,并且肯定会广泛使用此类。 也许这样的类太常见了,这样的实现没有错吗? 因此,让我们看一个简单的用法示例:

def "should find cart for given customer after correcting email address"() {given:Cart sampleCart = new Cart()Customer sampleCustomer = new Customer()sampleCustomer.setId(UUID.randomUUID())sampleCustomer.setEmail("emaill@customer.com")HashMap customerToCart = new HashMap<>()when:customerToCart.put(sampleCustomer, sampleCart)then:customerToCart.get(sampleCustomer) == sampleCartand:sampleCustomer.setEmail("email@customer.com")customerToCart.get(sampleCustomer) == sampleCart
}

在上述测试中,我们希望确保在更改示例客户的电子邮件后,我们仍然能够找到其购物车。 不幸的是,该测试失败。 为什么? 因为HashMap将密钥存储在“存储桶”中。 每个存储桶都具有特定范围的哈希。 由于这个想法,哈希映射非常快。 但是,如果我们将密钥存储在第一个存储桶中(负责1到10之间的散列),然后hashCode方法的值返回11而不是5(因为它是可变的),会发生什么? 哈希图尝试查找密钥,但是它检查第二个存储桶(保留哈希11到20)。 它是空的。 因此,对于给定的客户根本没有购物车。 这就是为什么拥有不可更改的哈希码如此重要的原因!

实现它的最简单方法是使用不可变对象。 如果由于某种原因在您的实现中不可能,那么请记住将hashCode方法限制为仅使用对象的不可变元素。
第二个hashCode规则告诉我们,如果两个对象相等(根据equals方法),则哈希值必须相同。 这意味着我必须将这两种方法相关联,这可以通过基于相同的信息(基本上是字段)来实现。

最后但并非最不重要的一点是,它告诉我们有关等式的传递性。 它看起来很琐碎,但事实并非如此-至少在您考虑继承时。 想象我们有一个扩展了日期时间对象的日期对象。 为一个日期实现equals方法很容易–当两个日期相同时,我们返回true。 日期时间也一样。 但是,当我想将日期与日期时间进行比较时会发生什么? 他们有相同的日期,月份和年份是否足够? 是否可以比较小时和分钟,因为日期上没有此信息? 如果我们决定使用这种方法,那我们就搞砸了。 请分析以下示例:

2016-11-28 == 2016-11-28 12:202016-11-28 == 2016-11-28 15:52

由于equals的传递性,我们可以说2016-11-28 12:20等于2016-11-28 15:52这当然是愚蠢的。 但是,当您考虑平等合同时是正确的。

JPA用例

不让我们谈论JPA。 看起来在这里实现equals和hashCode方法非常简单。 我们对每个实体都有唯一的主键,因此基于此信息的实现是正确的。 但是,何时分配了该唯一ID? 在对象创建期间还是在刷新更改到数据库之后? 如果您是手动分配ID,则可以,但是如果您依赖底层引擎,则可能会陷入陷阱。 想象这样的情况:

public class Customer {@OneToMany(cascade = CascadeType.PERSIST)private Setaddresses = new HashSet<>();public void addAddress(Address newAddress) {addresses.add(newAddress);}public boolean containsAddress(Address address) {return addresses.contains(address);}
}

如果地址的hashCode基于ID,则在保存Customer实体之前,我们可以假定所有哈希码均等于零(因为还没有ID)。 刷新更改后,将分配ID,这也会导致新的哈希码值。 现在,您可以调用containsAddress方法,不幸的是,由于与在第一部分中讨论HashMap的相同原因,它将始终返回false。 我们如何保护这种问题? 据我所知,有一种有效的解决方案– UUID。

class Address {@Id@GeneratedValueprivate Long id;private UUID uuid = UUID.randomUUID();// all other fields with getters and setters if you need@Overridepublic boolean equals(final Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;final Address address = (Address) o;return Objects.equals(uuid, address.uuid);}@Overridepublic int hashCode() {return Objects.hash(uuid);}
}

uuid字段(可以是UUID或简单地为String)在对象创建期间分配,并在整个实体生命周期中保持不变。 它存储在数据库中,并在查询该对象后立即加载到字段中。 它或当然会增加一些开销和占用空间,但没有免费的东西。 如果您想了解有关UUID方法的更多信息,可以查看有关此内容的两篇精彩文章:

  • https://www.percona.com/blog/2014/12/19/store-uuid-optimized-way/
  • https://vladmihalcea.com/2014/07/01/hibernate-and-uuid-identifiers/

偏向锁定

十多年来,Java中的默认锁定实现使用一种称为“偏置锁定”的东西。 可以在标志注释中找到有关此技术的简要信息(来源: Java Tuning White Paper ):

-XX:+ UseBiasedLocking
启用一种用于提高无竞争同步性能的技术。 一个对象被“偏向”线程,该线程首先通过Monitorenter字节码或同步方法调用来获取其监视器。 在多处理器计算机上,该线程执行的后续与监视器相关的操作相对要快得多。 在启用了此标志的情况下,某些具有大量无竞争同步的应用程序可能会实现明显的加速。 尽管已尝试将负面影响降到最低,但某些具有某些锁定模式的应用程序可能会变慢。

对于我们而言,有关此帖子的有趣之处是内部如何实现偏置锁定。 Java使用对象标头存储持有锁的线程的ID。 问题在于对象标头的布局定义明确(如果您有兴趣,请参阅OpenJDK源hotspot / src / share / vm / oops / markOop.hpp ),不能像这样“扩展”它。 在64位中,JVM线程ID的长度为54位,因此我们必须决定是否要保留此ID或其他。 不幸的是,“其他”意味着对象哈希码(实际上是身份哈希码,存储在对象头中)。

每当您对自Object类以来没有覆盖它的任何对象调用hashCode()方法时,或者当您直接调用System.identityHashCode()方法时,都将使用此值。 这意味着当您检索任何对象的默认哈希码时; 您禁用对此对象的偏向锁定支持。 这很容易证明。 看一下这样的代码:

class BiasedHashCode {public static void main(String[] args) {Locker locker = new Locker();locker.lockMe();locker.hashCode();}static class Locker {synchronized void lockMe() {// do nothing}@Overridepublic int hashCode() {return 1;}}
}

当您使用以下VM标志运行main方法时: -XX:BiasedLockingStartupDelay=0 -XX:+TraceBiasedLocking您会看到……没有什么有趣的事情:)

但是,从Locker类中删除hashCode实现后,情况将发生变化。 现在我们可以在日志中找到这样的行:
Revoking bias of object 0x000000076d2ca7e0 , mark 0x00007ff83800a805 , type BiasedHashCode$Locker , prototype header 0x0000000000000005 , allow rebias 0 , requesting thread 0x00007ff83800a800

为什么会发生? 因为我们要求提供身份哈希码。 总结一下这一部分:类中没有hashCode意味着没有偏向锁定。

非常感谢https://www.sitepoint.com/java/的 Nicolai Parlog审阅了这篇文章并指出了一些错误。

翻译自: https://www.javacodegeeks.com/2016/12/care-equals-hashcode.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/335489.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java判断时间是否在时间段内_具有C语言基础,利用半年时间学习Java是否够用

首先&#xff0c;对于具有一定C语言基础的同学来说&#xff0c;利用半年的时间来学习Java会有一个比较系统的学习过程&#xff0c;通常也能够达到实习程序员&#xff0c;或者是助理程序员的水平&#xff0c;然后在技术团队中实践一段时间之后&#xff0c;从事专业开发岗位应该是…

Liao Wenxiong‘s thoughts

点击看看在想什么鬼

python3字符串截取_从python3中的字符串中获取特定文本

只是使用str.split()和赋值给变量,还有一个默认的第二个参数可以帮助您处理唯一的total/free avail场景def get_free_memory(ssh_obj, rawTrue):stdin, stdout, stderr ssh_obj.exec_command(free -h)mem_stats stdout.readlines()[1]if raw:mem_stats_formatted " &qu…

kafka connect_Kafka Connect在MapR上

kafka connect在本周的白板演练中&#xff0c;MapR的高级产品营销经理Ankur Desai描述了Apache Kafka Connect和REST API如何简化和提高在处理来自包括旧数据库或数据仓库在内的各种数据源的流数据时的敏捷性。 他还解释了当您使用MapR Streams与Kafka进行数据传输时&#xff0…

二维动画作品_「咻动画」二维动画制作中角色造型的设计要点

关于二维动画可能就算不是动画行业的小伙伴们都多多少少都有所了解&#xff0c;近年来其在宣传片制作上面越来越受用。不少企业抛开传统保守的宣传片表现形式转而尝试动画制作宣传片&#xff0c;我们都知道在动画制作从脚本策划到输出成片这个过程中是一个很复杂的制作流程&…

MySQL函数/数据库函数

文章目录一、单行函数&#xff08;一&#xff09;数学函数1. round(x)&#xff0c;返回离 x 最近的整数&#xff0c;即对 x 进行四舍五入2. round(x,y)&#xff0c;返回数值 x 保留到小数点后 y 位的值3. truncate(x,y)&#xff0c;返回数值 x 保留到小数点后 y 位的值4. abs(x…

java请求并行方案_让 Yar Java Client 支持执行并行请求,ExecutorService 的使用

官方 php 客户端文档如下Yar_Concurrent_Client {/* 属性 */static $_callstack ;static $_callback ;static $_error_callback ;/* 方法 */public static int call ( string $uri , string $method , array $parameters [, callable $callback ] )public static boolean loop …

javaone_JavaOne 2016后续活动

javaone我很高兴今年参加了JavaOne&#xff0c;我可以用一个词概括一下这一经验&#xff1a;辉煌。 对我来说&#xff0c;今年与往年相比有很大不同&#xff0c;因为我在周日有一个演讲要共同主持&#xff0c;而我剩下的一周时间都可以参加会议。 因此&#xff0c;我了解到在Ja…

c# redis 如何设置过期时间_Redis 过期时间与内存管理

http://www.redis.cn/commands/expire.htmlhttp://www.redis.cn/topics/lru-cache.html内存管理 当 Redis 作为缓存使用时&#xff08;此时缓存仅作为热点数据提高服务的访问性能&#xff09;&#xff0c;需要考虑内存的限制&#xff0c;以及如何随着业务的增长&#xff0c;仅保…

Oracle 数据库的子查询(关联子查询)

文章目录一、子查询结果返回一个值&#xff08;单列单行&#xff09;二、子查询结果返回多行&#xff08;单列多行&#xff09;三、子查询结果返回多列多行四、关联子查询&#xff08;一&#xff09;子查询执行顺序&#xff08;二&#xff09;exists 关键字五、子查询总结六、关…

大端字节序码流中取出2字节_产生字节码

大端字节序码流中取出2字节在这篇文章中&#xff0c;我们将看到如何为我们的语言生成字节码。 到目前为止&#xff0c;我们已经看到了如何构建一种语言来表达我们想要的东西&#xff0c;如何验证该语言&#xff0c;如何为该语言构建编辑器&#xff0c;但实际上我们还不能运行代…

python编写程序时必须遵守的规则被称为_Python程序设计方案习题与答案

《 Python 程序设计》习题与参考答案 第 1 章 基础知识 简单说明如何选择正确的 Python 版本。 答&#xff1a; 在选择 Python 的时候&#xff0c;一定要先考虑清楚自己学习 Python 的目的是什么&#xff0c;打算做哪 方面的开发&#xff0c;有哪些扩展库可用&#xff0c;这些扩…

python写520_用Python做一个520表白神器,值得收藏

本文最后给出了打包好的软件&#xff0c;无需安装Python环境和各种依赖&#xff0c;直接下载软件&#xff0c;解压后&#xff0c;双击exe文件即可使用。先来看一下具体的效果。运行程序。用Python做一个520表白神器&#xff0c;值得收藏点击「选择图片」 选择JPG/JPGE/PNG三种中…

Oracle/MySQL数据库查询结果集的集合操作(合集/交集/差集)

文章目录集合操作的前提条件集合操作合集 union合集 union 和 union all 的比较交集 intersect差集 minus集合操作的前提条件 1.两个结果集必须结构相同。 当列的个数、列的顺序、列的数据类型一致时 , 我们称这两个结果集结构相同 2.只有结构相同的结果集才能做集合操作 集…

python有道自动翻译_利用python写一个有道翻译的脚本

废话不多说&#xff0c;直接上代码import urllib.request import urllib.parse import json content input("请输入要翻译的内容&#xff1a;") url http://fanyi.youdao.com/translate?smartresultdict&smartresultrule&smartresultugc&sessionFromn…

为什么java抗并发_用最通熟易懂的话说明,为什么要使用java并发编程

老早之前的计算机只有一个处理器&#xff0c;而 一个处理器在同一时刻只能处理一条指令 &#xff0c;换句话说&#xff0c;我们的代码需要一行一行的按顺序被计算机执行&#xff0c;计算机只能把一个程序完整的执行完&#xff0c;然后再执行第二个程序。所以计算机专业的同学们…

java ee的小程序_Java EE调度程序

java ee的小程序Java EE应用程序服务器具有本机调度支持&#xff0c;并且在大多数应用程序中&#xff0c;不需要包括外部依赖项&#xff0c;例如著名的Quartz调度程序库。 Java EE 6和7完整配置文件上提供的Java EE 6计时器服务为我们提供了许多选项来定义调度间隔&#xff0c…

MyEclipse for Mac快捷键

文章目录编辑查询/替换导航调试重构其他编辑 快捷键功能说明Command1快速修复&#xff0c;比如与Syso配合&#xff0c;与main配合可快速构造方法签名&#xff08;最经典的快捷键,就不用多说了&#xff0c;可以解决很多问题&#xff0c;比如import类、try catch包围等&#xff…

不同坐标系下角速度_最伟大的数学发明,坐标系的诞生,是人类史上的方向盘...

【想要了解更多精彩文章、视频&#xff0c;欢迎关注创鹏科学堂】人生最大的意义&#xff0c;莫过于过得更方便&#xff1b;数学最大的意义&#xff0c;莫过于帮助人类过得更方便。几千年来&#xff0c;自从数学出现之后&#xff0c;它就一直以人类生活为导向&#xff0c;以宇宙…

c++ double 截取_c选择double小数点后自动截取3位,不...

2016-09-01 01:05辛培兵 客户经理printf()函数是格式输出函数&#xff0c;请求printf()打印变量的指令取决与变量的类型&#xff0e;例如&#xff0c;在打印整数是使用&#xff05;d符号&#xff0c;在打印字符是用&#xff05;c 符号&#xff0e;这些符号被称为转换说明&#…