1. 概述

在本教程中,我们将深入探讨 Scala 中 case objectobject 的概念。

我们会对比它们之间的核心差异,并讨论 case object 所具备的一些额外特性。

2. Scala Object

Scala 的 object 表示一个类只有一个实例 —— 即单例对象(Singleton)。

下面是一个 object 的例子:

object Car {
  val numberOfWheels = 4

  def run(): Unit = {
    val currentDateAndTime: Date = new Date(System.currentTimeMillis())
    println(s"I am a new car running on $currentDateAndTime!")
  }
}

与类不同的是,**object 不允许在构造时传入参数**。使用 object 创建单例非常适合那些不可变的、全局唯一的实例。

3. Case Object 与 Object 的区别

这一章节我们先来看一个简单的 case object 示例,然后重点分析它和普通 object 的主要差异。

来看一个 case object 的例子:

case object Bicycle {
  val numberOfWheels = 2

  def run(): Unit = {
    val currentDateAndTime: Date = new Date(System.currentTimeMillis())
  }
}

从语法上看,唯一的不同就是前面加了关键字 case。但功能上,✅ case object 继承了 object 的所有特性,并在此基础上扩展了一些实用功能

  • 默认实现序列化支持
  • 支持模式匹配(Pattern Matching)
  • 更方便地用于枚举场景
  • 默认实现了 toString 方法

下面我们逐个来看这些增强功能。

3.1. 序列化支持

case object 默认实现了 Serializable 接口,而普通 object 则没有。

可以通过如下测试验证:

class ObjectExampleUnitTest extends AnyFlatSpec {

  "Bicycle" should "be an instance of Serializable" in {
    assert(Bicycle.isInstanceOf[Serializable])
  }

  "Car" should "not be an instance of Serializable" in {
    assert(!Car.isInstanceOf[Serializable])
  }
}

如果想让普通 object 可序列化,需要手动继承 Serializable trait:

object Car extends Serializable {
  val numberOfWheels = 4

  def run(): Unit = {
    val currentDateAndTime: Date = new Date(System.currentTimeMillis())
  }
}

3.2. 模式匹配支持

case object 在模式匹配中表现更友好。

首先定义一个抽象类 Vehicle,并让 CarBicycle 都继承它:

abstract class Vehicle
object Car extends Vehicle
case object Bicycle extends Vehicle

接着创建一个方法进行模式匹配:

def messageVehicle(vehicle: Vehicle): Unit = {
  vehicle match {
    case Car => println("send message to Car")
    case Bicycle => println("send message to Bicycle")
  }
}

当调用:

messageVehicle(Car)

输出结果为:

send message to Car

⚠️ 注意:虽然 object 也能参与模式匹配,但在某些场景下(如编译器检查)不如 case object 安全直观。

3.3. 枚举用途

objectcase object 都可以用来实现枚举类型,但方式和效果略有不同。

使用 object 实现枚举

Scala 没有像 Java 那样的 enum 关键字,而是提供了 Enumeration 类来模拟枚举:

object FlyingObject extends Enumeration {
  val airplane = Value("AP")
  val bird = Value("BD")
  val drone = Value("DE")
}

访问成员的方式包括:

println(FlyingObject.bird.id) // 输出 ID

默认情况下,ID 是按顺序递增分配的。也可以手动指定 ID:

object FlyingObjectChangingID extends Enumeration {
  val airplane = Value(2, "AP")
  val bird = Value(3, "BD")
  val drone = Value(1, "DE")
}

此时顺序会变成 "DE", "AP", "BD"

❌ 但是有个问题:在做模式匹配时不会触发编译期的完整性检查。例如:

def nonExhaustive(objects: FlyingObject.Value) {
  objects match {
    case FlyingObject.airplane => println("I am an airplane")
    case FlyingObject.bird => println("I am a bird")
  }
}

nonExhaustive(FlyingObject.drone) // 运行时报错:scala.MatchError

使用 sealed case object 实现安全枚举

为了规避这个问题,推荐使用 sealed trait + case object 的组合:

sealed trait FlyingCaseObjects
case object AirplaneCaseObject extends FlyingCaseObjects
case object BirdCaseObject extends FlyingCaseObjects
case object DroneCaseObject extends FlyingCaseObjects

✅ 编译器能识别所有子类型,从而在模式匹配不完整时给出警告:

def sealedTraitMatch(flyingObject: FlyingCaseObjects): Unit = {
  flyingObject match {
    case AirplaneCaseObject => println("I am an airplane")
    case BirdCaseObject => println("I am a bird")
    // 缺少 DroneCaseObject 的 case,编译器会报 warning
  }
}

这种方式比传统的 Enumeration 更现代、更安全。

3.4. toString 方法的默认实现

最后一个区别是 toString 方法的默认行为。

执行以下代码:

println(Car)
println(Bicycle)

输出结果如下:

com.baeldung.scala.caseobject.Car$@2f7c7260
Bicycle

case object 自动重写了 toString 方法返回其名称,而普通 object 则输出的是默认哈希地址字符串

这使得调试和日志打印更加清晰明了。

4. 总结

在这篇文章中,我们比较了 Scala 中 case objectobject 的异同点:

  • 语法上,case object 只是多了个 case
  • 功能上,case object 增强了序列化、模式匹配、枚举支持和 toString 实现;
  • 在实际开发中,如果需要使用到模式匹配、枚举或序列化等场景,建议优先考虑使用 case object
  • 对于只需要单例功能的对象,普通 object 已足够。

一如既往,完整代码可从 GitHub 获取。


原始标题:Difference Between Case Object and Object