Scala学习笔记-核心特性(2)-面向对象

包管理

包声明

在Scala中,同样存在包的概念。同Java一样,Scala的包具有如下的作用:

  • 区分相同名称的类
  • 当类数量众多的时候,可以很好地管理类
  • 能够控制访问范围

包声明语句

Scala中有两种包管理的风格。

一种方式和Java的包管理风格相同,每个源文件属于一个包(这里并不要求包名和源文件所在路径一致),如下:

1
package com.syh.scala

另一种方式是通过嵌套的风格表示层级关系,如下:

1
2
3
4
5
6
7
package com{
package syh{
package scala{

}
}
}

第二种风格有如下特点:

  • 一个源文件中可以声明多个package
  • 子包中的类可以直接访问父包中的内容,而无需导入包

在下面的代码中,子包中访问父包的对象可以直接访问,父包访问子包的对象需要进行包的导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com {

import com.syh.scala.Inner
// 父包访问子包的内容需要进行包的导入

object Outer {
var name = "xxx"
println(Inner.id)
}
package syh {
package scala {
object Inner {
var id = 1

def main(args: Array[String]): Unit = {
// 子包访问父包的内容无需进行包的导入
println(Outer.name)
}
}
}

}

}

包对象

在Scala中可以为每个包定义一个同名的包对象,定义在这个包对象中的成员,可以作为对应包下所有class和object的共享变量,可以直接被访问

举例来说,现在为包com.syh.scala定义一个包变量

如果采用Java的包管理风格,则包对象一般定义在其对应包下的pacakge.scala文件中,包名是为前面的层级,其中包对象名称与报名保持一致,如下

1
2
3
4
5
package com.syh

package object scala{
...
}

如果采用嵌套方式管理包,则包对象可以定义在同一文件中,但是需要保证包对象与包声明在同一作用域中

1
2
3
4
5
6
7
8
9
10
11
package com {
package syh {
package scala {

}

package object scala {
}
}

}

包导入

可以在任意位置导入(局部导入作用于代码块级别),也可以设置别名,可以选择性导入想要导入的内容,可以屏蔽某个类

1
2
3
4
5
import users._ // 引入users包下的所有类
import users.User // 引入单个类
import user.{User, UserPreferences} // 引入多个类
import users.{UserPreferences => UPrefs} // 给引入的类取别名
import users.{User => _,_} // 屏蔽某个类,这里屏蔽了User

所有的scala源文件默认导入以下的包:

1
2
3
import java.lang._
import scala._
import scala.Predef._

面向对象

类、属性以及方法

回顾Java中的类定义,一个java源文件中只能有一个public的类,并且需要和文件同名。而在Scala中,一个scala源文件可以写多个类,并且没有public的类(scala中没有public关键字,后续会提到其中的权限控制细节)

Scala中的类定义基本语法如下:

1
2
3
[访问修饰符] class 类名 {
...
}
  • 访问修饰符可选项有:private、protected以及默认,默认就是公有访问权限,无pubilc关键字

属性是类的一个组成部分,注意事项如下:

  • 属性也可以设置访问修饰符,默认为公有
  • 如果需要设置规范的setXXXgetXXX方法,可以在属性前增加@BeanProperty注解,每个注解对应一个属性
  • 公有属性可以直接被赋值和获取,但是在scala底层还是通过类似get和set的机制来完成的
  • 成员变量在赋值的时候可以给值_,这表示给变量赋值为默认值。定义常量值则不能使用_,必须在定义的时候给定

封装:

  • Java中的封装:将属性私有化,提供get和set方法
  • Scala中的封装:解决了Java中的冗余。scala中的公有属性,底层还是private,并通过get方法obj.field()和set方法obj.field_=(value)对其进行操作。因此在scala中并不推荐将属性设置为private。

但是在Java中许多框架需要提供满足规范的getter和setter方法,这时候可以增加@BeanProperty注解来生成

方法也是类的一部分,注意事项如下:

  • 基本设置方法与函数定义一致

  • 可以设置访问修饰符,放在def的前面

完成类的定义之后,可以使用new来创建对象,语法相对简单。需要注意的是,如果使用了自动推导变量,则无法完成多态。使用多态需要显式声明。

