资讯专栏INFORMATION COLUMN

How to Override Equals in Java and Scala

Kahn / 2761人阅读

摘要:总之,在编写单个类的方法时比较简单,当涉及子类继承时,就要多考虑一下了。另外不要忘记覆盖方法哦。

相信读过 《Effective Java》 的读者都已经知道编写 equals 方法的作用与重要性,基本概念不多做解释,这里就总结一下如何编写正确的 equals 方法。

equals 在 Java 和 Scala 中含义相同,都需要满足以下五个条件:

自反性

对称性

传递性

一致性

anyObject.equals(null) == false

现在我们有三个问题:

假如我们只有一个类 Person,如何写?

假如 Person 类有一个子类 Student,相互不能判断(一定返回 false),如何写?相互可以判断,如何写?

假如 PersonStudent 可以相互判断,但另一子类 Teacher 只能和同类判断,如何写?

Java

《Effective Java》 中最后推荐的写法步骤是:

通过 == 判断是否是同一个对象

instanceof 判断是否是正确的类型,注意这里已经包含了 null 的情况,所以不用多带带另写

将对象转换成正确的类型

对需要判断的域分别进行对比

需要注意,基本类型用 == 判断,例外是 floatFloat.comparedoubleDouble.compare,因为有 NaN 等特殊值存在。

上述第二步中还有另一个变种,是使用 getClass 进行类型判断,这样的话只有类型完全一致才能返回 true,如果只是单一的类还好,要是涉及类之间的继承,则违背了 Liskov Substitution Principle,所以最后书中的结论是:

There is no way to extend an instantiable class and add a value component while preserving the equals contract.

由于现在的 IDE 例如 IntelliJ IDEA 已经可以自动为我们生成 equals 方法,还可以选择是否允许子类判断,是否可为 null 等判断,所以我们就不必手动编写了,但是生成的结果也是符合上面的 4 步的:

class Person{
    private String name;
    private int age;

    @Override 
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) { // 不涉及继承,问题 1 和 问题 2 前半的写法
            return false;
        }

        Person person = (Person) o;

        if (age != person.age) {
            return false;
        }
        return name != null ? name.equals(person.name) : person.name == null;
    }
    
    @Override 
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Person)) { // 涉及继承,使得与子类之间也可以判断,问题 2 后半的写法
            return false;
        }

        Person person = (Person) o;

        if (age != person.age) {
            return false;
        }
        return name != null ? name.equals(person.name) : person.name == null;
    }
}
Scala

scala 中编写的方式大致相同,但是结合其语法,相比似乎又简单又繁琐。
简单是指当没有子类,或和子类判断一定为 false 时(违反LSP),可以这样写:

class Person(val name: String, val age: Int) { 
  override def equals(other: Any): Boolean = other match { // 问题 1 的写法
    case that: this.getClass == that.getClass &&
                Person => name == that.name && 
                age == that.age
    case _ => false
  }
}

繁琐是指假如这时出现了一个子类 Student 且增加了一个域 sid,假如我们需要两个类可相互判断,则上述方法在判断一个 Person 对象和一个 Student 对象时一定会返回 false

因此《Programming in Scala》中建议采用如下的编写方式:

class Person(val name: String, val age: Int) {
  def canEqual(other: Any): Boolean = other.isInstanceOf[Person]

  override def equals(other: Any): Boolean = other match { // 问题 2 的写法
    case that: Person =>
      (that canEqual this) &&
        name == that.name &&
        age == that.age
    case _ => false
  }
}

class Student(override val name: String, override val age: Int, val sid: Int) extends Person(name, age){
}

上面 canEqual 方法的作用和 Java 代码中判断 instanceof 的作用是一致的,但比 Java 中的判断更加灵活,比如可以限定不同子类与父类的判断关系。

比如有一个 Person 的子类 Teacher,我们希望它只能和 Teacher 类进行判断,与 PersonStudent 判断都返回 false,该如何写呢?一种错误的写法如下:

class Teacher(override val name: String, override val age: Int, val tid: Int) extends Person(name, age){
  override def equals(other: Any): Boolean = other match {
    case that: Teacher =>
      this.getClass == that.getClass &&
        name == that.name &&
        age == that.age
    case _ => false
  }
}

val s1 = new Student("z", 1, 2)
val t1 = new Teacher("z", 1, 2)
println(s1 == t1) // true
println(t1 == s1) // false 违反了对称性

正确的写法应该是:

class Teacher(override val name: String, override val age: Int, val tid: Int) extends Person(name, age){
  override def canEqual(other: Any): Boolean = other.isInstanceOf[Teacher]

  override def equals(other: Any): Boolean = other match { // 问题 3 的写法
    case that: Teacher =>
      super.equals(that) &&
        (that canEqual this) &&
        name == that.name &&
        age == that.age &&
        tid == that.tid
    case _ => false
  }
}

注意只覆盖了 canEqual 方法也会违反对称性。在 Java 中要实现相同的效果,则也需要编写类似的 canEqual 方法,就留给读者自己考虑了。

总之,在编写单个类的 equals 方法时比较简单,当涉及子类继承时,就要多考虑一下了。

另外不要忘记覆盖 hashcode 方法哦。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68055.html

相关文章

  • 《Kotlin 极简教程 》第4章 基本数据类型与类型系统

    摘要:本章我们来学习一下的基本数据类型与类型系统。字符串就是一个抽象数据类型。如果程序语言的语法中含有类型标记,就称该语言是显式类型化的,否则就称为隐式类型化的。但是,可以把中对应的这几种基本数据类型,理解为的基本类型的装箱类。 第4章 基本数据类型与类型系统 《Kotlin极简教程》正式上架: 点击这里 > 去京东商城购买阅读 点击这里 > 去天猫商城购买阅读 非常感谢您亲爱的读...

    MoAir 评论0 收藏0
  • Scala类型推导

    摘要:提供了类型推导来解决这个问题。函数式语言里比较经典的类型推导的方法是,并且它是在里首先使用的。的类型推导有一点点不同,不过思想上是一致的推导所有的约束条件,然后统一到一个类型上。而推导器是所有类型推导器的基础。 Scala类型推导 之剑 2016.5.1 00:38:12 类型系统 什么是静态类型?为什么它们很有用? 根据Picrce的说法:类型系统是一个可以根据代码段计算出来的值对...

    SQC 评论0 收藏0
  • Kotlin框架巡礼

    摘要:框架官方支持的框架,风格颇为类似,并且充分发挥了的强类型优势。这是一个主要面向的框架,为提供了一些额外特性。依赖注入框架用法简单,支持等特性。 首先要说明,Kotlin支持你所知道的所有Java框架和库,包括但不限于Spring全家桶、Guice、Hibernate、MyBatis、Jackson等,甚至有人在用Kotlin写Spark大数据程序,因此Kotlin不需要专门的框架。因此...

    _Suqin 评论0 收藏0
  • Start Using Java Lambda Expressions(转载)

    摘要:原文 Introduction (Business Case) Lambda expressions are a new and important feature included in Java SE 8. A lambda expression provides a way to represent one method interface using an expression...

    FullStackDeveloper 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<