第7章 面向对象进阶
继承、抽象类、case class 与 sealed:如何让数据建模既安全又好用,是 Scala 工程代码的常见形态。
本章学习目标
- 会写继承、override、super、抽象类与多态
- 能用 case class 替代样板式 JavaBean,并知道它自动生成了什么
- 能用 sealed trait + match 写出穷尽检查的数据建模
- 理解不可变数据模型在并发与测试中的好处
本章导览
核心内容
7.1
继承与抽象类:override、super 与多态
用抽象类定义骨架,用 override、super 实现多态。
能定义抽象类并让多个子类实现抽象成员能正确使用 override 与 super 改写父类行为能通过父类类型调用子类实现,理解运行时多态
知识点
抽象类负责定义“必须有什么”
- abstract class 不能 new,可以同时包含抽象成员和已经实现的方法。
- 抽象成员只有签名,没有方法体,子类负责补全。
- 需要构造参数或共享字段时,抽象类比 trait 更合适。
override 是明确的重写声明
- 覆盖父类的具体成员必须写 override,漏写会直接编译报错。
- 实现抽象成员时 override 可写可不写,显式写出来更清楚。
- 重载(参数列表不同)不是重写,不要为了消除报错乱加 override。
super 与运行时多态
- super.m() 调用父类版本,m() 或 this.m() 走运行时类型的方法。
- 父类方法里调用另一个可覆盖方法时,实际执行的是子类版本。
- 把子类对象交给父类类型的变量或集合,就是多态:同一调用产生不同结果。
代码示例
▶ 抽象类 Shape 与两个子类的面积计算
abstract class Shape {
def area: Double
def describe: String = "area=" + f"$area%.2f"
}
class Circle(val r: Double) extends Shape {
override def area: Double = math.Pi * r * r
}
class Square(val side: Double) extends Shape {
override def area: Double = side * side
}
object Main {
def main(args: Array[String]): Unit = {
val shapes = List[Shape](new Circle(1.0), new Square(2.0))
shapes.foreach(s => println(s.describe))
}
}👀 运行结果
area=3.14 area=4.00
▶ super 调用父类模板,动态派发到子类方法
class Animal {
def speak(): String = "..."
def intro(): String = s"I say ${speak()}"
}
class Dog extends Animal {
override def speak(): String = "Woof"
override def intro(): String = "Dog: " + super.intro()
}
object Main {
def main(args: Array[String]): Unit = {
val d = new Dog
println(d.intro())
println(d.speak())
val a: Animal = d
println(a.speak())
}
}👀 运行结果
Dog: I say Woof Woof Woof
▶ 表达式树:父类类型统一操作不同子类
abstract class Expr {
def eval: Int
}
class Num(val n: Int) extends Expr {
override def eval: Int = n
}
class Add(val left: Expr, val right: Expr) extends Expr {
override def eval: Int = left.eval + right.eval
}
object Main {
def main(args: Array[String]): Unit = {
val e: Expr = new Add(new Num(1), new Add(new Num(2), new Num(3)))
println(e.eval)
}
}👀 运行结果
6
常见错误
⚠️ 踩坑与解决
new Shape 实例化抽象类
→ abstract class 不能直接创建实例,编译报 class Shape is abstract; cannot be instantiated。;new 它的具体子类,例如 new Circle(1.0)。覆盖父类具体方法时漏写 override
→ 编译器把同名同参数的方法视为非法重定义,报 method ... needs override modifier。;在 def 前加 override;如果其实想新增重载,就改参数列表。子类忘了给父类构造器传参
→ 父类没有无参构造器时,子类 extends 后必须提供参数,否则编译报参数不足。;写成 class Dog(name: String) extends Animal(name),先满足父类再初始化自己。
💡 依赖抽象类型而不是具体子类;新增子类时不用改调用方,这才是继承带来的好处。
7.2
case class:样例类与自动生成的方法
用 case class 少写样板代码,获得比较、复制与匹配。
能定义 case class 并使用自动生成的 apply、copy、toString能解释结构相等与引用相等的区别能用 case class 做去重、分组和模式匹配
知识点
一行 case class 自动生成什么
- 伴生对象自动带 apply,可直接写 Point(1, 2),不必 new。
- 自动实现 equals、hashCode、toString,还能用 copy 复制并替换字段。
- 自动带 unapply,可直接 case Point(x, y) 解构。
适合做不可变数据载体
- 参数默认是 val,创建后字段不能重新赋值。
- “修改”用 copy:p.copy(y = 5) 返回新对象,原对象完全不变。
- 样例类可以当 Map 的键,因为内容相同的对象 equals 与 hashCode 也相同。
结构相等与引用相等
- case class 的 == 比较字段内容,两个不同实例只要字段相同就是 true。
- 普通 class 不重写 equals 时,== 等价于引用比较,两个 new 出来的对象通常不相等。
- 需要按内容去重、查找、比较时,优先 case class。
代码示例
▶ case class 的打印、取值、copy 与相等判断
case class Point(x: Int, y: Int)
object Main {
def main(args: Array[String]): Unit = {
val p = Point(1, 2)
println(p)
println(p.x)
println(p.copy(y = 5))
println(p == Point(1, 2))
println(p == Point(1, 3))
println(p.productArity)
}
}👀 运行结果
Point(1,2) 1 Point(1,5) true false 2
▶ case class 的模式匹配、copy 与 productIterator
case class User(name: String, age: Int)
object Main {
def main(args: Array[String]): Unit = {
val u = User("Alice", 20)
u match {
case User(n, a) => println(s"$n is $a")
case _ => println("other")
}
val older = u.copy(age = 21)
println(older)
println(u.productIterator.toList)
}
}👀 运行结果
Alice is 20 User(Alice,21) List(Alice, 20)
▶ 按内容去重、排序和分组
case class Book(title: String, price: Double)
object Main {
def main(args: Array[String]): Unit = {
val books = List(Book("Scala", 59.0), Book("Python", 45.0), Book("Scala", 59.0))
println(books.distinct)
println(books.distinct.size)
println(books.distinct.sortBy(_.price))
println(books.groupBy(_.title).keys.toList.sorted)
}
}👀 运行结果
List(Book(Scala,59.0), Book(Python,45.0)) 2 List(Book(Python,45.0), Book(Scala,59.0)) List(Python, Scala)
常见错误
⚠️ 踩坑与解决
拿普通 class 和 case class 比 ==,结果不同
→ 普通 class 默认继承 AnyRef 的 equals(引用相等),case class 自动生成按字段比较的 equals。;数据类改成 case class;必须用普通 class 时手动重写 equals 和 hashCode。把 case class 的字段声明成 var,放进 Set 后再修改
→ equals 与 hashCode 基于字段,对象作为 Set 元素或 Map 键后修改字段会破坏哈希查找。;保持字段为 val,改值用 copy;确实需要可变状态就改用普通 class。写 p.x = 3 想修改 case class 字段
→ case class 的参数默认是 val,重新赋值编译报 reassignment to val。;用 p.copy(x = 3) 得到新对象,原对象保持不变。
💡 只要这个类型主要在装数据,就用 case class;行为复杂、需要封装可变状态的再写普通类。
7.3
sealed + match:用模式匹配做数据建模
用 sealed 限定类型范围,用穷尽 match 拆解数据。
能用 sealed trait 加 case class、case object 建模有限状态能写出覆盖全部分支的 match 表达式能理解非穷尽匹配的警告与运行时 MatchError
知识点
sealed 把子类型锁在同一文件
- sealed trait 的直接子类必须写在同一个文件,编译器因此知道所有可能分支。
- 通常用 case class 携带数据,用 case object 表示无参分支。
- 有限集合不要开放继承,sealed 能把“还有哪些情况”交给编译器检查。
match 是表达式,不是语句
- 每个 case 返回一个值,整个 match 可以直接赋给变量或作为方法返回值。
- case class 的 unapply 负责把字段拆出来,case Circle(r) 就是解构。
- 守卫 if 写在模式后面,用来补充条件而不再嵌套 if。
穷尽性与可扩展性
- match 没有覆盖所有子类型时,编译器给出 non-exhaustive match 警告。
- 警告不是错误,运行时碰到漏掉的分支才会抛 MatchError。
- 新增一个 case class 后,所有未更新的 match 会立刻出现警告,这是 sealed 的价值。
代码示例
▶ sealed trait Shape 加穷尽 match 计算面积
sealed trait Shape
case class Circle(r: Double) extends Shape
case class Rect(w: Double, h: Double) extends Shape
object Main {
def area(s: Shape): Double = s match {
case Circle(r) => math.Pi * r * r
case Rect(w, h) => w * h
}
def main(args: Array[String]): Unit = {
println(f"${area(Circle(1.0))}%.2f")
println(f"${area(Rect(2.0, 3.0))}%.2f")
}
}👀 运行结果
3.14 6.00
▶ 递归代数数据类型:用 match 解释表达式
sealed trait Expr
case class Num(n: Int) extends Expr
case class Add(l: Expr, r: Expr) extends Expr
case class Mul(l: Expr, r: Expr) extends Expr
object Main {
def eval(e: Expr): Int = e match {
case Num(n) => n
case Add(l, r) => eval(l) + eval(r)
case Mul(l, r) => eval(l) * eval(r)
}
def main(args: Array[String]): Unit = {
val e = Add(Num(2), Mul(Num(3), Num(4)))
println(eval(e))
println(eval(Add(Num(1), Num(2))))
}
}👀 运行结果
14 3
▶ case object 无参分支加带守卫的 match
sealed trait Payment
case class Cash(amount: Double) extends Payment
case class Card(amount: Double, last4: String) extends Payment
case class Coupon(code: String) extends Payment
case object Free extends Payment
object Main {
def describe(p: Payment): String = p match {
case Cash(a) => f"cash $a%.2f"
case Card(a, last4) => f"card $a%.2f (****$last4)"
case Coupon(c) if c.nonEmpty => s"coupon $c"
case Coupon(_) => "invalid coupon"
case Free => "free"
}
def main(args: Array[String]): Unit = {
println(describe(Cash(12.5)))
println(describe(Card(88.0, "1234")))
println(describe(Coupon("SAVE10")))
println(describe(Coupon("")))
println(describe(Free))
}
}👀 运行结果
cash 12.50 card 88.00 (****1234) coupon SAVE10 invalid coupon free
常见错误
⚠️ 踩坑与解决
match 少写一个 sealed 子类分支
→ 编译器只给 non-exhaustive match 警告,运行时如果传入漏掉的类型会抛 scala.MatchError。;补齐分支;不要用 case _ 草草吞掉,除非确实是默认语义。把 sealed trait 的子类写在另一个文件
→ Scala 2 要求 sealed 的直接子类与 trait 同文件,否则编译报 illegal inheritance from sealed trait。;把子类移到同一个文件,或改用普通 trait 并接受无法检查穷尽性。模式顺序写反,宽泛的 case 挡住具体 case
→ match 从上往下匹配,case _ 或变量模式放在前面会让后面的分支永远不可达。;把具体模式放前面,通配和默认分支放最后。
💡 Scala 3 可以直接用 enum 定义这类有限集合,Scala 2.13 的等价写法就是 sealed trait 加 case class、case object。