JavaRush /Java 博客 /Random-ZH /equals 和 hashCode 方法:使用实践

equals 和 hashCode 方法:使用实践

已在 Random-ZH 群组中发布
你好!今天我们将讨论Java中的两个重要方法——equals()hashCode()。这不是我们第一次见到他们:在 JavaRush 课程开始时有一个简短的讲座,内容equals()是 - 如果您忘记了或以前没有看过,请阅读它。 方法等于 &  hashCode:使用实践 - 1在今天的课程中,我们将详细讨论这些概念 - 相信我,有很多东西要讨论!在我们继续讨论新内容之前,让我们回顾一下已经介绍过的内容:) 如您所知,通常使用“ ==”运算符比较两个对象是一个坏主意,因为“ ==”比较引用。这是我们最近一次讲座中的汽车示例:
public class Car {

   String model;
   int maxSpeed;

   public static void main(String[] args) {

       Car car1 = new Car();
       car1.model = "Ferrari";
       car1.maxSpeed = 300;

       Car car2 = new Car();
       car2.model = "Ferrari";
       car2.maxSpeed = 300;

       System.out.println(car1 == car2);
   }
}
控制台输出:

false
看起来我们创建了两个相同的类对象Car:两台机器上的所有字段都相同,但比较的结果仍然是 false。我们已经知道原因了:链接car1car2指向内存中的不同地址,因此它们不相等。我们仍然想要比较两个对象,而不是两个引用。比较对象的最佳解决方案是equals().

equals() 方法

你可能还记得,我们并不是从头开始创建这个方法,而是重写它——毕竟,该方法equals()是在类中定义的Object。然而,在通常的形式下,它没什么用处:
public boolean equals(Object obj) {
   return (this == obj);
}
equals()这就是在类中定义 方法的方式Object。同样的链接比较。他为何被造就成这个样子?那么,语言的创建者如何知道程序中的哪些对象被认为是相等的,哪些不是?:) 这是该方法的主要思想equals()- 类的创建者自己确定检查该类的对象是否相等的特征。equals()通过这样做,您可以重写类中的方法。如果你不太明白“你自己定义特征”的含义,让我们看一个例子。这里有一个简单的人类—— Man
public class Man {

   private String noseSize;
   private String eyesColor;
   private String haircut;
   private boolean scars;
   private int dnaCode;

public Man(String noseSize, String eyesColor, String haircut, boolean scars, int dnaCode) {
   this.noseSize = noseSize;
   this.eyesColor = eyesColor;
   this.haircut = haircut;
   this.scars = scars;
   this.dnaCode = dnaCode;
}

   //getters, setters, etc.
}
假设我们正在编写一个程序,需要确定两个人是否有双胞胎关系,或者只是分身关系。我们有五个特征:鼻子大小、眼睛颜色、发型、疤痕的存在和 DNA 生物测试的结果(为简单起见 - 以代码的形式)。您认为以下哪些特征可以让我们的程序识别双胞胎亲属? 方法等于 &  hashCode:使用实践 - 2当然,只有生物测试才能提供保证。两个人可以有相同的眼睛颜色、发型、鼻子,甚至疤痕——世界上有很多人,不可能避免重合。我们需要一个可靠的机制:只有DNA测试的结果才能让我们得出准确的结论。这对我们的方法意味着什么equals()Man我们需要考虑到我们程序的要求,在类中重新定义它。该方法必须比较两个对象的字段int dnaCode,如果它们相等,则对象相等。
@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
真的有那么简单吗?并不真地。我们错过了一些东西。在这种情况下,对于我们的对象,我们只定义了一个“重要”字段,通过该字段建立它们的相等性 - dnaCode。现在想象一下,我们不会有 1 个,而是 50 个这样的“重要”字段。如果两个对象的所有 50 个字段都相等,那么这两个对象就相等。这也可能发生。主要问题是计算 50 个字段的相等性是一个耗时且消耗资源的过程。现在想象一下,除了类之外,Man我们还有一个Woman与 中的字段完全相同的类Man。如果另一个程序员使用您的类,他可以轻松地在他的程序中编写如下内容:
public static void main(String[] args) {

   Man man = new Man(........); //a bunch of parameters in the constructor

   Woman woman = new Woman(.........);//same bunch of parameters.

   System.out.println(man.equals(woman));
}
在这种情况下,检查字段值是没有意义的:我们看到我们正在查看两个不同类的对象,并且原则上它们不能相等!这意味着我们需要在方法中进行检查equals()——比较两个相同类的对象。好在我们想到了这一点!
@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
但也许我们忘记了其他事情?嗯...至少,我们应该检查我们没有将对象与自身进行比较!如果引用 A 和 B 指向内存中的相同地址,那么它们是同一个对象,我们也不需要浪费时间比较 50 个字段。
@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
此外,添加对 的检查不会有什么坏处null:没有对象可以等于null,在这种情况下,额外的检查没有意义。考虑到所有这些,我们的equals()类方法Man将如下所示:
@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
我们执行上述所有初步检查。如果事实证明:
  • 我们比较同一类的两个对象
  • 这不是同一个对象
  • 我们不是将我们的对象与null