访问权限

在Java中,访问权限分为public、private、protected和默认。

在Scala中,访问权限分为private、protected和默认,权限描述如下:

  • 属性和方法的默认权限为public,但是无public关键字
  • private为私有权限,只有在类的内部和类的伴生对象可以访问
  • protected为受保护权限,Scala中的受保护权限比Java中更加严格,类内,子类可以访问,但是同一个包下无法访问
  • 可以使用private[包名]来增加包访问权限,这样可以在对应包中访问

构造器

Scala中类的构造器包括了主构造器辅助构造器

主构造器

1
2
3
class 类名(形参列表){
...
}

主构造器即在类定义的右边就开始写了,需要注意的是,类内部的所有可执行语句都是主构造器的一部分。如果主构造器没有参数,则小括号可以省略。主构造器也可以添加访问修饰符,添加到参数列表()之前

主构造器参数:主构造器的形参包括以下三种类型

  • 未使用任何修饰符修饰,这个参数就是一个局部变量(相当于传入Java构造器方法中的变量)
  • var修饰参数,表示参数作为类的成员属性使用,可以修改
  • val修饰参数,表示参数作为类的只读属性使用,不可以修改

辅助构造器

1
2
3
4
class 类名{
def this(形参列表){
}
}

辅助构造器的名称固定为this,可以有多个,编译器通过参数的个数和类型来进行区分。辅助构造器方法不能直接构建对象,必须直接或者间接调用主构造方法。构造器之间可以相互调用,但是被调用的构造器必须定义在前面。

注意事项:

  • 主构造器写在类定义上,一定是构造时最先被调用的构造器,方法体就是类定义
  • 主构造器和辅助构造器是重载的方法,所以参数列表不能一致
  • 可以定义和类名同名的方法,但是这就是一个普通方法
  • 推荐使用scala风格的主构造器来编写参数
  • 如果需要多种重载的构造器,则使用新的辅助构造器

继承和多态

继承

Scala中继承的语法与Java中相同,如下

1
2
3
class 子类名 extends 父类名 {
...
}
  • 子类会继承父类的属性和方法
  • 同Java一样,Scala也是单继承的机制
  • 在继承中,构造器的调用顺序为父类构造器 -> 子类构造器

在子类中可以重写父类的方法和属性,使用override关键字修饰。但是只能使用override重写常量属性即val,变量属性var需要直接利用赋值=来修改

多态

在Scala中也有多态的实现,底层实现借助了动态绑定的机制。但是具体细节与Java中存在不同

  • Java中:属性为静态绑定,方法为动态绑定
  • Scala中:属性和方法均为动态绑定

特殊类

抽象类

抽象类的基本语法:

  • 定义抽象类:使用abstract关键字
  • 定义抽象属性:一个属性没有初始化,则为抽象属性(一般属性必须要初始化)
  • 定义抽象方法:只声明而没有实现的方法就是抽象方法

注意事项:

  • 如果父类为抽象类,那么子类需要将抽象的属性和方法实现,否则子类也需要声明为抽象类

  • 抽象类中可以有一般的方法和属性

  • 需要区别实现重写,实现抽象类的抽象方法不需要使用override关键字,重写非抽象方法需要使用override(当然可以都使用override关键字)

  • 被实现的抽象属性可以是val也可以是var,被重写的一般属性只能是var

  • 子类中调用父类的方法需要使用super关键字

匿名子类

与Java中的一致,可以通过包含带有定义或者重写的代码块方式来创建一个匿名的子类,举例如下:

1
2
3
val/var p: baseClass = new baseClass {
override...
}

伴生对象

Scala语言是完全面向对象的语言,其中没有静态的操作。但是为了能够和Java语言交互,就产生了一种特殊的对象来模拟静态,该对象为单例对象,且对象名与类名一致。这个单例对象称为这个类的伴生对象。类中所有的“静态”内容都可以放置在它的伴生对象中声明,由于对象名和类名一致,所以在使用的时候看似是“通过类名调用”

1
2
3
4
5
6
7
class Student{

}

object Student{
val school = "xxx"
}

