特质当接口使用
Scala的特质可以像Java的接口那样工作,如下:
trait Logger {
def log(msg: String) // 定义一个抽象方法
}
无需使用abstract声明,特质中没有实现的方法默认就是抽象方法。子类可以实现,如下:
class ConsoleLogger extends Logger {
// override def log(msg: String): Unit = println(msg) // 也可以
def log(msg: String): Unit = println(msg) // 无须写override
}
可以用with关键字来添加多个特质,如下:
class ConsoleLogger extends Logger with Cloneable with Serializable
注意:在Sala中,Logger with Cloneable with Serializable首先是一个整体,然后再由类来扩展。
有具体实现的特质
在Scala中,特质中的方法并不需要一定是抽象的,如下:
trait Logger {
def log(msg: String) // 定义一个抽象方法
def logs(msg: String) {
println(msg)
}
}
带有特质的对象
在构造单个对象时,可以为它添加特质。先定义这样一个类:
class Demo
val demo = new Demo with ConsoleLogger
println(demo.log("========"))
叠加在一起的特质
可以为类或对象添加多个互相调用的特质,从最后一个开始。如下例子:
trait TimestampLogger extends ConsoleLogger {
override def log (msg: String) {
super.log(s"${java.time.Instant.now()} $msg" )
}
}
对特质而言,super.log
并不像类那样拥有相同的含义。实际上,super.log
调用的是另一个特质的log方法,具体是哪一个特质取决于特质被添加的顺序。对于简单的混入序列而言,“从后往前”的规则会带给你正确的直觉判断。
对特质而言,你无法从源码判断super.someMethod
会执行哪里的方法。确切的方法依赖于使用这些特质的对象或类给出的顺序。这使得super相比在传统的继承关系中要灵活得多。
如果你需要控制具体是哪一个特质的方法被调用,则可以在方括号中给出名称:super.[ConsoleLogger]. log( ... )
。这里给出的类型必须是直接超类型;你不能使用继承层级中更远的特质或类。
特质中重写抽象方法
trait Logger {
def log(msg: String) // 定义一个抽象方法
}
trait ConsoleLogger extends Logger {
def log(msg: String): Unit = {
super.log(msg)
}
}
现在ConsoleLogger类编译通不过。
根据正常的继承规则,这个调用永远都是错的,Logger.log
方法没有实现。但实际上,就像前面说到的,我们没法知道哪个log方法最终被调用,这取决于特质被混入的顺序。
Scala认为ConsoleLogger依旧是抽象的,它需要混入一个具体的log方法。因此你必须给方法打上abstract关键字以及override关键字,就像这样:
trait ConsoleLogger extends Logger {
abstract override def log(msg: String): Unit = {
super.log(msg)
}
}
特质中的具体字段
特质中的字段可以是具体的,也可以是抽象的。如果你给出了初始值,那么字段就是具体的。
trait ShortLogger extends Logger {
val maxLength = 15 // 具体的字段
abstract override def log(msg: String) {
super.log(
if (msg.length <= maxLength) msg
else s"${ msg.substring(O, maxLength - 3) } ...")
}
}
混入该特质的类将自动获得一个maxLength字段。一般而言,对于特质中的每一个具体字段 ,使用该特质的类都会获得一个字段与之对应。这些字段不是被继承的,它们只是简单地被加到了子类当中;这看上去像是一个很细微的差别,但这个区别很重要。
在JVM中,一个类只能扩展一个超类,因此来自特质的字段不能以相同的方式继承。由于这个限制, maxLength被直接加到了子类中,和子类字段一样,放在一起。可以把具体的特质字段当作针对使用该特质的类的“装配指令”,任何通过这种方式被混入的字段都自动成为该类自己的字段。
注意: 当你扩展某个类然后修改超类时,子类无须重新编译,因为虚拟机知道继承是怎么回事。不过当特质改变时,所有混入该特质的类都必须被重新编译。
特质的抽象字段
特质中未被初始化的字段在具体的子类中必须被重写。如下例子:
trait ShortLogger extends Logger {
val maxLength: Int // 抽象字段
abstract override def log(msg: String) {
super.log(
if (msg.length <= maxLength) msg
else s"${ msg.substring(O, maxLength - 3) } ...")
// 在这个实现中用到了maxLength字段
}
}
在一个具体的类中使用该特质时,必须提供maxLength字段。
特质构造顺序
和类一样,特质也可以有构造器,其由字段的初始化和其他特质体中的语句构成。例如:
trait FileLogger extends Logger {
val out= new PrintWriter("app.log")// 这是特质构造器的一部分
out.println(s"# ${java.time.Instant.now()}") // 这同样是特质构造器的一部分
def log(msg: String) {out.println(msg); out.flush()}
}
这些语句在任何混入该特质的对象于构造时都会被执行。
构造器以如下顺序执行:
- 首先调用超类的构造器;
- 特质构造器在超类构造器之后、类构造器之前执行;
- 特质由左至右被构造;
- 每个特质当中,父特质先被构造;
- 如果多个特质共有一个父特质,而那个父特质已经被构造,则该父特质不会被再次构造;
- 所有特质构造完毕,子类被构造。
例如:
class Demo extends Demo1 with FileLogger with ShortLogger
构造器将按照如下顺序执行:
- Demo1(超类);
- Logger(第一个特质的父特质);
- FileLogger(第一个特质);
- ShortLogger(第二个特质,它的父特质Logger已经被构造);
- Demo(类)。
说明: 构造器的顺序是类的线性化的反向。线性化是描述某个类型的所有超类型的一种技术规格,其按照以下规则定义:
If C extends C1 with C2 with … with Cn , then lin© = C >> lin(Cn) >> … >> lin(C2) >> lin(C1)
这里 >> 的意思是 “串接并去掉重复项,右侧胜出”。线性化给出了在特质中 super 被解析的顺序。
初始化特质中的字段
特质不能有构造器参数,每个特质都有一个无参数的构造器。缺少构造器参数是特质与类之间唯一的技术差别。除此之外,特质可以具备类的所有特性, 比如具体的和抽象的字段,以及超类。例如:
val demo = new Demo with FileLogger("myapp.log") // 错误:特质不能使用构造器参数
FileLogger可以有一个抽象的字段用来存放文件名。例如:
trait FileLogger extends Logger {
val filename: String
val out= new PrintWriter("app.log")
def log(msg: String) {out.println(msg); out.flush()}
}
使用该特质的类可以重写filename字段。不过很可惜,这里有一个陷阱,像这样直截了当的方案并不可行:
val Demo = new Demo with FileLogger{
val filename = "myapp.log" // 这样行不通
}
问题出在构造顺序上FileLogger构造器先于子类构造器执行。这里的子类并不那么容易看得清楚;new语句构造的其实是一个扩展自Demo(超类)并混入了FileLogger特质的匿名类的实例。filename的初始化只发生在这个匿名子类中。实际上,它根本不会发生到子类之前,FileLogger的构造器就会抛出一个空指针异常。
解决办法之一:提前定义(early definition)。 以下是正确的版本:
val Demo = new { // new之后提前定义块
val filename = "myapp.log"
} with Demo with FileLogger
提前定义发生在常规的构造序列之前。FileLogger被构造时, filename已经是初始化过的了。
解决办法二:是在FileLogger构造器中使用懒值,就像这样:
trait FileLogger extends Logger {
val filename: String
lazy val out= new PrintWriter("app.log")
def log(msg: String) {out.println(msg); out.flush()}
}
out字段将在初次被使用时才会初始化,而在那个时候,filename字段应该已经被设好值了;不过,由于懒值在每次使用前都会检查是否已经初始化,因此,它们用起来并非那么高效。
扩展类的特质
特质可以扩展另一特质,而由特质组成的继承层级也很常见,不那么常见的一种用法是,特质也可以扩展类,这个类将自动成为所有混入该特质的超类。
例如,LoggedException特质扩展自Exception类:
trait LoggedException extends Exception with ConsoleLogger{
def log() { log(getMessage()) }
}
LoggedException有一个log方法用来记录异常的消息。注意log方法调用了从Exception超类继承下来的getMessage()方法。
特质的超类也自动地成为我们的类的超类,创建一个混入该特质的类:
class UnhappyException extends LoggedException{
override def getMessage() = "xxx.log"
}
如果我们的类已经扩展了另一个类,只要是特质的超类的一个子类即可。 例如:
class UnhappyException extends IOException with LoggedException
这里的UnhappyException扩展自IOException,而这IOException已经是扩展自Exception了;在混人特质时,它的超类已经在那里了,因此无须额外再添加。不过,如果我们的类扩展自一个不相关的类,那么就不可能混入这个特质了。
自身类型
当特质扩展类时,编译器能够确保的一件事是,所有混入该特质的类都将这个类作为超类;Scala还有另一套机制可以保证这一点:自身类型(self type)。
当特质以如下代码开始定义时,
this: 类型 =>
它便只能被混人指定类型的子类。例如:
trait LoggedException extends ConsoleLogger{
this: Exception =>
def log(){log(getMessage())}
}
注意该特质并不扩展Exception类,而是有一个自身类型Exception。这意味着,它只能被混入Exception的子类。在特质的方法中,我们可以调用该自身类型的任何方法。
如果你想把这个特质混入一个不符合自身类型要求的类,就会报错,如下:
val f = new JFrame with LoggedException
// 错误: JFrame不是Exception的子类型,而Exception是LoggedException的自身类型
带有自身类型的特质和带有超类型的特质很相似。这两种情况都可确保混入该特质的类能够使用某个特定类型的特性。
在某些情况下自身类型这种写法比超类型版的特质更灵活,自身类型可以解决特质间的循环依赖,如果你有两个彼此需要的特质时,循环依赖就会产生。
自身类型也同样可以处理“结构类型(structural type)”,这种类型只给出类必须拥有的方法,而不是类的名称;以下是使用结构类型的LoggedException定义:
trait LoggedException extends ConsoleLogger{
this: {def getMessage(): String}=>
def log(){log(getMessage())}
}
这个特质可以被混入任何拥有getMessage方法的类。
参考:快学scala(第二版)