十一、处理相等性
在同一性和相等性之间有着明显的区别。如果两个事物实际上是相同的,那么它们就是相同的。如果你今天早上买了一支白蜡烛,姑且称之为 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?): Boolean
和fun hashCode(): Int
。如果您的类需要相等检查,您必须实现这两个。我们需要两个函数来进行相等性检查,这似乎有点奇怪。为什么只有equals()
用于相等性检查是不够的?原因在于性能,精确的思路后面再讲。
首先,我们声明,如果我们为一些a1
和a2
编写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.
我们观察到两件事:
-
只有当哈希代码查找成功时,
equals()
才会被调用。 -
为了使这个过程有意义,对于
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
}
}
如果提供比较的对象other
是null
或者不是Person
的实例,fun equals()
中的前两行返回null
。你会在几乎所有的equals()
实现中发现类似的代码行,尽管说你会在任何地方发现它们是夸张的;出于某种奇怪的原因,我们可能会接受与null
或其他类型的比较。
因为如果other
不是类型Person
我们就已经完成了,从第三行开始,Kotlin 知道other
是Person
的一个实例。这种自动类型检测有时被称为智能转换。接下来是对所有属性的逐步比较,只有当它们都匹配时,我们才返回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
如果两个变量a
和b
相同,下列哪一项是正确的?
-
a
和b
指的是同一个物体。 -
a == b
必然产生true
。 -
a !== b
必然产生false
。
练习 2
如果两个变量a
和b
相等,a == b
,下列哪一项是正确的?
-
a.equals(b)
一定是真的。 -
a != b
必然产生false
。 -
a.hashCode() == b.hashCode()
一定是真的。
版权属于:月萌API www.moonapi.com,转载请注明出处