Java Challengers#4:将对象与equals()和hashCode()进行比较

Java Challengers#4:将对象与equals()和hashCode()进行比较


预期将在“ Java Developer”课程上发布新线程我们将继续翻译一系列Java Challengers文章,其前面的部分可以在下面的链接中阅读:



走吧


在本文中,您将学习equals()hashCode()方法之间的关系以及在比较对象时如何使用它们。


等于哈希码


在不使用equals()hashCode()来比较两个对象的状态的情况下,我们需要编写大量的“ if ”比较来比较对象的每个字段。 这种方法使代码混乱并且难以阅读。 这两种方法一起工作,有助于创建更灵活,更一致的代码。


本文的源代码在这里


覆盖equals()和hashCode()


方法重写是一种技术,通过该技术可以在子类中重写(重新定义)父类或接口的行为(请参见Java Challengers#3:多态与继承英文 )。 在Java中,每个对象都具有equals()hashCode()方法,并且它们必须被重写才能正常工作。


为了了解equals()hashCode()的重新定义是如何工作的,让我们检查一下Java基类中的实现。 以下是Object类的equals()方法。 该方法检查当前实例是否与传递的obj对象匹配。


 public boolean equals(Object obj) { return (this == obj); } 

现在让我们看一下Object类中的hashCode()方法。


 @HotSpotIntrinsicCandidate public native int hashCode(); 

这是本机的-用另一种语言(例如C)编写的方法,它返回一些与对象的内存地址关联的数字代码。 (如果您不编写JDK代码,则确切了解此方法的工作原理并不重要。)
译者注:与地址关联的值并不完全正确( 感谢 vladimir_dolzhenko )。 默认情况下,HotSpot JVM使用伪随机数。 HotSpot的hashCode()实现的描述在此处此处


如果不覆盖equals()hashCode()方法,则将改为调用上述Object类的方法。 在这种情况下,这些方法不能满足equals()hashCode()的实际目的,即检查对象是否具有相同的状态。


通常,覆盖equals()也会覆盖hashCode()


用equals()比较对象


equals()方法用于比较对象。 要确定对象是否相同, equals()比较对象字段值:


 public class EqualsAndHashCodeExample { public static void main(String... args){ System.out.println(new Simpson("Homer", 35, 120) .equals(new Simpson("Homer",35,120))); System.out.println(new Simpson("Bart", 10, 120) .equals(new Simpson("El Barto", 10, 45))); System.out.println(new Simpson("Lisa", 54, 60) .equals(new Object())); } static class Simpson { private String name; private int age; private int weight; public Simpson(String name, int age, int weight) { this.name = name; this.age = age; this.weight = weight; } @Override public boolean equals(Object o) { // 1 if (this == o) { return true; } // 2 if (o == null || getClass() != o.getClass()) { return false; } // 3 Simpson simpson = (Simpson) o; return age == simpson.age && weight == simpson.weight && name.equals(simpson.name); } } } 

让我们看一下equals()方法。 第一个比较将当前实例与this传递的o进行比较。 如果是同一对象,则equals()将返回true


第二个比较检查以查看传递的对象是否为null以及它是什么类型。 如果传输的对象是其他类型,则对象不相等。


最后, equals()比较对象的字段。 如果两个对象具有相同的字段值,则这些对象是相同的。


分析对象比较的选项


现在让我们看一下main()方法中用于比较对象的选项。 首先,我们比较两个Simpson对象:


 System.out.println( new Simpson("Homer", 35, 120).equals( new Simpson("Homer", 35, 120))); 

这些对象的字段具有相同的值,因此结果为true


然后再次比较两个Simpson对象:


 System.out.println( new Simpson("Bart", 10, 45).equals( new Simpson("El Barto", 10, 45))); 

这里的对象相似,但名称的含义不同: BartEl Barto 。 因此,结果将为false


最后,让我们比较Simpson对象和Object类的实例:


 System.out.println( new Simpson("Lisa", 54, 60).equals( new Object())); 

在这种情况下,结果将为false ,因为对象的类型不同。


等于()与==


乍一看,似乎==运算符和equals()方法执行相同的操作,但是实际上它们的工作方式不同。 ==运算符比较两个链接是否指向同一对象。 例如:


 Simpson homer = new Simpson("Homer", 35, 120); Simpson homer2 = new Simpson("Homer", 35, 120); System.out.println(homer == homer2); 

我们使用new运算符创建了Simpson两个不同实例。 因此,变量homerhomer2将指向堆中的不同对象。 因此,结果是false


在下面的示例中,我们使用重写的equals()方法:


 System.out.println(homer.equals(homer2)); 

在这种情况下,将比较这些字段。 由于两个Simpson对象都具有相同的字段值,因此结果为true


用hashCode()标识对象


为了在比较对象时优化性能,使用了hashCode()方法。 hashCode()方法为每个对象返回唯一的标识符,从而简化了对象状态的比较。


如果一个对象的哈希码与另一个对象的哈希码不匹配,则可以省略equals()方法:您只知道两个对象不匹配。 另一方面,如果哈希码相同,则需要执行equals()方法来确定字段值是否匹配。


考虑一个带有hashCode()的实际示例。


 public class HashcodeConcept { public static void main(String... args) { Simpson homer = new Simpson(1, "Homer"); Simpson bart = new Simpson(2, "Homer"); boolean isHashcodeEquals = homer.hashCode() == bart.hashCode(); if (isHashcodeEquals) { System.out.println("   equals."); } else { System.out.println("    equals, .. " + " ,  ,     ."); } } static class Simpson { int id; String name; public Simpson(int id, String name) { this.id = id; this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Simpson simpson = (Simpson) o; return id == simpson.id && name.equals(simpson.name); } @Override public int hashCode() { return id; } } } 

始终返回相同值的hashCode()方法有效,但效率不高。 在这种情况下,比较将始终返回true ,因此将始终执行equals()方法。 在这种情况下,无法提高性能。


对集合使用equals()和hashCode()


实现Set接口(集合)的类应防止添加重复的元素。 以下是一些实现Set接口的类:



只能将唯一元素添加到Set 。 因此,例如,如果要将元素添加到HashSet ,则必须首先使用equals()hashCode()方法来确保此元素是唯一的。 如果equals()hashCode()方法未被覆盖,则可能会插入重复的值。


让我们看一下HashSet add()方法的实现部分:


 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; 

在添加新项目之前, HashSet检查该项目是否存在于此集合中。 如果对象匹配,则不会插入新元素。


不仅在Set中使用equals()hashCode()方法。 HashMapHashtableLinkedHashMap也需要这些方法。 通常,如果看到带有“ Hash”前缀的集合,则可以确保对于其正确操作,需要覆盖hashCode()equals()方法。


有关使用equals()和hashCode()的建议


仅对具有相同哈希码的对象运行equals()方法。 如果哈希码不同,则不要执行 equals()


表1.哈希码比较


如果hashCode()比较...那...
返回true执行equals()
返回false不执行equals()

由于性能原因,该原理主要用于SetHash集合中。


对象比较规则


hashCode()比较返回falseequals()方法也应该返回 false 。 如果哈希码不同,则对象肯定不相等。


表2.具有hashCode()的对象的比较


hashCode()比较返回...equals()方法应返回...
truetrue还是false
falsefalse

equals()方法返回true ,这意味着对象的所有值和属性都相等。 在这种情况下,哈希码比较也应该为真。


表3.具有equals()的对象的比较


equals()方法返回时...hashCode()方法应返回...
truetrue
falsetrue还是false

解决equals()和hashCode()上的问题


现在该测试您对equals()hashCode()方法的了解了。 任务是找出几个equals()的结果以及Set集合的总大小。


首先,请仔细研究以下代码:


 public class EqualsHashCodeChallenge { public static void main(String... args) { System.out.println(new Simpson("Bart").equals(new Simpson("Bart"))); Simpson overriddenHomer = new Simpson("Homer") { public int hashCode() { return (43 + 777) + 1; } }; System.out.println(new Simpson("Homer").equals(overriddenHomer)); Set set = new HashSet(Set.of(new Simpson("Homer"), new Simpson("Marge"))); set.add(new Simpson("Homer")); set.add(overriddenHomer); System.out.println(set.size()); } static class Simpson { String name; Simpson(String name) { this.name = name; } @Override public boolean equals(Object obj) { Simpson otherSimpson = (Simpson) obj; return this.name.equals(otherSimpson.name) && this.hashCode() == otherSimpson.hashCode(); } @Override public int hashCode() { return (43 + 777); } } } 

首先,分析代码,考虑结果是什么。 然后才运行代码。 目的是提高代码分析技能并学习基本的Java概念,以便使您的代码更好。


结果是什么?


 A) true true 4 B) true false 3 C) true false 2 D) false true 3 