...然后我们继续比较重要的特征。在我们的例子中,是两个对象的字段dnaCode。重写方法时equals(),请务必遵守以下要求:
  1. 反身性。

    任何物体都必须是equals()针对其自身的。
    我们已经考虑到了这一要求。我们的方法指出:

    if (this == o) return true;

  2. 对称。

    如果a.equals(b) == true,那么b.equals(a)它应该返回true
    我们的方法也满足这个要求。

  3. 传递性。

    如果两个对象等于某个第三个对象,那么它们必须彼此相等。
    如果a.equals(b) == truea.equals(c) == true,则检查b.equals(c)也应返回 true。

  4. 持久性。

    equals()只有当其中包含的字段发生变化时,工作结果才会发生变化。如果两个对象的数据没有改变,那么检查的结果equals()应该总是相同的。

  5. 不等式与null.

    对于任何对象,检查a.equals(null)都必须返回 false。
    这不仅仅是一组“有用的建议”,而是Oracle 文档中规定的严格的方法契约

hashCode() 方法

现在我们来谈谈方法hashCode()。为什么需要它?完全相同的目的——比较对象。但我们已经拥有了equals()!为什么还有另一种方法?答案很简单:提高生产力。哈希函数由 Java 中的 ,​​ 方法表示hashCode(),它为任何对象返回固定长度的数值。对于 Java,该方法hashCode()返回类型为 的 32 位数字int。相互比较两个数字比使用 方法比较两个对象要快得多equals(),特别是当它使用许多字段时。如果我们的程序要比较对象,则通过哈希码来执行此操作要容易得多,并且仅当它们相等时hashCode()- 继续通过 进行比较equals()。顺便说一句,这就是基于哈希的数据结构的工作原理——例如,您所知道的HashMap!该方法hashCode()与 一样equals(),由开发人员自己重写。就像 for 一样equals(),该方法hashCode()具有 Oracle 文档中指定的官方要求:
  1. 如果两个对象相等(即该方法equals()返回 true),则它们必须具有相同的哈希码。

    否则我们的方法就毫无意义。hashCode()正如我们所说,应该首先检查以提高性能。如果哈希码不同,即使对象实际上相等(正如我们在方法中定义的equals()),检查也会返回 false。

  2. 如果hashCode()对同一个对象多次调用一个方法,则每次都应返回相同的数字。

  3. 相反,规则 1 则不起作用。两个不同的对象可以具有相同的哈希码。

