1. 概述
在本教程中,我们将重点介绍 Scala 对两个核心 OOP 元素的处理:类和对象。
我们将首先介绍类,然后再深入研究隐式类和内部类。 然后我们将了解 Scala 对象。
最后,我们还将学习 Scala 中对象和类之间的区别。
2. 类
类是创建对象的蓝图。当我们定义一个类时,我们可以从该类创建新的对象(实例)。
我们使用 class 关键字定义一个类,后跟我们为该类指定的名称。
让我们看看如何在终端中使用 Scala REPL 创建一个简单的类
scala> class Vehicle
defined class Vehicle
scala> var car = new Vehicle
car: Vehicle = Vehicle@7c1447b5
scala>
我们现在有一个带有无参数构造函数的 Vehicle 类。
在 Scala 中,类构造函数通常比 Java 中更简洁、更易于阅读。
该语言支持两种类型:主和辅助。
2.1. 主构造函数
默认情况下,每个 Scala 类都有一个主构造函数。主构造函数由构造函数参数、类体中调用的方法以及在类体中执行的语句组成。
让我们定义一个名为 Abc 的类,其构造函数接受一个 String 和一个 Int
class Abc(var a: String = "A", var b: Int) {
println("Hello world from Abc")
}
这一次,我们没有像 Scala 提供的无参数构造函数那样,而是通过在类名旁边列出它们来定义了一个有两个参数的构造函数。 顶部参数和体内的语句构成了构造函数。
另请注意,对于我们的一个参数 a, 我们指定了一个默认值;“A”。
在 Java 中,这会更长;我们需要创建一个名为 Abc 的特殊方法作为构造函数来初始化对象。
当创建 Abc 的实例时,我们看到了来自 println 语句的输出
scala> val abc = new Abc(b=3)
Hello world from Abc
abc: Abc = Abc@70f68288
这是因为类体中的所有语句和表达式都是构造函数的一部分。
2.2. 辅助构造函数
要定义辅助(或二级)构造函数,我们定义名为 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. 类实例
我们可以将类视为创建对象的模板。类的实例是使用类作为模板创建的实际对象。 正如我们已经看到的,我们使用关键字 new 来创建类的实例。
假设我们有一个 Vehicle 模板,我们可以使用它来生产不同类型的车辆。我们的 Vehicle 模板代表类,它不代表一辆真实的车辆。当我们使用我们的 Vehicle 模板创建一个 car 时,car 就是 Vehicle 的一个实例。
在我们的最后一个例子中,我们创建了一个名为 car 的 Vehicle 类的实例。在大多数情况下,类和实例不像我们的 Vehicle 和 car 示例那么简单。
让我们在我们的车辆示例基础上添加一些新的有趣功能。首先,我们将创建一个名为 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.")
}
}
现在有了我们的 Car 类,我们可以创建任意数量的实例。让我们将我们的代码加载到终端
scala> :load path/to/my/scala/File.scala
args: Array[String] = Array()
Loading path/to/my/scala/File.scala
defined class Car
并创建一个名为 familyCar, 来自我们的 Car 类的对象
scala> var familyCar = new Car("Toyota", "SUV", "RAV4")
familyCar: Car = Car@2d5b549b
scala>
我们的 familyCar 变量是 Car 类的一个实例,具有 Car 的所有属性。
现在我们可以尝试使用我们的 familyCar 变量:
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. 扩展一个类
扩展一个类使我们能够创建一个新类,该类继承第一个类的所有属性。我们使用 extends 关键字扩展一个类。
让我们通过扩展我们的 Car 类来创建一个名为 Toyota 的新类
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
}
}
如果我们想在其中一个方法中提供不同的行为,我们可以覆盖它来定义我们自定义的行为。如我们在 Toyota 类中看到的,我们已经覆盖了 start 方法。
现在让我们看看我们的 Toyota 类与 Car 类有哪些共同之处以及发生了哪些变化
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
scala>
现在我们有一个 start 方法,它提醒我们要按住离合器。
由于我们没有覆盖任何其他方法,因此其余方法将与它们在 Car 中的工作方式相同。
2.5. 隐式类
隐式类(在 Scala 2.10 中引入)提供了一种向现有对象添加新功能的方法。这很有用,尤其是在我们没有修改源对象的选项时。
我们使用 implicit 关键字定义一个隐式类。例如,让我们创建一个向 String 类添加方法的隐式类
object Prediction {
implicit class AgeFromName(name: String) {
val r = new scala.util.Random
def predictAge(): Int = 10 + r. nextInt(90)
}
}
在这个例子中,我们创建了一个 implicit 类 AgeFromName,其中包含一个 predictAge 方法,该方法返回 10 到 100 之间的随机整数。不用担心 object 关键字,我们将在后面的章节中了解更多信息。
只要我们的隐式类在范围内,我们就可以在任何字符串上调用 predictAge 方法
scala> import Prediction._
import Prediction._
scala> "Faith".predictAge()
res0: Any = 89
scala> "Faith".predictAge()
res1: Any = 74
值得注意的是,创建隐式类有一些 限制
1. 它们必须定义在另一个 trait/class/object 内部。 在我们的 AgeFromName 示例中,我们将它放置在一个对象中。
2. 它们的构造函数只能接受一个非隐式参数。我们可以这样做
implicit class AgeFromName(name: String)
implicit class AgeFromName(name: String)(implicit val a: String, val b: Int)
但不能这样做
implicit class AgeFromName(name: String, val a: Int)
3. 它们应该是唯一的。换句话说,在范围内不能有与隐式类同名的任何方法、成员或对象。
4. 隐式类不能是 case 类。
2.6. 内部类
Scala 提供了在另一个类内部嵌套类的能力。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")
}
现在我们可以继续将合适的歌曲添加到我们的 funk 和 jazz 播放列表中 DemoPlayList
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)
scala>
一切都按预期工作,因为我们将正确的歌曲添加到正确的播放列表:song1 添加到 funk,song2 添加到 jazz。
再次查看DemoPlayList,我们看到song1和song2分别是funk和jazz的成员。虽然 song1和 song2是Song的实例,但**它们不属于同一个PlayList实例,因此属于不同的类型。**
这意味着我们不能将 song1添加到 jazz
scala> demo.jazz.addSong(demo.song1)
^
error: type mismatch;
found : demo.funk.Song
required: demo.jazz.Song
scala>
同样适用于 song2,我们不能将其添加到 funk
scala> demo.funk.addSong(demo.song2)
^
error: type mismatch;
found : demo.jazz.Song
required: demo.funk.Song
scala>
这种行为是因为**Scala内部类与外部对象绑定**。
3. 对象
回想一下我们之前car的例子,car是一个对象,能够完成Vehicle能完成的所有事情。类为我们提供了模板,而对象是我们从模板创建出来的。
通常在OOP中,我们可以完美地说对象是类的实例。**然而,Scala有一个object关键字,我们可以用它来定义单例对象。**
当我们说单例时,我们的意思是只能实例化一次的对象。创建对象只需要object关键字和一个标识符
object SomeObject
对象不接受任何参数,但我们可以定义字段、方法和类,就像在常规类中一样
object Router {
val baseUrl: String = "https://baeldung.cn"
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")
}
}
我们可以通过导入它们来使用我们的 Router对象的任何成员。让我们导入Router中的所有内容
scala> import Router._
import Router._
scala> Response("some url", "some path", "GET")
res1: Router.Response.type = Response(some url,some path,GET)
scala> baseUrl
Here we go about Routing!
res2: String = https://baeldung.cn
scala> get("/index")
This is a get method for /index
res4: Router.Response = Response(https://baeldung.cn,/index,GET)
scala> put("/index")
This is a put method for /index
res5: Router.Response = Response(https://baeldung.cn,/index,PUT)
scala> post("/scala-tutorials")
This is a post method for /scala-tutorials
res6: Router.Response = Response(https://baeldung.cn,/scala-tutorials,POST)
scala>
我们可以看到当我们访问 baseUrl时,打印了“Here we go about Routing!”。 这是因为对象在引用它时才会被延迟实例化。 我们可以看到,下次访问baseUrl时,我们的消息不会被打印出来。
Java没有直接等同于单例object的东西。对于每个Scala单例对象,编译器会为该对象创建一个Java类(在末尾添加一个美元符号),并创建一个名为MODULE$的静态字段来保存该类的单个实例。 **因此,为了确保只有一个对象的实例,Scala使用静态类持有者。**
3.1. 伴生对象
一个伴生对象**是一个与类同名且位于同一文件中的对象**;反之,该类是对象的伴生类。我们将修改我们的Router并创建一个伴生类,它将利用它作为伴生对象
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命令行**:**paste命令将我们的代码放到终端上
scala> :paste path/to/my/scala/File.scala
Pasting file path/to/my/scala/File.scala...
defined object Router
defined class Router
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://baeldung.cn,/index,GET)
scala> indexRouter.post()
This is a post method for /index
res1: Router.Response = Response(https://baeldung.cn,/index,POST)
scala> indexRouter.delete()
This is a delete method for /index
res2: Router.Response = Response(https://baeldung.cn,/index,DELETE)
scala>
尽管Router对象中的方法是private的,但Router伴生类可以访问它们。
伴生对象的另一个常见用途是创建工厂方法.
让我们考虑一个常见的使用场景,即在我们的应用程序中拥有四个环境:test、int、staging和production环境。 我们希望一个工厂能够根据字符串生成我们的当前环境。 此外,我们希望环境可序列化,以便我们可以保留状态。
我们可以使用伴生对象和类来实现这一点
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类和对象之间的区别
现在我们对Scala 类和对象有了更好的理解,让我们来看看一些区别
- 定义:类是用class关键字定义的,而对象是用object关键字定义的。 此外,虽然类可以接受参数,但对象不能接受任何参数
- 实例化:要实例化一个常规类,我们使用new关键字。 对于对象,我们不需要new关键字
- 单例与多实例:虽然一个类可以有无限数量的实例,但一个对象只有一个实例,当我们第一次引用它时才会被延迟创建
- 继承:由于对象是单例,它**不能被继承** / **扩展**; 这样做会导致创建多个对象的实例 - 类可以被扩展。
5. 结论
在本教程中,我们通过一系列简单的示例学习了 Scala 类和对象。我们了解了内部类、隐式类、伴生对象及其应用。然后,我们通过理解 Scala 中类和对象的一些区别来结束本教程。
支持本文的代码可在 GitHub 上获取。 一旦你以 Baeldung Pro 会员 身份登录,就开始学习并在项目上进行编码。