第6章 类与对象

class、object、伴生对象、apply,以及 trait 混入——Scala 的面向对象和 Java 像但更紧凑。

面向对象第8-9周3 讲9 段示例代码

本章学习目标

  • 会用 class 定义主构造器、字段、方法与访问控制
  • 能写 object 单例与 apply 工厂方法
  • 能用 trait 组合能力(with 混入)
  • 能对比 Java 的 interface / static 说明 Scala 的差异

本章导览

核心内容

6.1

class:主构造器、字段、方法与访问控制

写第一个类:构造器、字段、方法和访问控制。

能定义带主构造器和辅助构造器的类能区分 val、var 与无修饰符构造器参数的可见性能用 private、require 控制访问并校验对象状态

知识点

主构造器就是类名后的参数表

  • class Student(val name: String, var age: Int) 的花括号里可以直接使用 name 和 age。
  • 参数带 val 生成只读访问器,带 var 生成可读写访问器,什么都不带则只在本类内部可用。
  • 类体里定义的 val、var 是字段,def 是方法,初始化按从上到下的顺序执行。

方法、字段与辅助构造器

  • def greet(): String = ... 声明方法;无参方法可以省略括号,调用时保持一致。
  • 辅助构造器用 def this(...) 定义,第一句必须调用已有的构造器。
  • require 适合校验参数,失败时抛 IllegalArgumentException 并带上你写的消息。

访问控制:能 private 就别公开

  • private 成员只在当前类及伴生对象内可见,外部拿到对象也读不到。
  • private[包名] 把可见性扩大到指定包,适合包内协作。
  • 构造器参数前加 private val,可以既保留字段又阻止外部访问。

代码示例

▶ 带字段、私有状态和方法的 Student 类
SCALA
class Student(val name: String, var age: Int) {
  private var score: Int = 0
  def setScore(value: Int): Unit = { score = value }
  def getScore: Int = score
  def greet(): String = s"Hi, I'm $name, $age years old"
}

object Main {
  def main(args: Array[String]): Unit = {
    val s = new Student("Alice", 20)
    println(s.greet())
    s.age = 21
    println(s.age)
    s.setScore(95)
    println(s.getScore)
  }
}
👀 运行结果
Hi, I'm Alice, 20 years old
21
95
▶ 主构造器加辅助构造器
SCALA
class Rectangle(val width: Double, val height: Double) {
  def this(side: Double) = this(side, side)
  def area: Double = width * height
}

object Main {
  def main(args: Array[String]): Unit = {
    val r1 = new Rectangle(2.0, 3.0)
    val r2 = new Rectangle(4.0)
    println(r1.area)
    println(r2.area)
  }
}
👀 运行结果
6.0
16.0
▶ require 校验构造参数,异常消息可直接读取
SCALA
class Account(val owner: String, initial: Double) {
  require(initial >= 0.0, "initial balance must be non-negative")
  private var balance = initial

  def deposit(x: Double): Unit = {
    require(x > 0.0, "deposit must be positive")
    balance += x
  }

  def current: Double = balance
}

object Main {
  def main(args: Array[String]): Unit = {
    val a = new Account("Alice", 100.0)
    a.deposit(50.0)
    println(a.current)

    try {
      new Account("Bob", -1.0)
    } catch {
      case e: IllegalArgumentException => println(e.getMessage)
    }
  }
}
👀 运行结果
150.0
requirement failed: initial balance must be non-negative

常见错误

⚠️ 踩坑与解决
  • class Student(name: String) 之后写 s.name
    → 不带 val 或 var 的构造器参数不是公开字段,作用域仅限类体内部,编译报 value name is not a member of Student。;需要外部读取就声明为 val name: String。
  • 写 Student("Alice", 20) 编译报 not found: value Student
    → Scala 2 的普通类没有自动生成伴生 apply,构造实例必须写 new。;写 new Student("Alice", 20),或在伴生对象里定义 apply。
  • 在类外访问 s.score(private 字段)
    → private 把访问范围限制在类及伴生对象内部,外部直接访问会编译失败。;通过公开方法读写,例如 s.getScore;需要暴露就改成 val score 并评估可变性。

💡 构造器参数默认不加 val 或 var,先收紧可见性;只有确实要对外暴露的字段才加 val 或 var。

6.2

object 与伴生对象:单例、apply / unapply

用 object 写单例,用伴生对象统一构造与解构。

能用 object 实现单例、工具方法和程序入口能写出与类同名的伴生对象并共享私有成员能用 apply 简化构造、用 unapply 支持模式匹配

知识点

object 是语言内置的单例

  • object 中的成员全局只有一份,第一次被访问时惰性初始化。
  • 适合放常量、工具方法、配置和程序入口,使用时不需要 new。
  • object 也可以继承 class 或混入 trait,但一个 object 就是唯一实例。

伴生对象与类共享私有访问

  • class 与 object 同名且在同一文件里,二者互为伴生。
  • 伴生对象可以访问 class 的 private 成员,class 也能访问伴生对象的 private 成员。
  • 伴生对象常放工厂方法、默认值和类型类实例。

apply 与 unapply 是一对反向操作

  • apply 让 User("Alice", 20) 等价于 new User(...),少写 new。
  • unapply 把对象拆成若干值,case User(n, a) 由它驱动。
  • unapply 返回 Option 表示可能匹配失败,返回 Boolean 则用于谓词式匹配。