第三条规则有点令人困惑。怎么会这样?解释很简单。该方法hashCode()返回int. int是一个32位数字。它的值数量有限 - 从 -2,147,483,648 到 +2,147,483,647。换句话说,这个数字的变化刚刚超过 40 亿种int。现在想象一下,您正在创建一个程序来存储地球上所有活着的人的数据。每个人都会有自己的类对象Man。地球上生活着大约 75 亿人。换句话说,无论Man我们编写多么好的将对象转换为数字的算法,我们都不会拥有足够的数字。我们只有 45 亿种选择,而且还有更多的人。这意味着无论我们如何努力,哈希码对于不同的人来说都是相同的。这种情况(两个不同对象的哈希码匹配)称为冲突。重写方法时程序员的目标之一hashCode()是尽可能减少潜在的冲突次数。考虑到所有这些规则,我们的hashCode()类方法会是什么样子?Man像这样:
@Override
public int hashCode() {
   return dnaCode;
}
惊讶吗?:) 出乎意料,但如果你看看这些要求,你会发现我们遵守了一切。我们返回 true 的对象equals()将在 中相等hashCode()。如果我们的两个对象的Man值相等equals(即它们具有相同的值dnaCode),我们的方法将返回相同的数字。让我们看一个更复杂的例子。假设我们的程序应该为收藏家客户选择豪华汽车。收藏是一件复杂的事情,它有很多特点。一辆 1963 年生产的汽车可能比 1964 年生产的同一辆车贵 100 倍。一辆 1970 年生产的红色汽车的价格比同年生产的相同品牌的蓝色汽车贵 100 倍。 方法等于 &  hashCode:使用实践 - 4在第一种情况下,对于 类Man,我们丢弃了大部分字段(即人员特征),因为它们无关紧要,并且仅使用该字段进行比较dnaCode。我们正在处理一个非常独特的区域,不能有任何细节!这是我们的班级LuxuryAuto
public class LuxuryAuto {

   private String model;
   private int manufactureYear;
   private int dollarPrice;

   public LuxuryAuto(String model, int manufactureYear, int dollarPrice) {
       this.model = model;
       this.manufactureYear = manufactureYear;
       this.dollarPrice = dollarPrice;
   }

   //... getters, setters, etc.
}
这里,在比较时,我们必须考虑到所有字段。任何错误都可能给客户带来数十万美元的损失,因此最好保持安全:
@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   if (dollarPrice != that.dollarPrice) return false;
   return model.equals(that.model);
}
在我们的方法中,equals()我们没有忘记之前讨论过的所有检查。但现在我们比较对象的三个字段中的每一个。在这个计划中,每个领域的平等都必须是绝对的。关于什么hashCode
@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
我们类中的字段model是一个字符串。这很方便:String该方法hashCode()已经在类中被重写。我们计算字段 的哈希码model,并将其他两个数字字段的总和添加到其中。Java中有一个小技巧是用来减少冲突次数的:计算哈希码时,将中间结果乘以一个奇素数。最常用的数字是 29 或 31。我们现在不会详细讨论数学,但为了将来的参考,请记住将中间结果乘以足够大的奇数有助于“分散”散列结果函数并最终得到具有相同哈希码的更少对象。对于 LuxuryAuto 中的方法,hashCode()它将如下所示:
@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
您可以在 StackOverflow 上的这篇文章以及 Joshua Bloch 的书“ Effective Java ”中 了解有关此机制的所有复杂性的更多信息。最后,还有一点值得一提。每次重写时equals()hashCode()我们都会选择对象的某些字段,这些字段会在这些方法中考虑在内。但是我们可以考虑equals()和中的不同领域hashCode()吗?从技术上讲,我们可以。但这是一个坏主意,原因如下:
@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   return dollarPrice == that.dollarPrice;
}

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
以下是LuxuryAuto 类equals()的 方法。hashCode()方法hashCode()保持不变,equals()我们从方法中删除了该字段model。现在该模型不是通过 来比较两个对象的特征equals()。但计算哈希码时仍将其考虑在内。我们会得到什么结果?让我们创建两辆车并检查一下!
public class Main {

   public static void main(String[] args) {

       LuxuryAuto ferrariGTO = new LuxuryAuto("Ferrari 250 GTO", 1963, 70000000);
       LuxuryAuto ferrariSpider = new LuxuryAuto("Ferrari 335 S Spider Scaglietti", 1963, 70000000);

       System.out.println("Are these two objects equal to each other?");
       System.out.println(ferrariGTO.equals(ferrariSpider));

       System.out.println("What are their hash codes?");
       System.out.println(ferrariGTO.hashCode());
       System.out.println(ferrariSpider.hashCode());
   }
}

Эти два an object равны друг другу?
true
Какие у них хэш-codeы?
-1372326051
1668702472
错误!通过使用不同的字段equals()hashCode()我们违反了为他们制定的合同!两个相等的equals()对象必须具有相同的哈希码。我们对它们有不同的含义。此类错误可能会导致最令人难以置信的后果,尤其是在处理使用哈希的集合时。因此,重新定义时equals()hashCode()使用相同的字段将是正确的。讲座虽然很长,但今天你学到了很多新东西!:) 是时候回去解决问题了!
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION