覆盖equals和hashCode方法

Object类具有五个非最终方法,即equals,hashCode,toString,clone和finalize。
这些设计旨在根据特定的一般合同予以覆盖。 使用这些方法的其他类均假定这些方法遵守这些约定,因此有必要确保如果您的类重写了这些方法,则它们将正确执行。

在本文中,我将介绍equals和hashCode方法。

覆盖equals方法

当您要指定对象的逻辑相等规则时,请覆盖equals方法。 什么是逻辑平等? 一个简单的定义是,如果两个对象的唯一性属性相同,则它们在逻辑上相等。

合同

equals方法实际上实现了(非null)对象引用之间的等效关系。 以下是equals方法必须遵守的规则:


  • 对称性:对于任何参考值x和y,x.equals(y)为true表示y.equals(x)也为true。
  • 反射率:对于任何参考值x,x.equals(x)必须始终返回true。
  • 一致性:对于任何参考值x和y,如果未修改对象的equals比较中使用的信息,则x.equals(y)始终返回true或始终返回false。
  • 传递性:对于x,y和z的任何参考值,如果x.equals(y)返回true,而y.equals(z)返回true,则x.equals(z)必须返回true。
注意,上面引用的所有参考值都必须为非null。 空引用的规则是
  • 对于任何非空参考值x,x.equals(null)必须返回false。
已经在那里了吗?

对象类已经提供了equals方法的实现。 这里是
 public boolean equals(Object obj) {
         return (this == obj);
    }
多少钱
该方法只是测试对象引用是否相等。 这并不总是所需的行为,尤其是在比较字符串时。 这就是String类提供其自己的equals方法实现的原因。

性格测试

假设您正在创建一个Person类,每个人都有一个标题,fullName和age。 如果两个Person对象具有完全相同的fullName,title和age字段,则可以确定它们必须引用同一个人。 也就是说,您的唯一性属性集是(fullName,title,age)您的类可能像这样:

 public class Person {
        String title;
        String fullName;
        int age;
        public Person(String title, String fullName, int age) {
                         this.title = title;
                 this.fullName = fullName;
                 this.age = age;
        }
         String getFullName() {
                       return fullName;
         }
         int getAge() {
              return age;
             }
             String getTitle() {
             return title;
        }
}
现在,如果您创建两个具有相同属性的对象,则希望这两个对象是同一个人。
如果你这样做
Person admin1 = new Person("Admin", "r035198x", 2); //I'm not giving away my age that easily
Person admin2 = new Person("Admin", "r035198x", 2);
System.out.println(admin1.equals(admin2));
您将得到false,因为admin1和admin2是不同的引用,并且此处使用的equals方法(来自对象类)正在比较引用。 因此,在这里我们需要重写equals方法以检查我们的唯一性属性,以便进行比较。

提供自己的equals方法时,需要注意以下几点。

愚蠢的错误

一个愚蠢的错误是重载equals方法而不是重写它!
equals方法的签名是
 public boolean equals(Object obj) 
所以有
public boolean equals(Person person) 
不覆盖equals方法。 它只是创建另一个equals方法(equals方法的重载)。 equals方法的参数必须是一个对象。

优化它

