为什么没有多重继承
和Java相同,scala也不允许多重继承,如果允许,则当这些父类有了共同的字段或者方法时,子类对应字段或方法的获取就会存在歧义。
比如助教类继承了老师和学生两个类,并且老师和学生两个类都有自己的id,那么助教的id到底应该取哪个?
还有一种情况,假设老师和学生两个类都继承自人这个类,那就产生了菱形继承问题。

这种情况下如何合并name字段,又如何构造呢?
在Java中,类只能扩展自一个父类,可以实现任意数量的接口,但接口只能包括抽象方法,静态方法和默认方法,不能包括字段(属性)。
Java的默认方法有局限性,它可以调用其他接口的方法,但不能使用对象状态。因此,Java中经常要同时提供接口和抽象基类。如果要同时扩展两个抽象基类就不行了。
Scala提供特质trait而非接口,特质可以同时拥有抽象方法和具体方法,以及状态。
而类可以实现多个特质,这个设计解决了Java的接口问题。
当作接口使用的特质
特质的用法和Java接口类似
1 2 3
| trait Logger{ def log(msg:String) }
|
类可以实现特质,并且特质中的方法不需要标注为abstract,未实现的方法默认就是抽象的
1 2 3 4 5
| class ConsoleLogger extends Logger{ def log(msg:String){ print(msg) } }
|
用with添加多个trait
1
| class ConsoleLogger extends Logger with Cloneable with Serializable
|
所有Java接口都可以当成Scala的特质来实现
带有具体实现的特质
特质中的方法不一定是抽象的,特质中可以包括带有实现的方法,而扩展了这个特质的类可以直接调用这些实现的方法。
1 2 3 4 5 6 7 8 9 10
| trait ConsoleLogger{ def log(msg:String) {print(msg)} }
class SavingsAccount extends Account with ConsoleLogger{ def withdraw(amount: Double){ if(amount>balance) log("Insufficient funds") else balance -= amount } }
|
带有特质的对象
先构造一个抽象类添加特质
1 2 3 4 5 6
| abstract class SavingsAccount extends Account with Logger{ def withdraw(amount: Double){ if(amount>balance) log("Insufficient funds") else balance -= amount } }
|
抽象类无法直接实例化,可以在构造对象的时候混入一个具体的方法实现,通过这种方式实现抽象方法。
1 2 3 4 5 6 7 8
| trait ConsoleLogger extends Logger{ def log(msg:String) {print(msg)} }
val acct = new SavingsAccount with ConsoleLogger
val acct2 = new SavingsAccount with FileLogger
|
叠加在一起的特质
可以为类或者对象添加多个互相调用的特质,从最后一个开始,适用于分阶段加工处理某个值的场景。
当我们想给所有日志添加时间戳时:
1 2 3 4 5
| trait TimestampLogger extends ConsoleLogger{ override def log(msg:String){ super.log(s"${java.time.Instant.now()} $msg") } }
|
想修改过长的日志:
1 2 3 4 5 6
| trait shortLog extends ConsoleLogger{ override def log(msg:String){ if(msg.length>10)msg = msg.subString(0,10) super.log(s"$msg") } }
|
一下两种对象的初始化,在打印日志时输出结果完全不同
1 2
| val acc1 = new SavingsAccount with TimestampLogger with ShortLogger val acc2 = new SavingsAccount with ShortLog with TimestampLogger
|
acc1就会先截取,后加时间戳
acc2就会先加时间戳,后截取。
在简单的混入序列中,特质是从后往前生效的,即后面的特质先处理。
在特质中重写抽象方法
上面两个特质都调用了扩展特质ConsoleLogger的log方法。但如果这里的log方法是抽象的,比如直接扩展了Logger,则报错。
如果想直接调用这种抽象方法,则新的特质中方法也是抽象的,必须加上abstract和override关键字
1 2 3 4
| abstract override def log(msg:String){
super.log(s"$msg....") }
|
当作富接口使用的特质
特质可以包含大量工具方法,这些方法可以依赖一些抽象方法来实现。
1 2 3 4 5 6
| trait Logger{ def log(msg:String) def info(msg:String) {log(s"Info: $msg")} def warn(msg:String) {log(s"Warn: $msg")} def servere(msg: String){log(s"SEVERE: $msg")} }
|
之后扩展了Logger特质的类就可以调用这些方法了。注意,如果仍不实现log方法,只能在抽象类中调用,否则要实现这个抽象方法。
1 2 3 4 5
| class Test extends Logger{ def log(msg:String) = {print(msg)} def getWarn(msg:String) = warn(msg) def getInfo(msg:String) = info(msg) }
|
特质中的具体字段
特质中可以包含字段,这个字段也可以是具体的或者抽象的。如果给出了初始值,则字段就是具体的。
1 2 3 4 5 6 7 8 9
| trait ShortLogger extends Logger{ val maxLength = 15 abstract override def log(msg:String){ super.log( if(msg.length<=maxLength) msg else s"${msg.substring(0,maxLength-3)}..." ) } }
|
注意,特质中的字段只是被加到了使用该特质的类中,而不是继承关系。具体区别在于继承自超类的字段属于超类对象,当超类改变时,子类不需要重新编译就会拿到最新的值(引用关系),而特质内的字段会被归为该类自己的字段,当特质改变时,所有扩展了这个特质的类都需要重新编译来改变这个字段的值。
特质中的抽象字段
特质中未被初始化的字段在具体的子类中必须被重写。
1 2 3
| class Test extends ShortLogger{ val maxLength = 20 }
|
特质构造顺序
特质也有构造器,由字段初始化和特质体中其他语句构成
1 2 3 4 5 6
| trait FileLogger extends Logger{ val out = new PrintWriter("app.log") out.println(s"a new logger") def log(msg:String){...} }
|
语句在任何混入该特质的对象构造时会被执行
执行顺序如下:
- 调用超类的构造器
- 特质构造器在超类构造器后,类构造器前执行
- 特质由左到右构造
- 父特质先构造(多个特质有相同的夫特质,只构造一次)
- 子类最后构造
1
| class SavingAccount extends Account wiht FileLogger with ShortLogger
|
- Acount (超类)
- Logger(父特质)
- FileLogger(左一特质)
- ShortLogger(左二特质)
- SavingAccount(类)
初始化特质中的字段
特质不能有构造器参数,每个特质都带有一个默认的无参构造器。
缺少构造器参数时类和特质唯一的区别
如果需要初始化时给特质中的字段赋值,好的做法是将特质中需要使用某个字段的值定义为懒值lazy,之后在初始化后对特质中的值进行赋值。
1 2 3 4 5
| trait FileLogger extends Logger{ val fileName:String lazy val out = new PrintStream(filename) def log(msg:String){...} }
|
扩展类的特质
特质也可以扩展类,这个类将自动成为所有混入了该特质的超类
1 2 3 4 5 6 7
| trait LoggedException extends Exception with ConsoleLogger{ def log(){log(getMessage())} }
class TestException extends LoggedException{ override def getMessage() ="aaa!" }
|
特质扩展了超类Exception,并且调用了其方法getMessage(),TestException类在混入这个特质的时候自动成为类Exception的子类。
自身类型
混入特质时,编译器要确保所有混入该特质的类都能把这个类当作超类。Scala还可以通过自身类型的机制保证这一点。当特质以如下代码开始定义时,就只能被混入指定类型的子类中。
1 2 3 4
| trait LoggedExcepption extends ConsoleLogger{ this:Exception => def log(){log(getMessage())} }
|
这个特质并不扩展Exception类,而是有一个自身类型Exception,这意味着只能被混入Exception的子类。
背后发生了什么
只有抽象方法的特质被简单的变成一个Java接口
特质方法对应的是Java的默认方法
如果有特质字段,对应的Java接口就有getter和settter方法