1. 概述

本教程将聚焦于 Scala 对面向对象编程中两个核心元素:类(Class)与对象(Object) 的处理方式。

我们会先讲解类,包括其构造器、继承机制、隐式类和内部类等特性;随后深入探讨 Scala 的对象机制,特别是单例对象与伴生对象的使用。最后,我们会总结类与对象在 Scala 中的一些关键区别。

2. 类(Classes)

类是用于创建对象的模板。定义一个类后,我们可以通过它创建多个实例。

在 Scala 中,使用 class 关键字定义类:

scala> class Vehicle
defined class Vehicle

scala> var car = new Vehicle
car: Vehicle = Vehicle@7c1447b5

此时我们得到了一个没有参数的构造器。

2.1. 主构造器(Primary Constructor)

每个 Scala 类默认都有一个主构造器,它由构造参数、类体中的方法调用以及执行语句组成。

例如,定义一个带有两个参数的类:

class Abc(var a: String = "A", var b: Int) {
  println("Hello world from Abc")
}

这里构造器接受两个参数,其中 a 有一个默认值 "A"。所有类体中的表达式都会在实例化时被执行。

scala> val abc = new Abc(b=3)
Hello world from Abc
abc: Abc = Abc@70f68288

这说明构造器中包含的所有逻辑都会在对象创建时运行。

2.2. 辅助构造器(Auxiliary Constructor)

辅助构造器通过 def this(...) 定义,并且必须调用已有的构造器(主构造器或其他辅助构造器)。

val constA = "A"
val constB = 4

class Abc(var a: String, var b: Int) {
  def this(a: String) {
    this(a, constB)
    this.a = a
  }

  def this(b: Int) {
    this(constA, b)
    this.b = b
  }

  def this() {
    this(constA, constB)
  }
}

这样我们可以以多种方式创建实例:

new Abc("Some string")
new Abc(1)
new Abc()

⚠️ 使用辅助构造器有两个规则:

  • 每个构造器签名必须唯一(参数不同)
  • 必须调用主构造器或另一个辅助构造器

2.3. 类实例(Class Instance)

类是一个模板,而实例则是基于这个模板创建的具体对象。

比如我们有一个 Car 类:

class Car (val manufacturer: String, brand: String, var model: String) {
  var speed: Double = 0;
  var gear: Any = 0;
  var isOn: Boolean = false;

  def start(keyType: String): Unit = {
    println(s"Car started using the $keyType")
  }

  def selectGear(gearNumber: Any): Unit = {
    gear = gearNumber
    println(s"Gear has been changed to $gearNumber")
  }

  def accelerate(rate: Double, seconds: Double): Unit = {
    speed += rate * seconds
    println(s"Car accelerates at $rate per second for $seconds seconds.")
  }

  def brake(rate: Double, seconds: Double): Unit = {
      speed -= rate * seconds
      println(s"Car slows down at $rate per second for $seconds seconds.")
  }

  def stop(): Unit = {
    speed = 0;
    gear = 0;
    isOn = false;
    println("Car has stopped.")
  }
}

加载并创建实例:

scala> :load path/to/my/scala/File.scala
args: Array[String] = Array()
Loading path/to/my/scala/File.scala
defined class Car

scala> var familyCar = new Car("Toyota", "SUV", "RAV4")
familyCar: Car = Car@2d5b549b

然后可以操作该实例:

scala> familyCar.start("remote")
Car started using the remote

scala> familyCar.speed
res0: Double = 0.0

scala> familyCar.accelerate(2, 5)
Car accelerates at 2.0 per second for 5.0 seconds.

scala> familyCar.speed
res1: Double = 10.0

scala> familyCar.brake(1, 3)
Car slows down at 1.0 per second for 3.0 seconds.

scala> familyCar.speed
res2: Double = 7.0

2.4. 继承类(Extending a Class)

使用 extends 可以扩展一个类,从而复用其属性和方法:

class Toyota(transmission: String, brand: String, model: String) extends Car("Toyota", brand, model) { 
  override def start(keyType: String): Unit = { 
    if (isOn) {
      println(s"Car is already on.") 
      return
    } 
    if (transmission == "automatic") { 
      println(s"Car started using the $keyType") 
    } else { 
      println(s"Please ensure you're holding down the clutch.") 
      println(s"Car started using the $keyType") 
    } 
    isOn = true  
  } 
}

测试效果:

scala> val prado = new Toyota("Manual", "SUV", "Prado")
prado: Toyota = Toyota@4b4ff495

scala> prado.start("key")
Please ensure you're holding down the clutch.
Car started.

scala> prado.accelerate(5, 2)
Car accelerates at 5.0 per second for 2.0 seconds.

scala> prado.speed
res0: Double = 10.0

✅ 除了重写的 start 方法,其他方法行为保持不变。

2.5. 隐式类(Implicit Classes)

隐式类允许我们为已有类型添加新功能,特别适用于无法修改源码的情况。

定义语法如下:

object Prediction {
  implicit class AgeFromName(name: String) {
    val r = new scala.util.Random
    def predictAge(): Int = 10 + r. nextInt(90)
  }
}

使用方式:

scala> import Prediction._
import Prediction._

scala> "Faith".predictAge()
res0: Any = 89

scala> "Faith".predictAge()
res1: Any = 74

⚠️ 隐式类的限制:

  1. 必须定义在 trait / class / object 内部
  2. 构造器只能有一个非隐式参数
  3. 名称必须唯一
  4. 不能是 case class

