CodeGym /Java 博客 /随机的 /equals 和 hashCode 方法:最佳实践
John Squirrels
第 41 级
San Francisco

equals 和 hashCode 方法:最佳实践

已在 随机的 群组中发布
你好!今天我们将讨论 Java 中的两个重要方法:equals()hashCode(). 这不是我们第一次见到他们:CodeGym 课程从一个简短的课程开始equals()——如果您忘记了或者以前没有看过,请阅读…… 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对象:两个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 测试结果(为简单起见,我们将其表示为整数代码)。您认为这些特征中的哪一个可以让我们的程序识别同卵双胞胎? equals 和 hashCode 方法:最佳实践 - 2当然,只有DNA检测才能提供保证。两个人可以有相同的眼睛颜色、发型、鼻子,甚至疤痕——世界上有很多人,无法保证没有分身。但我们需要一个可靠的机制:只有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(.........); // The 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。因此,如果方法参数为 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()方法用于将任何对象与自身进行比较时,它必须返回 true。
    我们已经满足了这个要求。我们的方法包括:

    
    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()方法一样,由开发人员覆盖。和 一样equals(),该hashCode()方法在 Oracle 文档中有详细说明的官方要求:
  1. 如果两个对象相等(即equals()方法返回真),那么它们必须具有相同的散列码。

    否则,我们的方法将毫无意义。正如我们上面提到的,hashCode()检查应该首先进行以​​提高性能。如果哈希码不同,那么检查将返回 false,即使根据我们定义方法的方式对象实际上是相等的equals()

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

  3. 规则 1 不适用于相反的方向。两个不同的对象可以具有相同的哈希码。

第三条规则有点混乱。怎么会这样?解释很简单。该hashCode()方法返回一个int. Anint是一个 32 位数字。它的值范围有限:从 -2,147,483,648 到 +2,147,483,647。换句话说,一个 . 的可能值刚刚超过 40 亿个int。现在假设您正在创建一个程序来存储地球上所有人的数据。每个人都会对应自己的Person对象(类似于班级Man)。地球上生活着约 75 亿人。换句话说,无论我们为转换编写的算法多么巧妙Person对象到 int,我们根本没有足够的可能数字。我们只有 45 亿个可能的 int 值,但有比这更多的人。这意味着无论我们多么努力,一些不同的人都会有相同的哈希码。当这种情况发生时(两个不同对象的哈希码一致),我们称之为碰撞。覆盖该hashCode()方法时,程序员的目标之一是尽量减少潜在的冲突次数。考虑到所有这些规则,类hashCode()中的方法会是什么样子Person?像这样:

@Override
public int hashCode() {
   return dnaCode;
}
惊讶吗?:) 如果您查看要求,您会发现我们完全遵守这些要求。根据,我们的方法返回 true 的对象equals()也将相等hashCode()。如果我们的两个Person对象 in 相等equals(即它们具有相同的dnaCode),那么我们的方法返回相同的数字。让我们考虑一个更困难的例子。假设我们的程序应该为汽车收藏家选择豪华汽车。收藏可能是一种复杂的爱好,具有许多特点。一辆特定的 1963 年汽车的价格可能是 1964 年汽车的 100 倍。一辆1970年的红车比同年同品牌的蓝车贵100倍。 equals 和 hashCode 方法:最佳实践 - 4在我们前面的例子中,对于类Person,我们丢弃了大部分字段(即人类特征)作为无关紧要的并且只使用了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。我们现在不会深入研究数学上的微妙之处,但以后请记住,将中间结果乘以足够大的奇数有助于“散布”哈希函数的结果,并且,因此,减少具有相同哈希码的对象的数量。对于我们hashCode()在 LuxuryAuto 中的方法,它看起来像这样:

@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()andhashCode()方法时,我们都会选择在这些方法中考虑的某些实例字段。这些方法考虑相同的字段。但是我们可以考虑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;
}
这是我们的equals()和类hashCode()的方法LuxuryAuto。该hashCode()方法保持不变,但我们modelequals()方法中删除了该字段。模型不再是该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());
   }
}

Are these two objects equal to each other? 
true 
What are their hash codes? 
-1372326051 
1668702472
错误!equals()通过对方法和方法使用不同的字段hashCode(),我们违反了为它们建立的契约!根据equals()方法相等的两个对象必须具有相同的哈希码。我们为他们收到了不同的价值观。此类错误可能会导致绝对令人难以置信的后果,尤其是在处理使用散列的集合时。因此,当您覆盖equals()and时hashCode(),您应该考虑相同的字段。这节课很长,但你今天学到了很多东西!:) 现在是时候回去解决任务了!
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION