Logo
Section 1 / 2

引用相等性与结构相等性

Kotlin 中两种基本的相等类型及其使用方法

检查点

引用相等性与结构相等性

Kotlin 中两种基本的相等类型及其使用方法

定义

在任何编程语言中,我们有两种方式来比较对象并检查它们是否相等:

  • 引用相等性,
  • 结构相等性。

引用相等性指的是两个对象指向内存中完全相同的对象,即它们使用相同的引用。

结构相等性则意味着两个对象具有相同的值。尽管如此,它们仍可能指向内存中的不同位置,即它们不一定持有相同的引用。

注意: 引用相等性蕴含结构相等性。

在 Kotlin 中,我们为这两种相等性提供了对应的运算符:

  • 使用 === 检查引用相等性
  • 使用 == 检查结构相等性

代码示例

1val starters1 = listOf("charmander", "squirtle", "bulbasaur") // 一个列表对象 2val starters2 = listOf("charmander", "squirtle", "bulbasaur") // 另一个内容相同但不同的列表 3 4println(starters1 === starters2) // false:变量指向不同的内存位置 5println(starters1 == starters2) // true:变量持有的值相等 6println(starters1 !== starters2) // true:第一个比较的否定 7println(starters1 != starters2) // false:第二个比较的否定

两个对象是否结构相等取决于它们的类型,更具体地说,取决于其 equals 方法的实现方式。

实际上,Kotlin 的 == 运算符用于结构相等时会调用对象的 equals 方法进行比较。Kotlin 默认的 a == b 实现是 a?.equals(b) ?: (b === null)。这意味着:

  • 如果 a 不为 null,则它必须与 b 相等(根据其 equals 方法的实现)
  • 如果 a 为 null,则 b 也必须为 null

注意: Kotlin 会自动将 a == null 转换为 a === null。因此你无需手动做这种微优化。

equals 的特性

任何 equals 方法的实现都必须满足以下性质:

  • 自反性: 对于任意非空值 x,x.equals(x) 应返回 true。
  • 对称性: 对于任意非空值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才返回 true。
  • 传递性: 对于任意非空值 x、y 和 z,如果 x.equals(y) 返回 true 且 y.equals(z) 返回 true,则 x.equals(z) 也应返回 true。
  • 一致性: 对于任意非空值 x 和 y,在未修改 equals 比较所依赖的信息的前提下,多次调用 x.equals(y) 会一致地返回 true 或一致地返回 false。
  • 永不等于 null: 对于任意非空值 x,x.equals(null) 应返回 false。

练习

一种奇怪的现象

让我们探索 JVM 的一个特殊行为:

  • 编写一个返回类型为 Int? 的函数,并让它返回某个固定的整数(忽略编译器提示“你总是返回非空值”的警告)
  • 创建两个类型为 Int? 的变量,并调用你的函数为其赋值
  • 检查它们是否 a) 结构相等,以及 b) 引用相等
  • 探究当函数返回值在 -128 到 127 之间时会发生什么,以及当返回值超出此范围时又会发生什么

注意: 我们使用可空的 Int? 类型,是为了让编译器在运行时将其作为 JVM 上的 Integer 对象处理,而不是原始类型 int。

解答

1fun couldBeNull(): Int? = 128 // 尝试改为 127 2 3fun main() { 4 val nullable1: Int? = couldBeNull() 5 val nullable2: Int? = couldBeNull() 6 7 println("nullable1 == nullable2: ${nullable1 == nullable2}") 8 println("nullable1 === nullable2: ${nullable1 === nullable2}") 9}

你可能会认为这两个整数对象永远不会是引用相等的,因为它们不是基本类型。

然而,对于 -128 到 127 之间的较小数值,它们实际上共享了相同的引用。这是因为 Java(以及 Kotlin)内部使用 Integer.valueOf() 将你的整数字面量(例如 127)转换为 Integer 对象——而该方法会对 -128 到 127 之间的整数进行缓存。

提示: 你应该从中吸取什么教训?为了避免这类情况,应始终使用 == 来比较 Int? 和其他“可空基本类型”。

总结

现在你已经清楚地了解了 Kotlin 中 == 和 === 的工作原理及其区别:

  • == 检查结构相等性,即两个对象是否根据其 equals 方法的实现被认为是“相等”的
  • === 检查引用相等性,即两个变量是否指向内存中的同一位置
  • 应谨慎使用 ===

在下一课中,你将学习浮点数相等性的具体细节。

Quiz

Multiple Choice

以下哪些是结构上相等的?

你可以选择多个选项。

1 / 2

函数

浮点数相等性检查