教材:快学Scala
chapter 10. 特质 Traits
- Scala特质与Java接口不同,Scala特质可以给出这些特性的缺省实现
- class可以implement任意数量的trait
- trait可以要求实现它们的class具备特定字段/方法/超类
- 叠加多个trait时,排在后面的trait的方法会更优先执行
10.1 为什么没有多重继承
- 即不允许从多个超类继承,和Java一样
- 原因 菱形继承问题 C++的解决方案:虚基类
- Java 没有可以实现任意数量接口 但只能包含抽象方法 不能包含字段
- Scala trait 同时拥有抽象方法和具体方法 未被实现的方法都默认是抽象的
10.2 当接口使用的特质
class ConsoleLogger extends Logger with Cloneable with Serializable { def log(msg: String) }
- 1个超类 >=0个特质
10.3 带具体实现的特质
- trait可以带具体实现的方法,继承这个trait也叫做"混入"了这个trait
- 重写具体方法时加上
override
关键字
10.4 带有特质的对象
- new一个对象的时候可以加上trait
trait Logged {
def log(msg: String) { } // 一个空的实现
}
class SavingAccount extends Account with Logged {
def withDraw(...) {
...
log("xxx") // 混入Logged的log方法 但目前还是空的实现
}
}
trait ConsoleLogger extends Logged {
override def log(msg: String ) {...} // 有一个更好的实现
}
val acct = new SavingAccount with ConsoleLogger // 在具体对象中混入更好的实现,不再是空实现
val acct2 = new SavingAccount with FileLogger
10.5 叠加在一起的特质
- 从最后一个trait开始调用
- trait
super.方法
含义与类的super.方法
不一样!!调用的是trait层级中的下一个trait。下一个意思:根据trait的with顺序决定!! - 也可以具体制定哪个super的方法:
super[ConsoleLogger].log(...)
但只能是直接超类型
10.6 重写抽象方法
- 问题:万一
super.方法
所对应的那个trait方法是抽象方法怎么办? - 解决方法:重写的方法加上
abstract
关键字
abstract override def log(msg: String) { super.log(...) }
10.7 具体方法中调用抽象方法
- 在同一个trait中,具体方法里可以调用抽象方法和抽象字段,在被继承的时候再混入相应的具体方法和具体字段
trait Logger {
def log(msg: String)
def info(msg: String) { log("INFO: " + msg) } // 具体方法里调用了抽象方法
...
}
10.8 特质中的字段
- trait的字段可具体可抽象,给出初始值就是具体的
- 实现该trait的子类,这些trait的字段是被简单加到子类中的,不是被继承的
- 不能说是被继承的原因:一个类只能extends一个超类,因此来自trait的字段只能是作为子类的其中一个字段
- 抽象字段必须在子类中被重写
val acct = new SavingsAccount with ConsoleLogger with ShortLogger {
val maxLength = 20 // 在ShortLogger里的maxLength是抽象的,所以在被with的时候需要具体化
}
10.10 构造器执行顺序
- trait构造器 与类构造器类似 由trait体语句构成
- 构造器执行顺序:超类->左边的trait->右边的trait->子类
- trait构造器内部执行顺序:先执行父trait构造器(多个特质共有一个父的情况只执行一次构造)
e.g.class SavingsAccount extends Account with FileLogger with ShortLogger
构造器执行顺序:
Account(超类)->Logger(FileLogger的父trait)->FileLogger(最左边trait)->ShortLogger(下一个trait)->SavingsAccount(子类)
10.11 初始化抽象字段
- trait和类的区别:trait不能有构造器参数
- 用abstract val代替trait构造器参数
- 陷阱:可能在初始化抽象字段时,由于构造器执行顺序问题导致空指针异常
- 解决方法:对应用到抽象字段的val加上
lazy
关键字
10.12 trait extends class
- trait可以extends trait也可以extends class
trait T1 extends C1 {...} // T1扩展了类C1
class C2 extends T1 {...} // C1自动变成C2的父类!!
class C3 extends C0 with T1 {...} // 条件:C1是C0的父类!!
10.13 self type机制
- 问题:如何从编译机制上保证C0是C1的子类?或者更一般地,如何保证T1只能被某个父类(C1)的子类所扩展?
- 解决方案:在T1的定义里加入self type(自身类型)指定C1
trait T1 {
this: C1 =>
...
} // 指定了T1只能混入C1的子类
- 还可以指定T1只能混入【带有特定方法】的类,这叫做structural type(结构类型)
trait T1 {
this: { def getMsg(): String } =>
...
} // 指定了T1只能混入带有getMsg方法的类