Scala 面向对象编程

概述

与 Java 相比,Scala 面相对象编程具有如下不同:

  • Java 的访问修饰符包括 publicprotectedprivate;而 Scala 中没有 public,只有 protectedprivate
  • Java 类如果没有显式指定访问修饰符,其可见性为包可见性(package-private),即该类只能被同一个包内的其他类访问;而 Scala 中的类如果没有显式指定访问修饰符,其可见性为 public
  • 在一个 Java 源文件中只能定义一个类;但是在一个 Scala 源文件中可以同时定义多个类。

类成员

  • Scala 类成员包括字段方法
  • Scala 类成员默认访问控制为 public,但是 Scala 并没有 public 关键字,所有没有被 protectedprivate 修饰的成员都是 public

主构造方法

Scala 中每个类都有一个主构造方法,主构造方法的参数直接放在类名之后。类体中的代码会成为主构造方法的一部分。可以通过在参数前加 val 或者 var 关键字,将构造方法的参数同时转换为类的字段。

 1class Person(val name: String, var age: Int) {
 2  // 类体是构造方法的一部分
 3  println(s"Just created a person named $name who is $age years old")
 4
 5  // 定义一个方法
 6  def greet(): Unit = println(s"Hi, my name is $name and I am $age years old.")
 7}
 8
 9val p = new Person("Alice", 25)
10p.greet()
11// Expected output:
12// Just created a person named Alice who is 25 years old
13// Hi, my name is Alice and I am 25 years old.

在上面的示例中,Person 类有 nameage 两个字段,其中 name 是只读的(因为使用了 val),而age 是可变的(因为使用了 var),这些参数将自动成为类的字段,并可以在类的其他方法中使用。

辅助构造方法

Scala 类允许定义零个或多个辅助构造方法,每个辅助构造方法都必须以一个对先前定义的其他构造方法(包括主构造方法)的调用开始。

辅助构造方法是通过定义名为 this 的方法来实现的,例如:

 1class Person(val name: String, var age: Int) {
 2
 3  // 辅助构造方法
 4  def this(name: String) = {
 5    this(name, 0)  // 调用主构造方法
 6    println("Just created a Person with name only")
 7  }
 8
 9  def greet(): Unit = {
10    println(s"Hi, my name is $name and I am $age years old.")
11  }
12}
13
14val p1 = new Person("Alice")
15// Expected output:
16// Just created a Person with name only

在上面的示例中,辅助构造方法只接收 name 参数,并通过调用主构造方法 this(name, 0) 指定了 age 的默认值 0,然后打印一条信息。

字段

Scala 类中的字段有两种定义方式,一是在主构造方法中定义字段,所有在主构造方法中使用 valvar 关键字修饰的参数都自动成为类的字段。二是在类中直接定义字段。

 1// 在主构造方法中定义字段 name 和 age
 2class Person(val name: String, var age: Int) {
 3  
 4  // 在类体中声明另一个字段 email
 5  var email: String = ""
 6
 7  // 辅助构造方法
 8  def this(name: String, age: Int, email: String) = {
 9    this(name, age) // 首先调用主构造方法
10    this.email = email // 然后设置额外的字段
11  }
12}
13
14// 使用辅助构造方法创建对象
15val p = new Person("John Doe", 30, "[email protected]")
16
17println(p.name)  // 输出:John Doe
18println(p.age)   // 输出:30
19println(p.email) // 输出:[email protected]

在上面的示例中,Person 类有通过主构造方法参数定义了 nameage 两个字段,并在类体中定义了 email 字段。在辅助构造方法中,我们不仅调用了主构造方法来处理 nameage 字段,还显式地设置了 email 字段的值。

注:

  • 主构造方法中可以使用 valvar 关键字修饰参数来定义字段,但是辅助构造方法不行,辅助构造方法中参数的值只能被用来初始化或修改类的字段。

方法

Scala 方法参数都是 val 类型,因此如果你在方法体中修改参数将会导致编译错误:

1class Math {
2  def add(a: Int, b: Int): Int = {
3    a = 2 // error: reassignment to val
4  }
5}

方法如果没有显式给出 return 语句,则方法返回值就是最后一个表达式的值:

1class Math {
2  def add(a: Int, b: Int): Int = {
3    a + b
4  }
5}

实际上,Scala 推荐的风格是避免使用任何显式的 return 语句,而是将每个方法都视作是一个交出某个值的表达式。

对于方法返回值类型,Scala 推荐的风格是显式给出返回值类型,即使编译器可以帮你推导出返回值类型,例如:

1class Math {
2  def max(x: Int, y: Int): Int = {
3    if (x > y) x else y
4  }
5}

命名参数

命名参数允许在调用方法时显式地标明每个参数的名称,这样做的好处是可以不按照方法定义时参数的顺序来传递参数,从而增加代码的可读性,例如:

1def greet(firstName: String, lastName: String) = {
2  println(s"Hello, $firstName $lastName!")
3}
4
5// 不使用命名参数
6greet("John", "Doe") // Hello, John Doe!
7
8// 使用命名参数
9greet(lastName = "Doe", firstName = "John") // Hello, John Doe!

默认参数

默认参数允许在定义方法时为参数指定一个默认值,如果调用时没有为这个参数提供值,将使用其默认值。你可以为方法中的任意参数提供默认值,无需为所有参数都提供默认值,例如:

1case class Point(x: Double = 0.0, y: Double = 0.0)
2
3val p1 = Point() // Point(0.0, 0.0)
4val p2 = Point(1.0) // Point(1.0, 0.0)
5val p3 = Point(1.0, 2.0) // Point(1.0, 2.0)

在上面的示例中,我们定义了一个案例类,其构造器方法的参数都给出了默认值,因此,我们可以在调用构造器只给出部分参数即可。

命名参数和默认参数可以组合使用,例如:

1case class Point(x: Double = 0.0, y: Double = 0.0)
2
3val p1 = Point(x = 1.0, y = 2.0) // Point(1.0, 2.0)
4val p2 = Point(x = 1.0) // Point(1.0, 0.0)
5val p3 = Point(y = 2.0) // Point(0.0, 2.0)

单例对象

与 Java 不同,Scala 不允许在类中定义静态成员。为此,Scala 提供了单例对象(Singleton object),单例对象与类有点类似,可以包含字段和方法成员,但是使用的关键字是 object,而不是class,下面的示例定义了一个单例对象 Math

1object Math {
2  def abs(n: Int): Int = if (n < 0) -n else n
3  def max(x: Int, y: Int): Int = if (x > y) x else y
4}

单例对象具有如下特点:

  • 定义单例对象并不会定义新的类型;
  • 单例对象是一种特殊的类,有且只有一个实例;
  • 单例对象可以扩展自某个超类,还可以混入特质;
  • 单例对象是延迟创建的,它在首次访问时被初始化;
  • 类和单例对象的一个区别就是单例对象不接受参数,而类可以。
  • 每个单例对象都是通过一个静态变量引用合成类(synthetic class)的实例来实现的,因此单例对象从初始化的语义上跟 Java 的静态成员是一致的。

当单例对象和某个类共用同一个名字时,它被称作这个类的伴生对象(companion object);同时,这个类又叫做这个单例对象的伴生类(companion class)。伴生类和伴生对象具有如下特点:

  • 必须在同一个源文件中定义类和它的伴生对象;
  • 类和它的伴生对象可以互相访问对方的私有成员;
  • Java 类中的 static 成员对应于 Scala 中的伴生对象的普通成员。

没有伴生类的单例对象称为独立对象(standalone object)。独立对象有很多种用途,包括将工具方法归集在一起,或者定义 Scala 应用程序的入口等。 使用伴生对象来定义那些在伴生类中不依赖于实例化对象而存在的成员变量或者方法。

案例类

case 关键字修饰的类称为案例类case class),例如:

1case class Person(name: String, age: Int)

案例类会自动添加一个跟类同名的工厂方法,因此在实例化对象时可以省略 new 关键字:

1val person = Person("Alice", 25)
2println(person) // Person(Alice,25)

案例类参数列表中的参数都隐式地获得了一个 val 前缀,因此它们会被当作字段处理。

1val person = Person("Alice", 25)
2println(person.name) // Alice
3println(person.age) // 25
4
5person.name = "Bob" // 编译错误
6person.age = 26 // 编译错误

编译器会帮助我们以自然的方式实现 toStringhashCodeequals 方法:

1val p1 = Person("Alice", 25)
2val p2 = Person("Bob", 25)
3println(p1.toString) // Person(Alice,25)
4println(p1.hashCode) // -369217488
5println(p1.equals(p2)) // false

编译器会添加一个 copy 方法用于制作修改过的拷贝,这个方法可以用于制作一两个属性不同之外其余完全相同的该类的新实例:

1val p1 = Person("Alice", 25)
2val p2 = p1.copy()
3val p3 = p2.copy(name = "Bob")
4println(p2) // Person(Alice,25)
5println(p3) // Person(Bob,25)

抽象类

抽象成员包括抽象类型抽象 val抽象 var抽象方法

 1trait AbstractTrait {
 2  // 抽象类型
 3  type T
 4
 5  // 抽象val
 6  val initial: T
 7
 8  // 抽象var
 9  var current: T
10
11  // 抽象方法
12  def transform(x: T): T
13}

对于上面的特质,实现它需要填充所有的抽象成员:

1class Concrete extends AbstractTrait {
2  type T = String
3  val initial = "hi"
4  var current = initial
5  def transform(x: String): String = x + x
6}

继承

  • 只有主构造器才可以往基类的构造器里写参数
  • 子类重写父类具象方法时必须使用 override 关键字,而子类重新父类抽象方法时不需要使用 override 关键字。
上一页