特质(trait)是scala里代码复用的基础单元。 特质封装了方法和字段的定义, 并可以通过混入到类中复用它们。 与类的继承时每个类都只能继承唯一的超类不同, 类可以混入任意多个特质。
当做接口使用的特质
Scala的特质完全可以像Java的接口那样工作。例如:
trait Logger{
def log(msg:String) // 抽象方法
}
class ConsoleLogger extends Logger{ // 使用 extends 而不是Java 中的 implements
// 不需要写 override
def log(msg: String): Unit = {
println(msg)
}
}
trait 不需要你将方法声明为abstract,未被实现的方法默认就是抽象的; 在重写特质的抽象方法时也不需要给出override关键字。
使用反编译看下上述样例Java的定义:
// borey-zhu@AWS-TEST-DT:~/scala$ scalac log.scala
// borey-zhu@AWS-TEST-DT:~/scala$ ls
// ConsoleLogger.class Logger.class log.scala
// borey-zhu@AWS-TEST-DT:~/scala$ javap -p Logger.class
// Compiled from "log.scala"
public interface Logger { // 生成java接口
public abstract void log(java.lang.String);
}
// borey-zhu@AWS-TEST-DT:~/scala$ javap -p ConsoleLogger.class
// Compiled from "log.scala"
public class ConsoleLogger implements Logger { // 实现接口
public void log(java.lang.String);
public ConsoleLogger();
}
带有具体实现的特质
在Scala中,特质中的方法并不需要一定是抽象的。 例如:
trait Logger{
def log(msg: String): Unit = {
println(msg)
}
}
class Person(val Name:String){}
class Student(name:String) extends Person(name) with Logger{
def learn(): Unit ={
log(Name + "is learning ... ")
}
}
注意: Student 从 Logger特质得到了一个具体的 log方法实现。 用Java接口的话, 这是不可能做到的。
对上述样例进行反编译,查看java是怎么进行定义的:
// borey-zhu@AWS-TEST-DT:~/scala$ javap -p Logger.class
// Compiled from "log.scala"
public interface Logger {
// log 方法带有具体的实现,会生成对应的静态方法
public static void log$(Logger, java.lang.String);
public void log(java.lang.String);
public static void $init$(Logger);
}
// borey-zhu@AWS-TEST-DT:~/scala$ javap -p Student.class
// Compiled from "log.scala"
public class Student extends Person implements Logger {
public void log(java.lang.String);
public void learn();
public Student(java.lang.String);
}
带有特质的对象
在构造单个对象时, 你可以为它添加特质。 例如:
trait Logger{
// 带一个什么都没有做的实现
def log(msg: String): Unit = {}
}
trait ConsoleLogger extends Logger{
// override 必须添加
override def log(msg: String): Unit = {
println(msg)
}
}
class Person(val Name:String){}
class Student(name:String) extends Person(name) with Logger{
def learn(): Unit ={
log(Name + " is learning ... ")
}
}
定义了一个Logger特质, log函数是一个什么都不做的实现,再定义一个标准的ConsoleLogger扩展字Logger进行重写log 方法; 下列定义一个带有特质的对象,例如:
scala> val s1 = new Student("borey") with ConsoleLogger // 加入特质 ConsoleLogger
s1: Student with ConsoleLogger = $anon$1@18d11527
scala> s1.learn
borey is learning ...
我们在构造对象的时候可以加入任意一个扩展自Logger特质。
叠加在一起的特质
你可以为类或对象添加多个互相调用的特质,这对于需要分阶段加工处理某个值的场景很有用。
以下是一个简单示例,添加带有时间戳日志的特质和 截断过于长的日志消息的特质:
trait Logger{
def log(msg: String): Unit = {}
}
// 控制台输出
trait ConsoleLogger extends Logger{
override def log(msg: String): Unit = {
println(msg)
}
}
// 带时间戳的特质
trait TimestampLogger extends Logger{
override def log(msg: String): Unit = {
super.log(new java.util.Date() + " " + msg)
}
}
// 带消息截断的特质
trait ShortLogger extends Logger{
val maxLen = 10
override def log(msg: String): Unit = {
super.log( if (msg.length < maxLen) msg else msg.substring(0, maxLen -3) + "...")
}
}
class Person(val Name:String){}
class Student(name:String) extends Person(name) with Logger{
def learn(): Unit ={
log(Name + " is learning ... ")
}
}
注意: 特质是有层级的,TimestampLogger
和 ShortLogger
中的 super.log
方法是调用层级中的下一个特质的方法; 具体调用哪个特质的方法,要根据特质的添加顺序来决定。 一般来说,特质是从最后一个开始被处理。
下面来看下不同顺序叠加的特质对象展现的结果:
scala> val s1 = new Student("borey") with ConsoleLogger with ShortLogger with TimestampLogger
s1: Student with ConsoleLogger with ShortLogger with TimestampLogger = $anon$1@6d7740f0
scala> s1.learn
Tue Oct...
scala> val s2 = new Student("borey") with ConsoleLogger with TimestampLogger with ShortLogger
s2: Student with ConsoleLogger with TimestampLogger with ShortLogger = $anon$1@192b472d
scala> s2.learn
Tue Oct 17 11:29:42 GMT+08:00 2017 borey is...
根据变量 s1 和 s2, 调用learn展现出的不同结果可以看出,不同的叠加顺序会产生不同的结果(从右向左依次被调用), 以 s2 为例:
graph TD
A[Student.learn 中的 log 方法调用] --> B[ShortLogger 的 log 方法]
B --> C[TimestampLogger 的 log 方法]
C --> D[最后调用 ConsoleLogger 的 log 方法]
说明: 对特质而言, 你无法从源码中判断super.Method 会执行哪里的方法, 确切的方法依赖于使用这些特质的对象或类给出的顺序。 这使得super相比在传统继承关系中要灵活得多。
在特质中重写抽象方法
假如上述定义的Logger 并没有提供 log 方法的实现会怎样? 当我们再添加一个带时间戳的特质,该如何定义?
这时候必须给方法加上 abstract 关键字, 在使用时间戳特质的时候必须混入一个具体的 log 方法。 例如:
trait Logger{
def log(msg: String)
}
trait TimestampLogger extends Logger{
// 必须添加 abstract关键字, 告诉编译器 TimestampLogger 依旧为抽象的
abstract override def log(msg: String): Unit = {
super.log(new java.util.Date() + " " + msg)
}
}
trait ShortLogger extends Logger{
val maxLen = 15
abstract override def log(msg: String): Unit = {
super.log( if (msg.length < maxLen) msg else msg.substring(0, maxLen -3) + "...")
}
}
trait ConsoleLogger extends Logger{
override def log(msg: String): Unit = {
println(msg)
}
}
class Person(val Name:String){}
class Student(name:String) extends Person(name) with ConsoleLogger{
def learn(): Unit ={
log(Name + " is learning ... ")
}
}
申明叠加的对象使用:
scala> val s1 = new Student("borey") with TimestampLogger with ShortLogger
s1: Student with TimestampLogger with ShortLogger = $anon$1@7164e28a
scala> s1.learn
Tue Oct 17 12:03:54 GMT+08:00 2017 borey is lea...
当做富接口使用的特质
特质可以包含大量工具方法, 而这些工具方法可以依赖一些抽象方法来实现。 例如Scala的Iterator特质就利用抽象的next和hasNext定义了几十个方法。
// 抽象方法和具体方法结合在一起使用
trait Logger{
def log(msg: String)
def info(msg: String) = { log("INFO: " + msg)}
def warn(msg: String) = { log("WARN: " + msg)}
def error(msg: String) = { log("ERROR: " + msg)}
}
// 可以任意使用处理消息的方法 info、warn、error
class A extends Logger{
def show() = {
info("A: hello scala")
}
// ......
override def log(msg: String): Unit = {
println(msg)
}
}
在Scala中像这样在特质中使用具体和抽象方法是很普遍的。在Java中, 你需要声明一个接口和一个额外的扩展该接口的类。
特质中的字段
尝试在特质中定义字段,例如:
trait A_T{
// 特质字段
val AT_Field = 10
// 特质虚拟字段
val msg:String
def show() {println(msg)}
}
class B extends A_T {
// 类字段
val B_Field = 20
// 必须提供特质msg字段
val msg = "hello scala"
}
通过反编译来看下我们定义的类B:
// borey-zhu@AWS-TEST-DT:~/scala$ scalac TraitTest.scala
// borey-zhu@AWS-TEST-DT:~/scala$ ls
// A_T.class B.class TraitTest.scala
// borey-zhu@AWS-TEST-DT:~/scala$ javap -p A_T.class
// Compiled from "TraitTest.scala"
public interface A_T {
public abstract void A_T$_setter_$AT_Field_$eq(int);
public abstract int AT_Field();
public abstract java.lang.String msg();
public static void show$(A_T);
public void show();
public static void $init$(A_T);
}
// borey-zhu@AWS-TEST-DT:~/scala$ javap -p B.class
// Compiled from "TraitTest.scala"
public class B implements A_T {
private final int B_Field;
private final java.lang.String msg;
private final int AT_Field;
public void show();
public int AT_Field();
public void A_T$_setter_$AT_Field_$eq(int);
public int B_Field();
public java.lang.String msg();
public B();
}
// borey-zhu@AWS-TEST-DT:~/scala$
通过上述示例反编译,可以得出:
- 特质中定义字段可以是具体的值也可以是抽象的;
- 特质中字段在子类中不是被继承的,而是简单的进行添加到子类中;
- 特质中未被初始的字段在具体的子类中必须进行重写;
特质构造顺序
和类一样,特质也有构造顺序,由字段的初始化和其他特质中的语句构成。例如:
trait Logger{
println("trait Logger")
def log(msg: String) = {}
def info(msg: String) = { log("INFO: " + msg)}
def warn(msg: String) = { log("WARN: " + msg)}
def error(msg: String) = { log("ERROR: " + msg)}
}
trait TimestampLogger extends Logger{
println("trait TimestampLogger")
override def log(msg: String): Unit = {
super.log(new java.util.Date() + " " + msg)
}
}
trait ConsoleLogger extends Logger{
println("trait ConsoleLogger")
override def log(msg: String): Unit = {
println(msg)
}
}
trait ShortLogger extends Logger{
println("trait ShortLogger")
val maxLen = 15
override def log(msg: String): Unit = {
super.log( if (msg.length < maxLen) msg else msg.substring(0, maxLen -3) + "...")
}
}
class Person(val Name:String){
println("class Person")
}
class Student(name:String) extends Person(name) with Logger{
println("class Student")
def learn(): Unit ={
log(Name + " is learning ... ")
}
}
class Student2(name:String) extends Person(name) with ConsoleLogger with TimestampLogger{
println("class Student")
def learn(): Unit ={
log(Name + " is learning ... ")
}
}
声明一个带叠加特质的Student
对象和一个Student2
对象:
scala> val s1 = new Student("borey") with ConsoleLogger with TimestampLogger with ShortLogger
class Person
trait Logger
class Student
trait ConsoleLogger
trait TimestampLogger
trait ShortLogger
s1: Student with ConsoleLogger with TimestampLogger with ShortLogger = $anon$1@76d7881e
scala> s1.learn
Wed Oct 18 17:35:15 GMT+08:00 2017 borey is lea...
scala> val s2 = new Student2("borey")
class Person
trait Logger
trait ConsoleLogger
trait TimestampLogger
class Student
s2: Student2 = Student2@15a3b42
scala> s2.learn
Wed Oct 18 17:35:41 GMT+08:00 2017 borey is learning ...
根据上述的样例,结果得出:
- 首先调用超类的构造器;
- 特质构造器在超类构造器之后,类构造器之前执行;
- 每个特质当中,父特质构造器先被构造;
- 特质由左向右构造,方法由右向左执行;
- 所有特质构造完成,再进行子类构造;
- 带叠加特质对象的构造在类完成构造之后,再由左向右进行带叠加的特质构造。