代码示例

▶ object 单例存放配置与工具方法
SCALA
object Config {
  val appName = "CourseSite"
  def describe: String = s"$appName v1.0"
}

object Main {
  def main(args: Array[String]): Unit = {
    println(Config.appName)
    println(Config.describe)
  }
}
👀 运行结果
CourseSite
CourseSite v1.0
▶ 伴生对象的 apply 与 unapply
SCALA
class User(val name: String, val age: Int)

object User {
  def apply(name: String, age: Int): User = new User(name, age)
  def unapply(u: User): Option[(String, Int)] = Some((u.name, u.age))
}

object Main {
  def main(args: Array[String]): Unit = {
    val u = User("Alice", 20)
    println(u.name)
    println(u.age)
    u match {
      case User(n, a) => println(s"$n is $a")
      case _ => println("not a user")
    }
  }
}
👀 运行结果
Alice
20
Alice is 20
▶ 自定义 apply 与只匹配偶数的 unapply
SCALA
object MathUtil {
  def apply(x: Int): Int = x * x
  def unapply(n: Int): Option[Int] = if (n % 2 == 0) Some(n / 2) else None
}

object Main {
  def main(args: Array[String]): Unit = {
    println(MathUtil(5))
    println(MathUtil.unapply(8))
    println(MathUtil.unapply(7))

    8 match {
      case MathUtil(half) => println(s"half = $half")
      case _ => println("odd")
    }
  }
}
👀 运行结果
25
Some(4)
None
half = 4

常见错误

⚠️ 踩坑与解决
  • 在伴生对象里写 def apply(...) = User(...)
    → 这行调用的是 apply 自己,运行时会无限递归,最终 StackOverflowError。;工厂方法里写 new User(...),再用 apply 包装。
  • User("Alice", 20) 报 object User does not take parameters
    → User 只有伴生 object 而没有定义 apply,编译器把括号当成对 object 的调用。;补上 apply 方法,或改用 new User("Alice", 20)。
  • unapply 返回 User 或 (String, Int) 却拿去匹配
    → 模式匹配要求 unapply 返回 Option 或 Boolean,普通返回值不能作为提取器,编译会失败。;改成 Option[(String, Int)],用 Some(...) 或 None 表示匹配结果。

💡 对外统一暴露 apply,内部保留 new;伴生对象是放工厂逻辑和私有构造细节的地方。

6.3

trait:接口、混入与多重继承

用 trait 定义可混入能力,理解多重继承与 super 顺序。

能用 trait 声明抽象成员并提供默认实现能在一个类中混入多个 trait 并理解线性化能解释 trait 中状态与继承顺序带来的影响

知识点

trait 是带实现的接口

  • trait 可声明抽象成员,也可以直接写出方法体和字段。
  • 类用 extends 混入第一个 trait,其余用 with 追加。
  • 一个类只能有一个父 class,但可以混入多个 trait。

混入顺序决定 super 的调用链

  • 线性化从右往左组装,右侧 trait 先于左侧执行。
  • super.show 不是固定指向父类,而是调用混入链上的下一个实现。
  • 调整 with 的先后会改变结果,这是 trait 叠加的核心。

trait 的状态与组合

  • trait 里的 var 在混入它的每个实例中各有一份,不是全局共享。
  • 把可组合的行为拆成小 trait,比造一条深继承链更灵活。
  • Scala 3 允许 trait 带构造参数,Scala 2.13 不支持;2.13 里需要参数时改用抽象 class。

代码示例

▶ 抽象成员加默认实现的 Greeter
SCALA
trait Greeter {
  def name: String
  def greeting: String = s"Hello, $name"
}

class Person(val name: String) extends Greeter

object Main {
  def main(args: Array[String]): Unit = {
    val p = new Person("Alice")
    println(p.greeting)
  }
}
👀 运行结果
Hello, Alice
▶ 两个 trait 叠加时 super 的调用顺序
SCALA
trait A { def show: String = "A" }
trait B extends A { override def show: String = "B->" + super.show }
trait C extends A { override def show: String = "C->" + super.show }
class D extends C with B

object Main {
  def main(args: Array[String]): Unit = {
    println(new D().show)
  }
}
👀 运行结果
B->C->A
▶ trait 中的私有状态按实例各自一份
SCALA
trait Counter {
  private var n = 0
  def inc(): Unit = n += 1
  def count: Int = n
}

class Clicker extends Counter

object Main {
  def main(args: Array[String]): Unit = {
    val c = new Clicker
    c.inc()
    c.inc()
    c.inc()
    println(c.count)
  }
}
👀 运行结果
3

常见错误

⚠️ 踩坑与解决
  • trait 里把有实现的方法写成 def greeting: String
    → 抽象方法没有方法体,混入它的 Person 没实现就编译失败,提示 Person needs to be abstract。;补上 = s"Hello, $name",或让 Person 自己实现 greeting。
  • 写 class Duck extends Fly, Swim
    → Scala 2 的继承列表用 with 连接 trait,逗号是非法语法。;写成 class Duck extends Fly with Swim。
  • 覆盖 trait 的具体方法却没写 override
    → trait 中的具体方法视为父实现,子类重新定义时必须加 override。;改成 override def greeting: String = ...。

💡 用多个小 trait 叠加能力,并记住右侧先生效;写 super 之前先想清楚混入顺序。