2019独角兽企业重金招聘Python工程师标准>>>
Scala提供“特质”而非接口。特质可以同时拥有抽象方法和具体方法,而类可以实现多个特质。
1.当作接口使用的特质
Scala特质完全可以像Java的接口那样工作。例如:
trait Logger{
def log(msg: String) //这个是抽象方法
}
注意,你不需要讲方法声明为abstract,特质中未被实现的方法默认就是抽象的。
子类可以给出实现:
class ConsoleLogger extends Logger{ //用extends,而不是implements
def log(msg: String){ println(msg) } //不需要写override
}
如果你需要的特质不止一个,可以用with关键字来添加额外的特质:
class ConsoleLogger extends Logger with Cloneable with Serializable
所有的Java接口都可以作为Scala特质使用,和Java一样,Scala类只能有一个超类,但可以有任意数量的特质。
2.带有具体实现的特质
在Scala中,特质中的方法并不需要一定是抽象的。举例来说,我们可以把我们的具体类变成一个特质:
trait Logger{
def log(msg: String){ println(msg) }
}
如何使用:
class ConsoleLogger extends Logger{
def write(msg: String){ log("Hello World")}
}
3.带有特质的对象
首先,特质也是可以被奇特特质扩展的
trait Logger{
def log(msg: String){ log("Hello World") }
}
trait ConsoleLogger extends Logger{
override def log(msg: String){ println(“ConsoleLogger:” + msg) }
}
trait FileLogger extends Logger{
override def log(msg: String){ println(“FileLogger:” + msg) }
}
你可以在构造对象的时候介入这个特质:
val acct = new SavingsAccount with ConsoleLogger
当我们在acct对象上调用log方法时,ConsoleLogger特质的log方法就会被执行。当然了,另一个对象可以加入不同的特质:
val act = new new SavingsAccount with FileLogger
4.叠加在一起的特质
//日志特质
trait Logger{
def log(msg: String){}
}
//打印日志
trait ConsoleLogger{
def log(msg: String){ println(msg) }
}
//给日志添加时间戳
trait TimestampLogger extends Logged{
override def log(msg: String){
super.log(new java.util.Date() + " " + msg)
}
}
//过长日志截断
trait ShortLogger extends Logged{
val maxLength = 15
override def log(msg: String){
super.log(if(msg.length <= maxLength) msg else msg.substring(0, maxLength -3) + "...")
}
}
//存款类
class SavingsAccount extends Logged{
def withdraw(amount: Double){
if(amount: Double) log("Insufficient funds")
else
...
}
}
特质的调用,要根据特指添加的顺序界定。一般来说特质是从最后一个开始被处理,例如:
val acct1 = new SavingsAccount with ConsoleLogger with TimestampLogger with ShortLogger
val acct2 = new SavingsAccount with ConsoleLogger with ShortLogger with TimestampLogger
如果我们调用acct1中的withdraw方法,我们将得到这样的消息:
Sun Feb 06 17:45:45 ICT 2011 Insufficient ...
因为ShortLogger首先执行,然后他的super.log调用的是TimestampLogger。
但是,执行acct2的withdraw方法输出的是:
Sun Feb 06 1...
这里,TimestampLogger在特质列表中最后出现。器log方法首先被调用,之后调用ShortLogger被截断
5.在特质中重写抽象方法
//日志特质
trait Logger{
def log(msg: String) //这是个抽象方法
}
现在,让我们用时间戳特质来扩展它,就像前一节中示例那样。很不幸,TimestampLogger 特质不能编译了。
//给日志添加时间戳
trait TimestampLogger extends Logged{
override def log(msg: String){ //重写抽象方法
super.log(new java.util.Date() + " " + msg) //super.log定义了吗?
}
}
根据正常的继承规则,这个调用永远都是错的,Logger.log方法没有实现。但实际上,吉祥前一节看到的我们没法知道那个log方法最终被调用,这个取决于特质被混入的顺序。
Scala认为TimestampLogger 依旧是抽象的,因此必须给方法加上abstract关键字以及override关键字:
trait TimestampLogger extends Logged{
abstract override def log(msg: String){
super.log(new java.util.Date() + " " + msg)
}
}
6.当做富借口是用的特质
特质包含了大量的工具方法,而这些工具方法可以依赖一些抽象方法来实现。
trait Logger{
def log(msg: String) //这是个抽象方法
def info(msg: String) {log("INFO: " + msg)}
def warn(msg: String) {log("WARN: " + msg)}
def servere(msg: String) {log("SEVERE: " + msg)}
}
注意我们是怎样把抽象方法和具体方法结合在一起的。
这样使用Logger特质的类就可以任意调用这些日志消息方法了。例如:
//存款类
class SavingsAccount extends Logged{
def withdraw(amount: Double){
if(amount: Double) servere("Insufficient funds")
else
...
}
...
override def log(msg: String){ println(msg); }
}
7.特质中的具体字段
特质中的字段可以试具体的可以试抽象额,如果给了初始值,name字段就是具体的。
通常,对于特质中每一个具体字段,使用该特制的类会获得一个字段预支对应。这些字段不是被继承的;他们只是简单的被加入到了子类当中。
在JVM中,一个类智能扩展一个超类,因此来自特质的字段不能以相同的方式继承。由于这个限制,特质中的字段被接入到了子类当中,并非父类字段的继承。
8.特质构造顺序
构造器以如下顺序执行:
- 首先调用超累的构造器。
- 特质构造器在超累构造之后、类构造器之前执行。
- 特质由左到右被构造。
- 每个特质当中,父特质先被构造。
- 如果多个特质共有一个父特质,而那个父特质已经被构造,则不会再次构造
- 所有特质构造完毕,子类被构造。
例如:
class SavingAccount extends Account with FileLogger with ShortLogger
构造器将按照如下的顺序执行:
- Account(超类)
- Logger(第一个特质的父特质)
- FileLogger(第一个特质)
- ShortLogger(第二个特质)。注意他的父特质L噢GG而已被构造
- SavingsAccount(类)
9.初始化特质中的字段
特质不能有构造器参数。每个特质都有一个无参数的构造器。
说明:缺少构造器参数是特质与类之间唯一的技术差别
提前定义:
trait Logger {
def log(msg: String)
}
trait FileLogger extends Logger{
val filename: String
val out = new PrintStream(filename)
def log(msg: String){ out.println(msg); out.flush()}
}
class SavingsAccount extends Logged{
def withdraw(amount: Double){
if(amount: Double) servere("Insufficient funds")
else
...
}
...
override def log(msg: String){ println(msg); }
}
val acct = new SavingsAccount with FileLogger {
val filename = "Hello" //将会报错,因为他的初始化顺序在FileLogger 之后,使得FileLogger初始化时filename 字段未完成初始化报错
}
解决办法:
val acct = new {
val filename = "Hello"
} with SavingsAccount with FileLogger
如果你需要在类中做同样的事,可以这样
class SavingsAccount extends{
val filename = "Hello"
}with FileLogger {
...
}
或者在FileLogger定义中使用懒值,但是并不高效
trait FileLogger extends Logger{
val filename: String
lazy val out = new PrintStream(filename)
def log(msg: String){ out.println(msg); out.flush()}
}
10.扩展类的特质
一种不常见的用法是,特质也可以扩展类。这个被特质扩展的类将会自动成为所有混入该特质的超类。
假定有如下代码:
trait LoggedException extends Exception with Logged{
def log() { log(getMessage()) }
}
class UnhappyException extends LoggedException{
...
}
那么UnhappyException的超类自动变成LoggedException特质的扩展类Exception
如果我们的类已经扩展了另一个类怎么办?没关系,只要是特质的超类的一个子类即可。如果我们的类扩展了一个不相关的类,那么就不可能混入这个特质了。
11.自身类型
当特质扩展类时,编译器能够确保的一件事是所有混入该特质的类都认为这个类作超类。Scala还有另一套机制可以保证这一点:自身类型。
当特质以如下代码开始定义时:
this:类型 =>
它便只能被混入指定类型的子类
trait FileLogger extends Logger{
this: Test => //Test必须是接口或者特质
def log(msg: String){ println(msg); }
}
自身类型同样也可以处理结构类型,这种类型只给出类必须拥有的方法,而不是特质或接口名称:
trait LoggedException extends Logger{
this: {def getMessage(): String} =>
def log(msg: String){ println(msg); }
}
这个特质可以被混入任何拥有getMessage方法返回值类型为String的类