十一、处理相等性

同一性相等性之间有着明显的区别。如果两个事物实际上是相同的,那么它们就是相同的。如果你今天早上买了一支白蜡烛,姑且称之为 A,你购物袋里的白蜡烛和今天下午放在烛台上的白蜡烛是一样的,因此完全相同(假设这是你拥有的唯一一支蜡烛)。现在假设你从同一个制造商那里买了第二支相同型号的蜡烛 B。除了你有时会听到的一些语言错误,这两根蜡烛是相同的。蜡烛 A 和 B 不一样,但是等于。这是因为它们具有相同的特征:相同的颜色、相同的重量、相同的直径和相同的长度。但是,打住:这不一定是真的。制造商称这种蜡烛重 300 克,但是高精度天平告诉我们蜡烛 A 重 300.00245 克,蜡烛 B 重 299.99734 克,但是如果你用厨房秤,蜡烛 A 和 B 的重量是一样的。因此,你可以看到,相等性取决于严格,它是相对的。

同一性和相等性之间的比较给我们上了重要的一课:同一性适用于相同的事物,而相等性是相对的,取决于某种定义。

Kotlin 的同一性

在 Kotlin 中有一个恒等运算符===和它的反义词!==。在 Kotlin,同一性代表参照同一性,这意味着如果两个变量指向同一个对象,或者引用同一个对象,它们被认为是相同的:

data class A(val x:Double)
val a = A(7.0)
val b = A(7.0)
val c = a
val aIdenticalToC = a === c // -> true
val aIdenticalToB = a === b // -> false

实际上,您可能不会经常使用标识。在大多数情况下,让不同的变量指向同一个对象无论如何都不是好的编码风格,此外,同一性对于不同的程序流来说没有太大的不同。最后,尽管两个对象中的所有属性都具有相同的值,但是这两个对象的比较结果都为 false,这是令人困惑的,并且会影响代码的可读性。因此,同一性检查的实际用途是有限的。

注意

在数据库环境中,还有另一个同一性的概念。那里通常有一个用于数据记录的数字 ID 字段。这个字段被用作相应对象标识的代理,而不是语言的引用标识===。在本章中,我们不讨论这种数据库类型的同一性。

Kotlin 的相等性

对于等式,Kotlin 提供了比较运算符==,以及它的反义词!=。除了标识之外,一个对象必须告诉它是否等于其他对象。如果不显式地这样做,将使用相等检查的基本实现,这又回到了同一性检查。

对数字、布尔值、字符和字符串的相等性检查做了显而易见的事情:如果字符串包含完全相同的字符,则它们相等;如果字符包含相同的字母,则它们相等;如果数字和布尔值具有相同的值,则它们相等。

等于和哈希代码

类处理相等性检查的方式由两个函数控制:fun equals(otherObject:Any?): Booleanfun hashCode(): Int。如果您的类需要相等检查,您必须实现这两个。我们需要两个函数来进行相等性检查,这似乎有点奇怪。为什么只有equals()用于相等性检查是不够的?原因在于性能,精确的思路后面再讲。

首先,我们声明,如果我们为一些a1a2编写a1 == a2作为类A的实例,函数equals()在类A上被调用,并且只有当它返回true时,比较的结果也是true。对于==等式检查,那么equals()函数实际上就足够了。

对于地图,情况就不同了。例如,如果我们有一个映射,将某个类A的实例映射到任何对象

class A(val v:Int) {

    override fun hashCode():Int {
        return ...
    }
    override fun equals(other:Any?):Boolean {
        return ...
    }
}

val m = mapOf(A(7) to 8, A(8) to 9)

然后执行查找,如

val searchKey:A = ...
m[searchKey]

实际情况是这样的:

  • 通过对其调用hashCode()来计算searchKey的散列码。

  • []操作符(或get()函数)应用一种非常快速的算法,根据整数散列键找到一个条目。

  • 对于在散列关键字查找期间找到的条目,对所有可能的条目调用equals()。如果equals()找到了精确的条目,[]操作符返回该条目的相应值。

  • 如果哈希键查找失败或者所有后续的equals()检查失败,那么[]操作符也会失败并返回null.

我们观察到两件事:

  1. 只有当哈希代码查找成功时,equals()才会被调用。

  2. 为了使这个过程有意义,对于hashCode()函数,以下条件必须为真:(1)如果a == b,我们也需要a.hashCode() == b.hashCode()。②如果说a != b,在大多数情况下我们也应该有a.hashCode() != b.hashCode()。如果(1)不为真,地图查找功能将失败,如果(2)不为真,我们将不得不经常调用equals()

作为一个例子,考虑类

class Person(val lastName:String,
    val firstName:String,
    val birthday:String,
    val gender:Char)

我们基于所有属性实现了一个equals()函数:

class Person(val lastName:String,
      val firstName:String,
      val birthday:String,
      val gender:Char) {
    override fun equals(other:Any?):Boolean {
        if(other == null) return false
        if(other !is Person) return false
        if(lastName != other.lastName) return false
        if(firstName != other.firstName) return false
        if(birthday != other.birthday) return false
        if(gender != other.gender) return false
        return true
    }
}

如果提供比较的对象othernull或者不是Person的实例,fun equals()中的前两行返回null。你会在几乎所有的equals()实现中发现类似的代码行,尽管说你会在任何地方发现它们是夸张的;出于某种奇怪的原因,我们可能会接受与null或其他类型的比较。

因为如果other不是类型Person我们就已经完成了,从第三行开始,Kotlin 知道otherPerson的一个实例。这种自动类型检测有时被称为智能转换。接下来是对所有属性的逐步比较,只有当它们都匹配时,我们才返回true

对于一个hashCode()函数,你可能会想到很多算法,在网上你也会找到一些关于它的想法。幸运的是,我们不必在这方面花费太多的脑力;包java.util中的对象Objects为此提供了一个方便的函数,我们可以写:

class Person(val lastName:String,
      val firstName:String,
      val birthday:String,
      val gender:Char) {
    override fun equals(other:Any?):Boolean {
        if(other == null) return false
        if(other !is Person) return false
        if(lastName != other.lastName) return false
        if(firstName != other.firstName) return false
        if(birthday != other.birthday) return false
        if(gender != other.gender) return false
        return true
    }

    override fun hashCode(): Int {
        return Objects.hash(super.hashCode(),
            lastName, firstName, birthday, gender)
    }
}

对于这种明显的情况,即等式依赖于检查是否相等的所有属性,Kotlin 有一个捷径。我们已经讲过:数据类。它们完全基于所有属性实现了一个equals()和一个hashCode()函数。对于Person类,我们可以删除显式的equals()hashCode()函数,只需编写

data class Person(val lastName:String,
      val firstName:String,
      val birthday:String,
      val gender:Char)

练习 1

如果两个变量ab相同,下列哪一项是正确的?

  1. ab指的是同一个物体。

  2. a == b必然产生true

  3. a !== b必然产生false

练习 2

如果两个变量ab相等,a == b,下列哪一项是正确的?

  1. a.equals(b)一定是真的。

  2. a != b必然产生false

  3. a.hashCode() == b.hashCode()一定是真的。