Baeldung Pro – Scala – NPI EA (类别 = Baeldung on Scala)
announcement - icon

通过超简洁的 Baeldung Pro 体验学习

>> 会员和 Baeldung Pro.

没有广告,深色模式,并免费获得 6 个月的 IntelliJ Idea Ultimate,供您入门。

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 的一个实例。

在我们的最后一个例子中,我们创建了一个名为 carVehicle 类的实例。在大多数情况下,类和实例不像我们的 Vehiclecar 示例那么简单。

让我们在我们的车辆示例基础上添加一些新的有趣功能。首先,我们将创建一个名为 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)
  }
}

在这个例子中,我们创建了一个 implicitAgeFromName,其中包含一个 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 添加到 funksong2 添加到 jazz

再次查看DemoPlayList,我们看到song1song2分别是funkjazz的成员。虽然 song1和 song2Song的实例,但**它们不属于同一个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、stagingproduction环境。 我们希望一个工厂能够根据字符串生成我们的当前环境。 此外,我们希望环境可序列化,以便我们可以保留状态。

我们可以使用伴生对象和类来实现这一点

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 对象有了更好的理解,让我们来看看一些区别

  1. 定义:类是用class关键字定义的,而对象是用object关键字定义的。 此外,虽然类可以接受参数,但对象不能接受任何参数
  2. 实例化:要实例化一个常规类,我们使用new关键字。 对于对象,我们不需要new关键字
  3. 单例与多实例:虽然一个类可以有无限数量的实例,但一个对象只有一个实例,当我们第一次引用它时才会被延迟创建
  4. 继承:由于对象是单例,它**不能被继承** / **扩展**; 这样做会导致创建多个对象的实例 - 类可以被扩展。

5. 结论

在本教程中,我们通过一系列简单的示例学习了 Scala 类和对象。我们了解了内部类、隐式类、伴生对象及其应用。然后,我们通过理解 Scala 中类和对象的一些区别来结束本教程。

支持本文的代码可在 GitHub 上获取。 一旦你Baeldung Pro 会员 身份登录,就开始学习并在项目上进行编码。
2 条评论
最早
最新
内联反馈
查看所有评论
© .