Scala特质(特征)Trait
场景简介
例:
某助教(TeachingAssistant.class)。他既是学生(Student.class)也是员工(Employee.class)。
理想情况是:
class Student { def id: String = ... ... } class Employee { def id: String = ... ... } // 提醒:这是无法实现的多重继承示例 Class TeachingAssistant extends Student, Employee { ··· }
问题来了:scala和java一样不允许类从多个超类继承,但为什么不能有多重继承呢?
如果是 毫不相干 的多个类,多重继承只是把他们组合在一起,只要实现各自类中的方法就没问题。但如上面的例子所示,TeachingAssistant类继承了两个id方法,这显然是有冲突和混乱的(调用助教对象中的id方法时,无法确定返回学生id还是员工id)
再复杂些,假定Student类和Employee类都扩展自同一个超类Person:
class Person { var name: String = _ } class Student extends Person { ... } class Employee extends Person { ... }
这就引出了菱形继承问题
Teaching Assistant中的name字段要如何合并呢?又如何被构造呢?——扩展知识:C++的“虚拟基类”
Java的解决办法是采取强限制策略来规避这种使用:类只能扩展自一个超类;它可以实现任意数量的接口【但接口只能包含抽象方法,不能包含字段**Java8的特性中允许接口包含静态方法static和默认方法default,只是看起来似乎违反了接口作为一个抽象定义的理念】
Scala提供“特质”而非接口来解决这个问题。
特质比java接口更强大
首先,特质最大的特点就是可以同时拥有抽象方法和具体方法,与此同时一个基类可以像实现接口一样实现多个特质。
// 例如有一个Logger的特质(包含抽象方法) trait Logger { // 这是个抽象方法(但不需要用abstract特别声明,因为特质中没有实现的方法默认就是抽象的) def log(msg: String): Unit } // 子类可以给出具体实现 // 用的是extends,而不是implements class ConsoleLogger extends Logger { // 不需要写override,写上也没事 def log(msg: String): Unit = { println(msg) } } // --------------------------------我是分割线----------------------------------------- // 再例如有一个ConsoleLogger的特质(包含具体方法) trait ConsoleLogger { def log(msg: String): Unit = { println(msg) } } // 子类使用ConsoleLogger class SavingsAccount extends Account with ConsoleLogger { // 当账单金额大于余额时,显示账户余额不足,否则正常扣款 def withdraw (amount: Double) { // 这里直接调用了ConsoleLogger特质中的log方法来实现 if (amount > balance) log("Insufficient funds") else balance -= amount } } // --------------------------------我是分割线----------------------------------------- // tips:如果需要一个类需要继承多个特质的话,用with关键字 trait Trait1 { ··· } trait Trait2 { ··· } trait Trait3 { ··· } // 实际上extends的是**(Trait1 with Trait2 with Trait3)**这么个特质的整体 class example extends Trait1 with Trait2 with Trait3 { ··· } // 扩展知识:类的构造顺序(从左到右)是线性化(super被解析的顺序,从右到左)的反序 // 1.继承的超类 // 2.第一个特质的父特质(如果有的话) // 3.第一个特质 // 4.第二个特质的父特质(如果有的话),如果该特质此前已被构造,则跳过 // 5.第二个特质 // ··· // ··· // 特质不能有构造器参数。 // 缺少构造器参数是特质和类之间唯一的技术差别。除此之外,特质可以具备类的所有特性,包括具体和抽象的字段、方法,以及超类
当我们在SavingAccount中使用到了ConsoleLogger中的log方法时,可以说ConsoleLogger的功能被“混入”了SavingAccount类。
但需要注意的是,让特质拥有具体实现存在一个弊端,即当特质改变时,所有混入了该特质的类都必须重新编译。
带有特质的对象以及特质的叠加
构造单个对象时,也可以为对象添加特质。下面来看看构造对象时如何使用特质:
// 首先,我们使用一个方法实现为空的Logged特质 trait Logged { def log(msg: String): Unit = { // 什么也不做 } } // 此时将Logged特质混入类中时,log方法理所当然不会打印任何东西 class SavingsAccount extends Account with Logged { def withdraw (amount: Double) { // Logged方法不会打印"Insufficient funds" if (amount > balance) log("Insufficient funds") // 假设Account类中也有log方法,我们可以指定使用哪个log方法 ↓ // if (amount > balance) super[Logged].log("Insufficient funds") else balance -= amount } } // 但我们可以基于Logged构造更好的特质,并在构造SavingsAccount实例对象时混入他们 trait ConsoleLogger extends Logged { override def log(msg: String): Unit = { println("¥¥¥:" + msg) } } trait FileLogger extends Logged { override def log(msg: String): Unit = { println("***:" + msg) } } // 特质的叠加 class Test { val acct1 = new SavingsAccount with ConsoleLogger val acct2 = new SavingsAccount with FileLogger acct1.log("测试1的信息") // ¥¥¥:测试1的信息 acct2.log("测试2的信息") // ***:测试2的信息 // 下面的情况会如何呢? val acct3 = new SavingsAccount with FileLogger with ConsoleLogger val acct4 = new SavingsAccount with ConsoleLogger with FileLogger // 特质的方法调用顺序是从后往前,即后继承的方法优先被调用,可以通过加方括号的方法来明确要调用哪个特质的方法super[xxx].xxx(),例子在上面 acct3.log("测试3的信息") // ***:¥¥¥:测试3的信息 acct4.log("测试4的信息") // ¥¥¥:***:测试4的信息 }
在特质中重写抽象方法
让我们回到最开始那个只有抽象方法的Logger特质:
// 例如有一个Logger的特质(包含抽象方法) trait Logger { // 这是个抽象方法(但不需要用abstract特别声明,因为特质中没有实现的方法默认就是抽象的) def log(msg: String): Unit } // 现在我们用时间戳特质来扩展它,提醒:下列代码是编译不过的 trait TimestampLogger extends Logger { override def log(msg: String): Unit = { super.log(new java.util.Date() + "" + msg) // super.log定义了吗? } }
scala会认为super.log调用的是Logger中的log方法,而不是TimestampLogger重写的log方法,因此会报错。正确的写法应该是:
trait TimestampLogger extends Logger { // 需要加入abstract关键字,此时的TimestampLogger依旧是抽象的 abstract override def log(msg: String): Unit = { super.log(new java.util.Date() + "" + msg) } }
当做富接口使用的特质
特质可以包含许多工具方法,这些工具方法可以依赖某几个抽象方法来实现。例如日志中的分级:
// info、warn、severe是依赖于抽象方法log实现的具体方法,我们只需要在外部类中实现了log方法,即可任意调用这几个日志消息方法 trait Logger { def log(msg: String) def info(msg: String) { log("INFO: " + msg) } def warn(msg: String) { log("WARN: " + msg) } def severe(msg: String) { log("SEVERE: " + msg) } } class SavingAccount extends Account with Logger { def withdraw(amount: Double): Unit = { // 打印的内容是:【我在前面:SEVERE: Insufficient funds】 if (amount > balance) servre("Insufficient funds") else ··· } ··· override def log(msg: String): Unit = { println("我在前面:" + msg) } }
想想在JAVA中要如何实现上述代码呢?
1:首先我们需要一个只包含了log方法的接口Logger。
2:接着构造一个扩展至它的类CollectLogger,实现log方法,并基于log方法实现info、warn、severe方法。
3:在外部使用的时候调用CollectLogger里对应的方法。
特质中的字段
同特质中的方法类似,特质中的字段也可以是具体的,也可以是抽象的。如果给出了初始值,那么字段就是具体的。
// 特质 trait ShortLogger extends Logged { val minLength: Int = 2 // 具体的字段 val maxLength: Int // 抽象的字段 override def log(msg: String) { // 截断多余长度,用到了抽象字段maxLength super.log( if (msg.length <= maxLength) msg else msg.substring(0, mnaxLength -3) + "..") ··· } } // 例子中的父类 class Account { val balance: Double = 0.0 } // 混入了特质的Account类的子类SavingAccount class SavingAccount extends Account with ShortLogger { // 抽象字段必须提供,此时你可以使用ShortLogger中的log方法,override可以省略 override val maxLength: Int = 20 val interest: Int = 3 println(minLength + interest) // 5 }
混入了ShortLogger特质的类会自动获得具体字段minLength,但获得的方式不是继承。区别于从父类中继承的字段,minLength字段和子类自身拥有的字段性质相同。
SavingAccount必须实现ShortLogger中的抽象字段maxLength(除非声明为abstract类),与此同时,log方法会自动截断对应长度的日志。
初始化特质中的字段
当特质中某个具体方法需要传参,但特质的构造器是无法传参的,因此只能用抽象字段代替(在子类中重写这个抽象字段仿佛是可行的)注意了,此时就会有坑。
trait FileLogger extends Logger { val filename: String val out = new PrintStream(filename) def log(msg: String) { out.println(msg) } }
class Person {
var name: String = _
}
class Student extends Person {
...
}
class Employee extends Person {
...
}
原因就在此前所说的构造顺序,子类构造器是在特质构造器之后工作的(意味着这个抽象字段还没被重写),那么特质的构造器运行就会抛出空指针异常。
解决方法之一是在构造子类时提前定义,因为提前定义发生在常规在构造序列之前,因此但特质中的方法调用到抽象字段时,该抽象字段已经被重写了。
// 对象构造 val acct = new { val filename = "myapp.log" } with SavingsAccount with FileLogger // 类构造 class SavingsAccount extends {val filename = "savings.log"} with Account with FileLogger { ··· // 类的实现 }
解放方法之二是对特质中需要传参的方法使用懒值,那么在该方法被外部调用时它才会初始化,而此时特质的抽象字段应当已经被重写好了。
trait FileLogger extends Logger { val filename: String // val out = new PrintStream(filename) lazy val out = new PrintStream(filename) def log(msg: String) { out.println(msg) } }
扩展类的特质
特质可以扩展另一个特质,同时还能扩展类,而这个类会自动成为所有混入该特质的类的超类(不常见)
trait LoggedException extends Exception with Logged { def log() { log(getMessage()) } }
此外,LoggedException还可以混入其他继承于Exception的子类中,因为超类不冲突,但LoggedException无法混入那些扩展自与Exception无关的类的子类中
class UnhappyFrame extends JFrame with LoggedException // 错误:不相关的超类
自身类型
当特质扩展类时,Scala还可以通过自身类型来确保所有混入该特质的类都认该类作为自己的超类。
// 此时的LoggedException就只能混入Exception的子类中了 trait LoggedException extends Logged { this: Exception => def log() { log(getMessage()) } }
背后发生了什么
Scala需要将特质翻译成JVM的类和接口。
1.只有抽象方法的特质被简单的变成了一个Java接口:
trait Logger { def log(msg: String) }
====>
public interface Logger { void log(String msg); }
2.如果特质有具体的方法,Scala会帮我们创建出一个伴生类,该伴生类用静态方法存放特质的方法。
trait ConsoleLogger extends Logger { def log(msg: String) {println(msg)} }
====>
public interface ConsoleLogger extends Logger { // 生成的java接口 void log(String msg); } public class ConsoleLogger$class { // 生成的Java伴生类 public static void log(ConsoleLogger self, String msg) { println(msg) } }
3.当某个类实现该特质时,字段会被自动加入。
trait ShortLogger extends Logger { val maxLength = 15 // 具体的字段 }
====>
public interface ShortLogger extends Logger { public abstract int maxLength(); // 特质中的字段对应到接口中的getter和setter方法中 public abstract void weird_prefix$maxLength_$eq(int); // 这些weird开头的setter方法是用来初始化该字段的,这个过程发生在伴生类的一个初始化方法内 ... } // 字段初始化 public class ShortLogger$class { public void $init$(ShortLogger self) { self.weird_prefix$maxLength_$eq(15); } ··· }