2.6. 内部类(Inner Classes)

Scala 支持在类中嵌套定义类,这类内部类绑定到外部对象。

class PlayList {
  var songs: List[Song] = Nil
  def addSong(song: Song): Unit = {
    songs = song :: songs
  }
  class Song(title: String, artist: String)
}

class DemoPlayList {
  val funk = new PlayList
  val jazz = new PlayList
  val song1 = new funk.Song("We celebrate", "Laboriel")
  val song2 = new jazz.Song("Amazing grace", "Victor Wooten")
}

验证行为:

scala> val demo = new DemoPlayList
demo: DemoPlayList = DemoPlayList@4ab2e70c

scala> demo.funk.addSong(demo.song1)

scala> demo.jazz.addSong(demo.song2)

scala> demo.funk.songs
res0: List[demo.funk.Song] = List(PlayList$Song@6c3b477b)

scala> demo.jazz.songs
res1: List[demo.jazz.Song] = List(PlayList$Song@d963c85)

❌ 尝试跨 playlist 添加歌曲会失败:

scala> demo.jazz.addSong(demo.song1)
                              ^
       error: type mismatch;
        found   : demo.funk.Song
        required: demo.jazz.Song

✅ 这是因为 Scala 的内部类是绑定到外部对象上的。

3. 对象(Objects)

对象是单例实例,使用 object 关键字定义:

object SomeObject

对象不接收参数,但可以定义字段、方法和类:

object Router {
  val baseUrl: String = "https://www.baeldung.com"
  
  case class Response(baseUrl: String, path: String, action: String)
  def get(path: String): Response = {
    println(s"This is a get method for ${path}")
    Response(baseUrl, path, "GET")
  }

  def post(path: String): Response = {
    println(s"This is a post method for ${path}")
    Response(baseUrl, path, "POST")
  }

  def patch(path: String): Response = {
    println(s"This is a patch method for ${path}")
    Response(baseUrl, path, "PATCH")
  }

  def put(path: String): Response = {
    println(s"This is a put method for ${path}")
    Response(baseUrl, path, "PUT")
  }

  def delete(path: String): Response = {
    println(s"This is a delete method for ${path}")
    Response(baseUrl, path, "DELETE")
  }
}

使用方式:

scala> import Router._
import Router._

scala> baseUrl
Here we go about Routing!
res2: String = https://www.baeldung.com

scala> get("/index")
This is a get method for /index
res4: Router.Response = Response(https://www.baeldung.com,/index,GET)

💡 对象是懒加载的,只有第一次引用才会初始化。

3.1. 伴生对象(Companion Objects)

当一个类和一个同名对象位于同一文件中时,它们互为“伴生”关系。

object Router {
  //..
}

class Router(path: String) {
  import Router._
  def get(): Response = getAction(path)
  def post(): Response = postAction(path)
  def patch(): Response = patchAction(path)
  def put(): Response = putAction(path)
  def delete(): Response = deleteAction(path)
}

使用示例:

scala> val indexRouter = new Router("/index")
indexRouter: Router = Router@29e61e82

scala> indexRouter.get()
Here we go about Routing!
This is a get method for /index
res0: Router.Response = Response(https://www.baeldung.com,/index,GET)

✅ 伴生类可以直接访问伴生对象中的私有成员。

工厂方法也是伴生对象的经典用途之一:

sealed class BaeldungEnvironment extends Serializable {val name: String = "int"}

object BaeldungEnvironment {
  case class ProductionEnvironment() extends BaeldungEnvironment {override val name: String = "production"}
  case class StagingEnvironment() extends BaeldungEnvironment {override val name: String = "staging"}
  case class IntEnvironment() extends BaeldungEnvironment {override val name: String = "int"}
  case class TestEnvironment() extends BaeldungEnvironment {override val name: String = "test"}

  def fromEnvString(env: String): Option[BaeldungEnvironment] = {
    env.toLowerCase match {
      case "int" => Some(IntEnvironment())
      case "staging" => Some(StagingEnvironment())
      case "production" => Some(ProductionEnvironment())
      case "test" => Some(TestEnvironment())
      case e => println(s"Unhandled BaeldungEnvironment String: $e")
        None
    }
  }
}

单元测试验证:

@Test
def givenAppropriateString_whenFromEnvStringIsCalled_thenAppropriateEnvReturned(): Unit ={
  val test = BaeldungEnvironment.fromEnvString("test")
  val int = BaeldungEnvironment.fromEnvString("int")
  val stg = BaeldungEnvironment.fromEnvString("staging")
  val prod = BaeldungEnvironment.fromEnvString("production")

  assertEquals(test, Some(TestEnvironment()))
  assertEquals(int, Some(IntEnvironment()))
  assertEquals(stg, Some(StagingEnvironment()))
  assertEquals(prod, Some(ProductionEnvironment()))
}

4. Scala 类与对象的区别

特性 类(Class) 对象(Object)
定义关键字 class object
参数支持 ✅ 支持构造参数 ❌ 不支持
实例化 new 关键字 自动单例
多实例 ✅ 无限多个 ❌ 单例
继承 ✅ 可被继承 ❌ 不能继承

5. 总结

本文通过一系列示例介绍了 Scala 中类与对象的基本用法,涵盖了主构造器、辅助构造器、隐式类、内部类、单例对象及伴生对象等内容。同时也明确了类与对象之间的核心差异。

完整代码可参考 GitHub 仓库


原始标题:Classes and Objects in Scala