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
⚠️ 隐式类的限制:
- 必须定义在
trait
/class
/object
内部 - 构造器只能有一个非隐式参数
- 名称必须唯一
- 不能是 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 仓库。