Scala 特質全面解析

Scala 特質全面解析

要點以下:html

  1. Scala中類只能繼承一個超類, 能夠擴展任意數量的特質
  2. 特質能夠要求實現它們的類具有特定的字段, 方法和超類
  3. 與Java接口不一樣, Scala特質能夠提供方法和字段的實現
  4. 當將多個特質疊加使用的時候, 順序很重要

1. Scala類沒有多繼承

若是隻是把絕不相關的類組裝在一塊兒, 多繼承不會出現問題, 不過像下面這個簡單例子就能讓問題就浮出水面了;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 中類能夠擴展任意數量的特質.測試

2. 帶有特質的類

2.1 當作接口使用的特質

  1. Scala能夠徹底像Java接口同樣工做, 你不須要將抽象方法聲明爲 abstract, 特質中未被實現的方法默認就是抽象方法;
  2. 類能夠經過 extends 關鍵字繼承特質, 若是須要的特質不止一個, 經過 with 關鍵字添加額外特質
  3. 重寫特質的抽象方法時, 不須要 override 關鍵字
  4. 全部 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
*/

2.2 帶有具體實現的特質

trait Logger {
  def log(msg: String)  // 抽象方法
  def printAny(k: Any) { // 具體方法
    println("具體實現")
    }
}

讓特質混有具體行爲有一個弊端. 當特質改變時, 全部混入該特質的類都必須從新編譯.this

2.3 繼承類的特質

特質繼承另外一特質是一種常見的用法, 而特質繼承類卻不常見.
特質繼承類, 這個類會自動成爲全部混入該特質的超類spa

trait Logger extends Exception { }

class Mylogger extends Logger { } // Exception 自動成爲 Mylogger 的超類

若是咱們的類已經繼承了另外一個類怎麼辦?
不要緊只要這個類是特質超類的子類就行了;scala

//IOException 是 Exception 的子類
class Mylogger extends IOException with Logger { }

不過若是咱們的類繼承了一個和特質超類不相關的類, 那麼這個類就無法混入這個特質了.

3. 帶有特質的對象

在構造單個對象時, 你能夠爲它添加特質;

特質能夠將對象本來沒有的方法與字段加入對象中
若是特質和對象改寫了同一超類的方法, 則排在右邊的先被執行.

// 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
*/

4. 特質的疊加

能夠爲類和對象添加多個相互調用的特質時, 從最後一個開始調用. 這對於須要分階段加工處理某個值的場景頗有用.

下面展現一個char數組的例子, 展現混入的順序很重要

定義一個抽象類CharBuffer, 提供兩種方法

  • put 在數組中加入字符
  • get 從數組頭部取出字符
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)
  }
}

定義兩種對輸入字符進行操做的特質:

  • ToUpper 將輸入字符變爲大寫
  • ToLower 將輸入字符變爲小寫

由於上面兩個特質改變了原始隊列類的行爲而並不是定義了全新的隊列類, 因此這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
*/

上面代碼的一些說明:

  1. 上面的特質繼承了超類charBuffer, 意味着這兩個特質只能混入繼承了charBuffer的類中

  2. 上面每個put方法都將修改過的消息傳遞給 super.put, 對於特質來講, super.put 調用的是特質層級的下一個特質(下面說), 具體是哪個根據特質添加的順序來決定. 通常來講, 特質從最後一個開始被處理.

  3. 在特質中,因爲繼承的是抽象類,super調用時非法的。這裏必須使用abstract override 這兩個關鍵字,在這裏表示特質要求它們混入的對象(或者實現它們的類)具有 put 的具體實現, 這種定義僅在特質定義中使用。

  4. 混入的順序很重要,越靠近右側的特質越先起做用。當你調用帶混入的類的方法時,最右側特質的方法首先被調用。若是那個方法調用了super,它調用其左側特質的方法,以此類推。

若是要控制具體哪一個特質的方法被調用, 則能夠在方括號中給出名稱: super[超類].put(...), 這裏給出的必須是直接超類型, 沒法使用繼承層級中更遠的特質或者類; 不過在本例中不行, 因爲兩個特質的超類是抽象類, 沒有具體方法, 編譯器報錯

5. 特質的構造順序

特質也能夠有構造器,由字段的初始化和其餘特質體中的語句構成。這些語句在任何混入該特質的對象在構造時都會被執行。
構造器的執行順序:

  1. 調用超類的構造器;
  2. 特質構造器在超類構造器以後、類構造器以前執行;
  3. 特質由左到右被構造;
  4. 每一個特質當中,父特質先被構造;
  5. 若是多個特質共有一個父特質,父特質不會被重複構造
  6. 全部特質被構造完畢,子類被構造。

線性化細節

  1. 線性化是描述某個類型的全部超類型的一種技術規格;
  2. 構造器的順序是線性化順序的反向;
  3. 線性化給出了在特質中super被解析的順序,

若是 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 調用解決的方向。

6. 特質中的字段

特質中的字段能夠是具體的也能夠是抽象的. 若是給出了初始值那麼字段就是具體的.

6.1 具體字段

trait  Ability {
  val run = "running" // 具體字段
  def log(msg: String) = {}
}

class Cat extends Ability {
    val name = "cat"
    }

1.混入Ability特質的類自動得到一個run字段.
2.一般對於特質中每個具體字段, 使用該特質的類都會得到一個字段與之對應.
3.這些字段不是被繼承的, 他們只是簡單的加到了子類中.任何經過這種方式被混入的字段都會自動成爲該類本身的字段, 這是個細微的區別, 卻很重要
scalatrait

JVM中, 一個類只能繼承一個超類, 所以來自特質的字段不能以相同的方式繼承. 因爲這個限制, run 被直接加到Cat類中, 和name字段排在子類字段中.

6.2 初始化特質抽象字段

在類中初始化

特質中未被初始化的字段在具體的子類中必須被重寫

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

相關文章
相關標籤/搜索