类用主构造器直接声明字段与方法;object 是单例;样例类(case class) 是编译器替你生成相等性、拷贝与解构能力的不可变数据载体。本节是 Scala 数据建模的地基。
class Account(val owner: String, var balance: BigDecimal) { def deposit(amount: BigDecimal): Unit = balance = balance + amount def canAfford(price: BigDecimal): Boolean = balance >= price } val acc = new Account("Wang", 100) acc.deposit(50) acc.balance // 150
参数前的 val/var 决定它是否成为字段:val owner 只读,var balance 可变。什么都不写则只是构造参数,类外不可见。相比 Java 的"字段 + getter + setter + 构造器"三件套,一行顶十几行。
类体里还可以有辅助构造器,但 Scala 风格更倾向用工厂方法(见下面伴生对象)。
object RateTable { val standard: BigDecimal = 1.13 def apply(net: BigDecimal): BigDecimal = net * standard } RateTable(200) // 226.0 —— apply 让对象像函数一样调用
与类同名的 object 叫伴生对象,能访问类的私有成员,惯例上放工厂方法与常量。Scala 没有 static 关键字,静态成员全部住在伴生对象里——这让"一切皆对象"在语言层面没有例外。
case class Point(x: Int, y: Int) val p1 = Point(1, 2) val p2 = Point(1, 2) p1 == p2 // true,按值比较 p1.copy(y = 10) // Point(1, 10),改一列其余照抄 val Point(a, b) = p1 // 解构,a=1, b=2
样例类自动获得:按值的 equals 与 hashCode、可读 toString、copy、伴生对象里的 apply(所以不需要 new)与 unapply(供模式匹配解构)。注意 p1 == p2——普通类比的是引用,样例类比的是内容,这个差别在往集合里放、做去重时立刻显现。
| 场景 | 选择 | 原因 |
|---|---|---|
| 纯数据(消息、配置、事件) | 样例类 | 相等性、拷贝、模式匹配白送 |
| 有身份的实体(账户、会话) | 普通类 | 判等按引用,状态可变 |
| 无数据的类型标记 | 样例对象 | 全局唯一,模式匹配可作分支 |
样例对象示例:case object Idle,常与样例类组成状态机(3.3 节会大量使用)。
Scala 3 的 enum 是样例类的语法糖衣:
enum Color(val rgb: Int): case Red extends Color(0xFF0000) case Green extends Color(0x00FF00) case Blue extends Color(0x0000FF)

来看一个贴近真实的建模过程。背景是支付网关需要对账,每笔支付可能是成功、失败或待确认三种状态,且要携带不同的附加信息。面向对象的直觉是建一个 Payment 基类加三堆字段,样例类的答案干净得多:
enum PaymentState: case Succeeded(txnId: String, at: Long) case Failed(code: Int, reason: String) case Pending(expiresAt: Long) import PaymentState.* case class Payment(id: Long, amount: BigDecimal, state: PaymentState): def isSettled: Boolean = state match case Succeeded(_, _) => true case _ => false val p1 = Payment(1001, 59.9, Succeeded("TX-8", 1721000000L)) val p2 = p1.copy(amount = 69.9) // 基于旧值造新值,id 与 state 原样保留
操作过程只有三步:声明枚举与样例类、用 match 分支处理状态、用 copy 做无副作用修改。结果是编译器免费送了 equals、hashCode、toString、模式匹配支持与不可变复制;解读一下,样例类的本质是"代数数据类型"——状态空间被类型完整穷尽,少写一个分支编译器直接警告。变式也简单:若状态会随时间流转,把新状态做成新值(p1.copy(state = Pending(...))),旧值留作审计痕迹,这正是第三章不可变思想在对象世界的预演。
case class Money(cents: Int) val a = Money(500) val b = Money(500) a == b // true:按值比较,不是按引用 Money(500).hashCode == Money(500).hashCode // true:可安全放进 Set/Map println(a) // Money(500):可直接打印
普通类这三条全部相反:== 比引用、hashCode 按身份、打印是地址。经验法则随之确立——数据用 case class,有身份概念的实体(如活跃的连接对象)才用普通 class。
把样例类实例 match 到两个字段但只声明了一个,报 pattern mismatch;或者对普通类的实例用样例类风格的模式匹配,直接找不到 extractor。排错方向就一句:模式匹配的字段形状必须与构造形状一致。
val/var 决定字段可见性与可变性。apply 提供函数式调用外观。