要點以下: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 使人着迷的類設計中介紹的方法, 有興趣的能夠看看.