1. 概述
在这篇文章中,我们将回顾 Scala 中的相等性概念,并与 Java 中的相等性机制 做一些对比。
2. 运算符 ==
和 !=
在 Scala 中,单参数方法支持中缀写法,因此我们通常将它们称为“运算符”。
比如 ==
和 !=
实际上是类 Any
的方法,而 Any
是 Scala 类型体系的根类。这两个方法接收另一个 Any
类型的参数,并返回一个 Boolean
类型的结果。由于 !=
只是 ==
的否定形式,所以只需理解 ==
的行为即可。
对于 AnyVal
类型(对应 Java 的基本类型),==
的行为与 Java 中一致,用于比较两个值是否相等:
val intAnyVal = 4
assert(intAnyVal == 2 * 2)
而对于引用类型,情况有所不同。与 Java 不同的是,Scala 的 ==
并不直接比较引用,而是会转换为对 AnyRef
实例的 null 安全的 equals()
调用:
val firstString = new String("AnyRef")
val secondString = new String("AnyRef")
val thirdString = null
val fourthString = null
assert(firstString == secondString)
// 与 Java 不同,以下代码不会抛出 NullPointerException
assert(thirdString != secondString)
assert(fourthString == thirdString)
虽然这不是 Scala 推荐的写法,但显式调用 equals()
也是允许的:
val firstString = new String("AnyRef")
val secondString = new String("AnyRef")
assert(firstString.equals(secondString))
3. 运算符 eq
和 ne
重写的 equals()
方法定义的是“结构相等性”(structural equality)。
但有时候我们需要更强的判断条件:两个引用是否指向堆中的同一个对象,也就是所谓的“引用相等性”(referential equality)。
Scala 提供了 eq
和它的否定形式 ne
来处理这种情况:
val firstString = new String("AnyRef")
val secondString = new String("AnyRef")
val thirdString = secondString
assert(firstString ne secondString)
assert(thirdString eq secondString)
// 这两个运算符对 null 也是安全的
assert(null eq null)
assert(null ne firstString)
4. 重写 equals()
和 hashCode()
虽然 Scala 的类型推断和语法糖让它比 Java 更简洁,但它并没有像 Java 那样提供默认的相等性语义。
除非你使用的是 case class
,Scala 编译器会自动为你生成合适的 equals()
和 hashCode()
方法,否则在普通的 Scala 类中,你需要手动重写这两个方法。否则,相等性判断可能不会如你所愿。
我们先来看一个简单的类定义:
class PersonSimpleClass(val name: String, val age: Int)
由于我们没有重写 equals()
方法,两个实例即使内容相同也不会被认为相等:
val firstSimpleClassInstance = new PersonSimpleClass("Donald", 66)
val secondSimpleClassInstance = new PersonSimpleClass("Donald", 66)
assert(firstSimpleClassInstance != secondSimpleClassInstance)
为了解决这个问题,我们可以手动重写这两个方法:
class PersonClassWithOverrides(val name: String, val age: Int) {
override def equals(other: Any): Boolean = other match {
case person: PersonClassWithOverrides =>
this.name == person.name && this.age == person.age
case _ => false
}
override def hashCode(): Int = if (name eq null) age else name.hashCode + 31 * age
}
注意,PersonClassWithOverrides
的 equals()
实现相比 Java 更加简洁,这主要得益于 Scala 的模式匹配和 ==
运算符的 null 安全性。此时,==
的行为就符合预期了:
val firstClassWithOverridesInstance = new PersonClassWithOverrides("Donald", 66)
val secondClassWithOverridesInstance = new PersonClassWithOverrides("Donald", 66)
assert(firstClassWithOverridesInstance == secondClassWithOverridesInstance)
最后,最简单的方式是使用 case class
:
case class PersonCaseClass(name: String, age: Int)
除了其他便利特性,case class
会由编译器自动生成 equals()
和 hashCode()
方法。因此我们可以放心地用它来判断相等性:
val firstCaseClassInstance = PersonCaseClass("Donald", 66)
val secondCaseClassInstance = PersonCaseClass("Donald", 66)
assert(firstCaseClassInstance == secondCaseClassInstance)
5. 总结
本文我们回顾了 Scala 中的相等性概念,并列举了相关特性,帮助你从 Java 平滑过渡到 Scala。
一如既往,本文的所有代码都可以在 GitHub 上找到。