发生什么事了 了解equals()和hashCode()


在第一个比较中, equals()的结果为true ,因为对象的状态相同,并且hashCode()方法为两个对象返回相同的值。


在第二个比较中, hashCode()方法被重写为hashCode()变量。 对于两个Simpson对象,名称均为“ Homer” ,但对于overriddenHomer hashCode()方法将返回不同的值。 在这种情况下, equals()方法的结果将为false ,因为它包含与哈希码的比较。


您必须已经意识到集合中将存在三个Simpson对象。 让我们弄清楚。


集合中的第一个对象将照常插入:


 new Simpson("Homer"); //  

以下对象也将以通常的方式插入,因为它包含与前一个对象不同的值:


 new Simpson("Marge"); //  

最后,下一个Simpson对象具有与第一个对象相同的名称值。 在这种情况下,将不会插入对象:


 set.add(new Simpson("Homer")); //   

众所周知,与常规Simpson("Homer")实例Simpson("Homer")不同, overridenHomer对象使用不同的哈希值。 因此,该项目将被插入到集合中:


 set.add(overriddenHomer); //  

答案


正确答案是B。结论将是:


 true false 3 

equals()和hashCode()的常见错误


  • 缺少覆盖的hashCode()以及覆盖的equals() ,反之亦然。
  • 使用HashSet类的哈希集合时,缺少覆盖equals()hashCode()
  • hashCode()方法中返回常量值,而不是为每个对象返回唯一的代码。
  • 等价使用==equals()==运算符比较对象引用,而equals()方法比较对象值。

关于equals()和hashCode()的注意事项


  • 建议始终在POJO( 俄语英语 )中覆盖equals()hashCode()方法。
  • 使用高效的算法来创建唯一的哈希码。
  • 覆盖equals()方法时,请始终覆盖hashCode()方法。
  • equals()方法应比较对象的完整状态(来自字段的值)。
  • hashCode()方法可以是POJO标识符(ID)。
  • 如果比较两个对象的哈希码的结果为false ,则equals()方法也必须为false
  • 如果在使用哈希集合时未重新定义equals()hashCode() ,则该集合将具有重复的元素。

了解有关Java的更多信息



传统上,我会等待您的评论并邀请您参加公开课程 ,该课程将于3月18日由我们的老师Sergei Petrelevich举办

Source: https://habr.com/ru/post/zh-CN443710/


All Articles