另一个重要问题是优化。 请记住,对称性表示对象始终等于自身(x.equals(x)应始终返回true)。 我们可以利用这一优势,并首先对此案例进行测试。 因此,“智能”实现equals应该从

 public boolean equals(Object obj) {
           if(this == obj) {
                return true;
            }
      ...
如果通过的对象本身就是,则无需通过进一步的测试来困扰处理器。

用一个实例杀死许多鸟

接下来,您现在需要比较实际字段值,以根据逻辑确定相等性。 由于传递给equals方法的参数始终是Object类型的,因此在比较字段之前,您首先需要将其强制转换为类类型的对象。 在我们的示例中,我们将需要转换为Person类型的对象。

如果我们传递了一个String(或任何其他类型的对象)而不是Person对象,该怎么办?
这是instanceof运算符派上用场的地方。 实际上,instanceof运算符所做的不仅仅是确保我们的强制转换不会失败。 它还提供了一些优化,因为如果x instanceof y为false,则x.equals(y)始终为false。 因此,我们可以使用它来消除由于其类类型而在逻辑上不等于我们的对象的任何传递的对象。 当传递的对象为null时,instanceof还消除了琐碎的情况,因为如果第一个参数为null,则返回false。 所以我们有
 if (!(obj instanceof Person)) {
        return false; 
}
把它放在一起

现在,我们需要为相等性测试添加自己的逻辑。
public boolean equals(Object obj) {
          if(this == obj) {
                return true;
           }
           if (!(obj instanceof Person)) {
                  return false; 
           }
           Person person = (Person)obj;
           return age == person.getAge() && fullName.equals(person.getFullName())
        && title.equals(person.getTitle()); 
    } 
这次
System.out.println(admin1.equals(admin2)); 
将打印true。

继承可能违约

现在,假设我们用一个ChessPlayer类扩展Person类,以将排名和国籍作为已经从Person类继承的字段的附加字段。 进一步假设我们希望通过标题,全名,年龄,国籍和排名来确定ChessPlayers的唯一性。 现在,来自Person类的equals方法不再足够。
您会看到,在不违反相等合同的情况下扩展扩展方面的具体类是不可能的。 每次上课时都要考虑一下。 将方面添加到抽象类的情况最好留给读者研究。
因此,必须在ChessPlayer类中重写equals方法以适应国籍和排名方面的问题,而在我看来,我不妨将其作为练习留给读者。

简单的优先

请注意,我故意比较了年龄(整数)。 &&运算符具有短路行为,这意味着如果年龄比较失败,则其余的比较将被放弃,并返回false。 因此,性能上的优势是首先进行较便宜的(内存方面)测试,最后进行更多的内存需求测试。

何必呢?

值得庆幸的是,在某些情况下,您不必重写equals方法。
  • 当引用检查足够时。 这是该类的每个实例都是唯一的。 Thread类就是一个例子。
  • 父类已经实现了所需的行为。 您必须对此小心,并确保父类的equals方法对于子类而言确实足够。
防范愚蠢的程序员

请注意,如果没有必要为类定义逻辑相等,则应包括equals方法,并简单地抛出UnsupportedOperationException。 如果您不包括它,那么如果有人调用它,他们将获得超类实现,并在不应该进行比较的情况下获得结果。

一个例外

从Timestamp类的规范中,我们发现

注意:此类型是java.util.Date和单独的纳秒值的组合。 仅整数秒存储在java.util.Date组件中。 小数秒-纳米-是分开的。 传递不是java.sql.Timestamp实例的对象时,Timestamp.equals(Object)方法从不返回true,因为日期的nanos组件是未知的。 结果,Timestamp.equals(Object)方法相对于java.util.Date.equals(Object)方法不对称。 另外,hashcode方法使用底层的java.util.Date实现,因此在其计算中不包括nanos。
不好吧? 就是这样。 在那里也解释了原因

Timestamp和java.util.Date之间的继承关系实际上表示实现继承,而不是类型继承。
换句话说,他们说:“我们做到了,但是没有做到”。 我们要争论谁?

现在,无论何时重写equals方法,都必须重写hashCode方法。

我们已经看到了equals方法,所以现在让我们继续...

重写hashCode方法。

equals方法的协定实际上应该有另一行,表示您必须在覆盖equals方法之后继续覆盖hashCode方法。 支持hashCode方法是为了使基于哈希的集合受益。

合同

再次从规格:

  • 只要在应用程序执行期间在同一对象上多次调用它,则hashCode方法必须一致地返回相同的整数,前提是不修改该对象的equals比较中使用的信息。 从一个应用程序的执行到同一应用程序的另一执行,此整数不必保持一致。
  • 如果根据equals(Object)方法,两个对象相等,则在两个对象中的每个对象上调用hashCode方法必须产生相同的整数结果。
  • 根据equals方法,如果两个对象不相等是不必需的,则在两个对象中的每个对象上调用hashCode方法必须产生不同的整数结果。 但是,程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。
因此,相等的对象必须具有相等的hashCodes。 确保始终满足此条件的一种简单方法是在确定hashCode时使用与确定相等性相同的属性。 现在,您应该明白为什么每次覆盖均值时都必须覆盖hashCode的重要性。
旧的哈希表和存储桶示例

将哈希表想象成一组存储桶。 添加键值对时,键的hashCode用于确定放置映射的存储桶。

同样,当您在哈希表上使用键调用get方法时,键的hashCode用于确定映射存储在哪个存储桶中。 这是(顺序)搜索映射的存储桶。 如果您有两个“相等”的对象,但是具有不同的hashCode,则哈希表会将它们视为不同的对象,并将它们放在不同的存储桶中。 同样,您只能通过传递具有与您尝试检索的对象相同的hashCode的对象来从哈希表中检索对象。 如果找不到匹配的hashCode,则返回null。

因此,让我们再说一遍,“相等的对象必须具有相等的hashCodes”。

惰性哈希码

现在,如果您的hashCode方法返回相同的常数值,则每个映射都存储在相同的存储桶中,并且您将哈希表简化为(禁止上帝使用)LinkedList。 这是合同第三部分的内容。允许具有相同hashCode的两个不相等对象,但是这会使哈希表非常慢。 最好的hashCode

相反的情况是使所有不相等的对象具有不相等的hashCodes。 这意味着每个映射都存储在其自己的存储桶中。 这是哈希表的最佳情况,并且会导致线性搜索时间,因为仅需要搜索正确的存储桶。 找到正确的存储桶后,搜索完成。 这就是API文档说的原因
但是,程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。
好了,现在我们知道在hashCode中需要什么和不希望什么。 让我们看看如何创建它。

我们希望它以某种方式链接到equals方法,因此它必须使用与equals方法相同的属性。

这只是一个整数

签名是
public native int hashCode()
。 这里要注意的关键是该方法返回一个整数。 这意味着我们应该尝试获取用于等于方法中确定相等性的所有属性的整数表示。 诀窍是,我们应该以确保始终为相同的属性值获得相同的int值的方式获取此整数表示形式。

一旦有了整数,我们就可以找到一种将它们组合成一个代表我们对象的hashCode的整数的方法。

一种方法

一种常见的方法是选择一个乘数,例如p,然后通过应用以下公式来计算int值

hashCode =乘数* hashCode +所有属性的属性的hashCode。

对于三个属性(a1,a2,a3),将在以下步骤中计算hashCode

 hashCode =  multiplier  * hashCode  +  a1's hashCode //step 1
hashCode =  multiplier  * hashCode  +  a2's hashCode //step 2
hashCode =  multiplier  * hashCode  +  a3's hashCode //step 3 
您应该将hashCode初始化为某个值,以便第一个乘法不为0。

您的乘数最好应是质数,原因是最好的理由是让读者娱乐。

如果您仔细考虑一下,您会发现即使使用上述方法,两个不相等的对象仍可能具有相同的hashCode。 确保hashCode对于不相等的对象总是不相等,这绝非易事。 但是,无论您决定使用哪种算法,都要确保结果始终是整数,并且对于相同的对象,返回的整数将是相同的。

那么我们如何确定属性本身的hashCodes?

对于单个属性值,您可以使用以下常用方法,

  • 对于布尔变量,如果为true,则使用0;如果为false,则使用1。
  • 将byte,char或short转换为int很容易。 只是转换为int。 对于相同属性值,结果始终相同。
  • 长于整数。 您可以使用(int)value ^(value >>> 32)。 这是java.lang.Long类使用的方法。
  • 如果字段是浮点型,请使用Float.floatToIntBits(value)。
  • 如果该字段是双精度型,请使用Double.doubleToLongBits(value),然后使用上述用于long类型的方法对所得的long进行散列。
  • 如果字段是对象引用,并且此类的equals方法通过递归调用equals来比较字段,则还递归调用字段上的hashCode。
  • 如果该字段的值为null,则返回0(或其他常数,0更为常见,但您可能希望将其与布尔值区分)。
  • 最后,如果该字段是一个数组,请遍历每个元素并计算每个元素的hashCode值。 使用hashCodes的总和作为array属性的hashCode。
这并不困难。 这很麻烦,而且通常很无聊。 讨厌数学吗?

另一个数学方法较少的方法是将每个属性转换为字符串,将字符串连接起来,并使用结果字符串的hashCode。 您需要谨慎使用此属性将属性转换为字符串。

特别是在属性上调用toString方法可能不是正确的方法,因为toString方法可能为相等的对象返回不同的字符串!

优化它

计算对象的hashCode可能会变得相当复杂,有时会很耗时。 特别是当您有许多数组属性时。

事实证明,通过将hashCode计算并存储在变量中,您可以避免不必要地为不变类重新计算相同的hashCode。 随后对hashCode方法的调用将仅返回该(缓存的)值。 无需再次计算。

将其放在我们的Person类中,并对hashCode使用第一种方法,即可得出

最后一些代码

public class Person {
    String title;
    String fullName;
    int age;
    private volatile int hashCode = 0; 
    public Person(String title, String fullName, int age) {
        this.title = title;
        this.fullName = fullName;
        this.age = age;
    } 
     String getFullName() {
        return fullName;
    } 
    int getAge() {
        return age;
    } 
    String getTitle() {
        return title;
    } 
    public boolean equals(Object obj) {
        if(this == obj) {
            return true;
        }
        if (!(obj instanceof Person)) {
            return false; 
        }
        Person person = (Person)obj;
        return age == person.getAge() && fullName.equals(person.getFullName())
        && title.equals(person.getTitle()); 
    } 
    public int hashCode () {
        final int multiplier = 23;
        if (hashCode == 0) {
            int code = 133;
            code = multiplier * code + age;
            code = multiplier * code + fullName.hashCode();
            code = multiplier * code + title.hashCode();
            hashCode = code;
        }
        return hashCode;
    }
} 

该对象的hashCode仅计算一次。 请注意,hashCode已延迟初始化为零。 这仅在类是不可变的时才有效,因为一旦创建,唯一性属性值就不会更改,因此hashCode始终相同。 如果允许更改年龄,则hashCode也将更改,但返回的值仍将是缓存的值。 那将是一场灾难。

聪明的亚历克?

您可以使用此方法通过每次更改唯一性属性时重新计算缓存的值来尝试甚至针对可变类进行优化。 仅当您控制更改唯一性属性的所有可能方式时,才有可能。 小心一点,尽管您不会自己动手,但最终会为计算机创造更多的工作,同时尝试为计算机创造更少的工作!

结论

还有许多其他方法可以做到这一点,其中一些方法比上述方法更好,更彻底。 您当然应该努力学习另一种方法。

不要忘记hashCode的重要约定。 相等的对象必须具有相等的hashCodes。

这就是我拥有的hashCode。 希望有一天您会发现它有用。

From: https://bytes.com/topic/java/insights/723476-overriding-equals-hashcode-methods

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值