要点以下:html
若是只是把绝不相关的类组装在一块儿, 多继承不会出现问题, 不过像下面这个简单例子就能让问题就浮出水面了;java
class Student { val id: Int = 10 } class Teacher { val id: Int = 100 }
假设能够有:数组
class TeacherAssistant extends Student, Teacher { ... }
要求返回id时, 该返回哪个呢?app

对于 class A 中的字段, class D 从 B 和 C 都获得了一份, 这两个字段怎么获得和被构造呢?
C++ 中经过虚拟基类解决它, 这是个脆弱而复杂的解决方法, Java设计者对这些复杂性心生畏惧, 采起了强硬的限制措施, 类只能继承一个超类, 能实现任意数量的接口.ide
一样, Scala 中类只能继承一个超类, 能够扩展任意数量的特质,与Java接口相比, Scala 的特质能够有具体方法和抽象方法; Java 的抽象基类中也有具体方法和抽象方法, 不过若是子类须要多个抽象基类的方法时, Java 就作不到了(无法多继承), Scala 中类能够扩展任意数量的特质.测试
比起Java接口, 特质和类更为类似ui
trait Logger { def log(msg: String) // 抽象方法 } class ConsoleLogger extends Logger with Serializable { // 使用extends def log(msg: String): Unit = { // 不须要override关键字 println("ConsoleLogger: " + msg) } } object LoggerTest extends App{ val logger = new ConsoleLogger logger.log("hi") } /*输出 ConsoleLogger: hi */
trait Logger { def log(msg: String) // 抽象方法 def printAny(k: Any) { // 具体方法 println("具体实现") } }
让特质混有具体行为有一个弊端. 当特质改变时, 全部混入该特质的类都必须从新编译.this
特质继承另外一特质是一种常见的用法, 而特质继承类却不常见.
特质继承类, 这个类会自动成为全部混入该特质的超类spa
trait Logger extends Exception { } class Mylogger extends Logger { } // Exception 自动成为 Mylogger 的超类
若是咱们的类已经继承了另外一个类怎么办?
不要紧只要这个类是特质超类的子类就行了;scala
//IOException 是 Exception 的子类 class Mylogger extends IOException with Logger { }
不过若是咱们的类继承了一个和特质超类不相关的类, 那么这个类就无法混入这个特质了.
在构造单个对象时, 你能够为它添加特质;
特质能够将对象本来没有的方法与字段加入对象中
若是特质和对象改写了同一超类的方法, 则排在右边的先被执行.
// Feline 猫科动物 abstract class Feline { def say() } trait Tiger extends Feline { // 在特质中重写抽象方法, 须要在方法前添加 abstract override 2个关键字 abstract override def say() = println("嗷嗷嗷") def king() = println("I'm king of here") } class Cat extends Feline { override def say() = println("喵喵喵") } object Test extends App { val feline = new Cat with Tiger feline.say // Cat 和 Tiger 都与 say 方法, 调用时从右往左调用, 是 Tiger 在叫 feline.king // 能够看到即便没有 cat 中没有 king 方法, Tiger 特质也能将本身的方法混入 Cat 中 } /*output 嗷嗷嗷 I'm king of here */
能够为类和对象添加多个相互调用的特质
时, 从最后一个开始调用. 这对于须要分阶段加工处理某个值的场景颇有用.
下面展现一个char数组的例子, 展现混入的顺序很重要
定义一个抽象类CharBuffer, 提供两种方法
abstract class CharBuffer { def get: Char def put(c: Char) } class Overlay extends CharBuffer{ val buf = new ArrayBuffer[Char] override def get: Char = { if (buf.length != 0) buf(0) else '@' } override def put(c: Char): Unit = { buf.append(c) } }
定义两种对输入字符进行操做的特质:
由于上面两个特质改变了原始队列类的行为而并不是定义了全新的队列类, 因此这2种特质是可堆叠的,你能够选择它们混入类中,得到所需改动的全新的类。
trait ToUpper extends CharBuffer { // 特质中重写抽象方法 abstract override abstract override def put(c: Char) = super.put(c.toUpper) // abstract override def put(c: Char): Unit = put(c.toUpper) // java.lang.StackOverflowError, 因为put至关于 this.put, 在特质层级中一直调用本身, 死循环 } trait ToLower extends CharBuffer { abstract override def put(c: Char) = super.put(c.toLower) }
特质中 super 的含义和类中 super 含义并不相同, 若是具备相同含义, 这里super.put调用时超类的 put 方法, 它是一个抽象方法, 则会报错, 下面会详细介绍 super.put 的含义
测试
object TestOverlay extends App { val cb1 = new Overlay with ToLower with ToUpper val cb2 = new Overlay with ToUpper with ToLower cb1.put('A') println(cb1.get) cb2.put('a') println(cb2.get) } /*output a A */
上面代码的一些说明:
上面的特质继承了超类charBuffer, 意味着这两个特质只能混入继承了charBuffer的类中
上面每个put
方法都将修改过的消息传递给 super.put
, 对于特质来讲, super.put 调用的是特质层级
的下一个特质(下面说), 具体是哪个根据特质添加的顺序来决定. 通常来讲, 特质从最后一个开始被处理.
在特质中,因为继承的是抽象类,super调用时非法的。这里必须使用abstract override 这两个关键字,在这里表示特质要求它们混入的对象(或者实现它们的类)具有 put 的具体实现, 这种定义仅在特质定义中使用。
混入的顺序很重要,越靠近右侧的特质越先起做用。当你调用带混入的类的方法时,最右侧特质的方法首先被调用。若是那个方法调用了super,它调用其左侧特质的方法,以此类推。
若是要控制具体哪一个特质的方法被调用, 则能够在方括号中给出名称: super[超类].put(...), 这里给出的必须是直接超类型, 没法使用继承层级中更远的特质或者类; 不过在本例中不行, 因为两个特质的超类是抽象类, 没有具体方法, 编译器报错
特质也能够有构造器,由字段的初始化和其余特质体中的语句构成。这些语句在任何混入该特质的对象在构造时都会被执行。
构造器的执行顺序:
若是 C extends c1 with c2 with c3
, 则:
lin(C) = C >> lin(c3) >> lin(c2) >> lin(c1)
这里>>
意思是 "串接并去掉重复项, 右侧胜出"
下面例子中:
class Cat extends Animal with Furry with FourLegged lin(Cat) = Cat>>lin(FourLegged)>>lin(Furry)>>lin(Animal) = Cat>>(FourLegged>>HasLegs)>>(Furry>>Animal)>>(Animal) = Cat>>FourLegged>>HasLegs>>Furry>>Animal
线性化给出了在特质中super被解析的顺序, 举例来讲就是
FourLegged中调用super会执行HasLegs的方法
HasLegs中调用super会执行Furry的方法
Scala 的线性化的主要属性能够用下面的例子演示:假设你有一个类 Cat,继承自超类 Animal 以及两个特质 Furry 和 FourLegged。 FourLegged 又扩展了另外一个特质 HasLegs:
class Animal trait Furry extends Animal trait HasLegs extends Animal trait FourLegged extends HasLegs class Cat extends Animal with Furry with FourLegged
类 Cat 的继承层级和线性化次序展现在下图。继承次序使用传统的 UML 标注指明:白色箭头代表继承,箭头指向超类型。黑色箭头说明线性化次序,箭头指向 super 调用解决的方向。

特质中的字段能够是具体的也能够是抽象的. 若是给出了初始值那么字段就是具体的.
trait Ability { val run = "running" // 具体字段 def log(msg: String) = {} } class Cat extends Ability { val name = "cat" }
1.混入Ability特质的类自动得到一个run字段.
2.一般对于特质中每个具体字段, 使用该特质的类都会得到一个字段与之对应.
3.这些字段不是被继承的, 他们只是简单的加到了子类中.任何经过这种方式被混入的字段都会自动成为该类本身的字段, 这是个细微的区别, 却很重要

JVM中, 一个类只能继承一个超类, 所以来自特质的字段不能以相同的方式继承. 因为这个限制, run
被直接加到Cat类中, 和name
字段排在子类字段中.
特质中未被初始化的字段在具体的子类中必须
被重写
trait Ability { val swim: String // 具体字段 def ability(msg: String) = println(msg + swim) // 方法用了swim字段 } class Cat extends Ability { val swim = "swimming" // 不须要 override }
这种提供特质参数的方式在零时构造某种对象颇有利, 很灵活,按需定制.
特质不能有构造器参数. 每一个特质都有一个无参构造器. 值得一提的是, 缺乏构造器参数是特质与类惟一不相同的技术差异. 除此以外, 特质能够具备类的全部特性, 好比具体的和抽象的字段, 以及超类.
这种局限对于那些须要定制才有用的特质来讲会是一个问题, 这个问题具体就表如今一个带有特质的对象身上. 咱们先来看下面的代码, 而后在分析一下, 就能一目了然了.
/** * Created by wangbin on 2017/7/11. */ trait Fruit { val name: String // 因为是字段, 构造时就输出 val valPrint = println("valPrint: " + name) // lazy 定义法, 因为是lazy字段, 第一次使用时输出 lazy val lazyPrint = println("lazyPrint: " + name) // def 定义法, 方法, 每次调用时输出 def defPrint = println("defPrint: " + name) } object TestFruit extends App { // 方法1. lazy定义法 println("** lazy定义法 构造输出 **") val apple1 = new Fruit { val name = "Apple" } println("\n** lazy定义法 调用输出 **") apple1.lazyPrint apple1.defPrint // 方法2. 提早定义法 println("\n** 提早定义法 构造输出 **") val apple2= new { val name = "Apple" } with Fruit println("\n** 提早定义法 调用输出 **") apple2.lazyPrint apple2.defPrint } /* ** lazy定义法 构造输出 ** valPrint: null ** lazy定义法 调用输出 ** lazyPrint: Apple defPrint: Apple ** 提早定义法 构造输出 ** valPrint: Apple ** 提早定义法 调用输出 ** lazyPrint: Apple defPrint: Apple */
为了便于观察, 先把输出整理成表格
方法 | valPrint | lazyPrint | defPrint |
---|---|---|---|
lazy定义法 | null | Apple | Apple |
提早定义法 | Apple | Apple | Apple |
咱们先来看一下 lazy定义法 和 提早定义法 的构造输出, 即 valPrint, lazy定义法输出为 null, 提早定义法输出为 "Apple"; 问题出在构造顺序上, Fruit 构造器(特质的构造顺序)先与子类构造器执行. 这里的子类并不那么明显, new 语句构造的实际上是一个 Fruit 的匿名子类的实例. 也就是说 Fruit 先初始化, 子类的 name 还没来得及初始化, Fruit 的 valPrint 在构造时就当即求值了, 因此输出为 null.
因为lazy值每次使用都会检查是否已经初始化, 用起来并非那么高效.
关于 val, lazy val, def 的关系能够看看 lazy
特质背后的实现: Scala经过将 trait 翻译成 JVM 的类和接口 , 关于经过反编译的方式查看 Scala 特质的背后工做方式能够参照Scala 使人着迷的类设计中介绍的方法, 有兴趣的能够看看.