通过伴生对象,我们可以实现单例模式。实现单例模式的关键在于将构造器私有化,然后提供一个公共的静态方法来获取这个单例的类对象,此时我们可以将这个静态方法放在伴生对象中。并且我们可以使用一个特殊的方法apply,这个方法在调用的时候可以省略.apply(一个语法糖,只要是apply方法就可以省略,无论是伴生对象还是一般new的对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
object Test {
def main(args: Array[String]): Unit = {
var person1:Person = Person()
var person2:Person = Person("hahaha")
}
}

class Person private(var name: String)

object Person {
def apply(): Person = {
new Person("xx")
}

def apply(name: String): Person = {
new Person(name)
}
}

其中第三行和第四行中实际上是调用了Person伴生对象的apply方法,通过参数类型和个数来区别

枚举类

需要继承Enumeration,其中使用Value类型来定义枚举值

1
2
3
4
5
6
7
8
9
10
11
12
object WorkDay extends Enumeration {
val MONDAY = Value(1, "Monday")
val TUESDAY = Value(2, "Tuesday")
val THURSDAy = Value(3, "Thrusday")
}

object EnumClass {
def main(args: Array[String]): Unit = {
println(WorkDay.MONDAY) // Monday
println(WorkDay.TUESDAY) // Tuesday
}
}

应用类

需要继承App,其中包装了main方法,因此不需要显式定义main方法了,可以直接执行

1
2
3
object TestApp extends App {
println("hello,world!")
}

密封类

在定义类的时候使用sealed进行修饰,这样这个类的子类必须和该类定义在同一个文件中

特征(Trait)

在Scala中,采用特征(Trait)来代替接口的概念。

Scala中的Trait可以有抽象属性和方法,也可以有具体的属性和方法,一个类可以混入(mixin)多个特征。Scala中引入特质,一可以代替Java中的接口,第二也是对单继承机制的一种补充

特征声明与基本语法

特征声明语法如下:

1
2
3
trait 特征名 {
...
}

特征引入语法,分为有无父类

1
2
3
4
5
// 没有父类
class 类名 extends 特征1 with 特征2 with 特征3

// 有父类
class 类名 extends 父类 with 特征1 with 特征2 with 特征3

注意事项:

  • 父类和特征是同等地位

  • 在特征中可以声明抽象属性、抽象方法、一般属性、一般方法

  • 所有的Java接口都可以当作Scala的特征使用

  • 匿名子类也可以引入特征

  • 特征和基类或者多个特征中存在重名的属性或者方法,则需要在子类中进行覆写来进行冲突解决

特征和抽象类的区别:

  • 优先使用特征,一个类扩展多个特征是很方便的,但是只能扩展一个抽象类
  • 如果需要构造函数参数,则使用抽象类。因为抽象类可以定义带参数的构造函数,而特征不行

特征叠加

由于一个类可以混多个trait,并且trait中可以有具体的属性和方法,如果混入的特征中具有相同的方法,必然会出现继承冲突问题,冲突分为以下两种:

简单冲突:一个类混入的两个特征中具有相同的方法,且两个特征之间没有任何关系。解决这类冲突问题,直接在类中重写冲突的方法,可以写自己的逻辑,也可以通过super调用对应特征的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
trait TraitA {
def sayHi() = {
println("I am TraitA")
}
}

trait TraitB {
def sayHi() = {
println("I am TraitB")
}
}

class Sub extends TraitA with TraitB {
override def sayHi(): Unit = {
println("I am Sub")
}
}


object Test {
def main(args: Array[String]): Unit = {
val sub = new Sub
sub.sayHi()
}
}

上面的程序通过重写自己的逻辑来解决冲突,输出如下

1
I am Sub

输出符合我们的直觉。前面提到还有另一种方式,即可以利用super来调用特征的实现,这样我们就很自然要联想到这样会调用哪一个实现,下面将代码进行修改,仅修改Sub类中的实现如下:

1
2
3
4
5
class Sub extends TraitA with TraitB {
override def sayHi(): Unit = {
super.sayHi()
}
}

得到的输出如下,我们可以看到实际上默认的调用是调用了最后一个with进来的特征的实现

1
I am TraitB

当然我们还可以在调用super的时候通过方括号[]来显式指定调用哪一个实现

1
2
3
4
5
class Sub extends TraitA with TraitB {
override def sayHi(): Unit = {
super[TraitA].sayHi()
}
}

自然得到输出如下:

1
I am TraitA

菱形继承:一个类混入的两个特征中具有相同的方法,并且这两个特征继承自相同的特征,即所谓的菱形继承问题。解决这类冲突问题,Scala采用了特征叠加的策略。

考察如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
trait TraitC {
def sayHi() = {
println("I am TraitC")
}
}

trait TraitA extends TraitC {
override def sayHi() = {
super.sayHi()
println("I am TraitA")
}
}

trait TraitB extends TraitC {
override def sayHi() = {
super.sayHi()
println("I am TraitB")
}
}

class Sub extends TraitA with TraitB {
override def sayHi(): Unit = {
super.sayHi()
}
}


object Test {
def main(args: Array[String]): Unit = {
val sub = new Sub
sub.sayHi()
}
}

当然我们可以在Sub中实现自己的逻辑,那么就正常按照逻辑输出。这里我们考虑的是在使用super的默认场景,我们得到的输出如下

1
2
3
I am TraitC
I am TraitA
I am TraitB

看起来是都运行了,并且顺序也有所讲究。实际上,Scala中对这种情况的处理采用了特征叠加的策略,当一个类混入多个特征的时候,Scala会对其所有的特征以及其父特征按照一定的顺序进行排序,按照举例代码来说,排序规则如下:

  1. 列出混入的第一个特征的继承关系,作为临时叠加顺序:TraitA -> TraitC
  2. 列出混入的第二个特征的继承关系,并将该顺序叠加到上一步顺序的前面,其中已经出现的特征将不再重复:TraitB -> TraitA -> TraitC
  3. 之后将子类放在临时叠加顺序的最后一个,得到最终的叠加顺序:Sub -> TraitB -> TraitA -> TraitC

之后代码执行的时候,其中的super并不是标识父对象,而是表示在叠加顺序中的下一个。

当然,如果要指定调用某一个混入特征中的方法,同样可以利用[]来增加约束,但是需要注意只能指定有直接关系的特征。(不能指定TraitC)

1
2
3
4
5
class Sub extends TraitA with TraitB {
override def sayHi(): Unit = {
super[TraitA].sayHi()
}
}

输出如下

1
2
I am TraitC
I am TraitA

特征自身类型

特征的自身类型能够实现依赖注入的功能。一个类或者特征指定了自身类型的话,它的对象和子类对象就会拥有这个自身类型中所有的属性和方法。这种方式是将一个类或者特征插入到另一个类或者特征中,属性和方法就像直接复制插入过来一样,能够直接使用,但不是继承,也不能使用多态。

举例如下:这里的语法中,_位置是别名定义,也可以是其他任何的字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class User(val name: String, val password: String)

trait UserDao {
_: User =>
def insert() = {
println(s"insert into db: ${name} ${password}")
}
}
class RegisterUser(name:String, password:String) extends User(name, password) with UserDao

object Test {
def main(args: Array[String]): Unit = {
val user = new RegisterUser("xxx", "123456")
user.insert()
}
}

输出如下

1
insert into db: xxx 123456

由于特征UserDao中指定的特征自身类型为User,因此它可以访问其中的成员变量。但是在extends的时候,不能单独extends UserDao特征,还需要一个User对象才是合法操作

扩展

类型检查和转换

  • 判断类型:obj.isInstanceOf[T],确切匹配的类型或者父类都返回true。
  • 转换类型:obj.asInstance[T],转换为目标类型。
  • 获取类名:classOf[T],得到类对应的Class对象Class[T],转字符串结果是class package.xxx.className
  • 获取对象的类:obj.getClass

检查的都是运行时类型

定义类型别名

使用type关键字可以定义新的数据类型名称,本质上是一个类型的别名

1
2
3
4
5
6
object Test {
def main(args: Array[String]): Unit = {
type myString = String
var s: myString = "abc"
}
}

Scala学习笔记-核心特性(2)-面向对象
http://example.com/2022/05/01/Scala学习笔记-核心特性-2-面向对象/
作者
EverNorif
发布于
2022年5月1日
许可协议