快學Scala第10章----特質

本章要點

  • 類可以實現任意數量的特質
  • 特質可以要求實現它們的類具備特定的字段、方法或超類
  • 和Java接口不同,Scala特質可以提供方法和字段的實現
  • 當你將多個特質疊加在一起時,順序很重要----其方法先被執行的特質排在更后面

為什么沒有多重繼承

Scala和Java一樣不允許類從多個超類繼承;從多了超類繼承可能會導致許多問題,例如兩個超類有相同的方法,子類該如何使用和菱形繼承。在java 中類只能擴展自一個超類,它可以實現任意數量的接口,但接口只能包含抽象方法,不能包含字段。
Scala提供了特質(trait)而非接口。特質可以同時擁有抽象方法和具體方法,而類可以實現多個特質。


當做接口使用的特質

trait Logger {
    def log(msg: String)  // 這是個抽象方法
}

在這里不需要將方法聲明為abstract,在特質中,未被實現的方法默認就是抽象的。
子類的實現,使用extends而不是implements

class ConsoleLogger extends Logger {
    def log(msg: String) { println(msg) }
}

在重寫特質的抽象方法時不需要給出override關鍵字。
對于需要多個特質,可以使用with來添加:

class ConsoleLogger extends Logger with Coneable with Serializable

Scala會將 Logger with with Coneable with Serializable首先看成一個整體,然后再由類來擴展。


帶有具體實現的特質

在Scala中,特質中的方法并不需要一定是抽象的。

trait ConsoleLogger {
    def log(msg: String)  { println(msg) }
}


class SavingsAccount extends Account with ConsoleLogger {
    def withdraw(amount: Double) {
        if (amount > balance) log("Insufficient funds")
        else banlance -= amount; banlance
    }
}

在Scala中,我們說ConsoleLogger 的功能被“混入”了SavingsAccount 類。但是這樣有一個弊端:當特質改變時,所有混入了該特質的類都必須重新編譯。


帶有特質的對象

在構造單個對象時,你可以為它添加特質:

trait Logged {
    def log(msg: String) { }
}

class SavingsAccount extends Account with Logged {
    def withdraw(amount: Double) {
        if (amount > balance) log("Insufficient funds")
        else ...
    }
    ...
}

現在,什么都不會被記錄到日志。但是你可以在構造具體對象時“混入”一個更好的日志記錄器的實現

trait ConsoleLogger extends Logged {
    override def log(msg: String) { println(msg) }
}

// 構造SavingsAccount 類對象時加入這個特質
val acct = new SavingsAccount with ConsoleLogger
acct.withdraw(10)   // 此時調用的是 ConsoleLogger 類的log方法

// 另一個對象可以加入不同的特質
val acct2 = new SavingsAccount with FileLogger

疊加在一起的特質

你可以為類或對象添加多個互相調用的特質,從最后一個開始。

trait TimestampLogger extends Logged {
    override def log(msg: String) {
        super.log(new java.util.Date() + " " + msg)
    }
}

trait ShortLogger extends Logged {
    override def log(msg: String) {
        super.log( if (msg.length <= maxLength) msg else msg.substring(0, maxLength - 3)  + "...")
    }
}

對于特質,super.log并不像類那樣用于相同的含義,否則就沒有什么意義了,因為Logged的log方法什么也沒做。實際上,super.log調用的是特質層級中的下一個特質,具體是哪一個,要根據特質添加的順序來決定。一般來說,特質是從最后一個開始被處理的。例如:

val acct1 = new SavingsAccount with ConsoleLogger with TimestampLogger with ShortLogger
val acct2 = new SavingsAccount with ConsoleLogger with ShortLogger with TimestampLogger

如果從acct1取款,得到的log信息:
Wed Jun 22 23:41:37 CST 2016 Insufficient...
這里是ShortLogger的log方法先被執行,然后它的super.log調用的是TimestampLogger 的log方法,最后調用ConsoleLogger 的方法將信息打印出來
從acct2提款時,輸出的log信息
Wed Jun 22 2...
這里先是TimestampLogger 的log方法被執行,然后它的super.log調用的是ShortLogger的log方法,最后調用ConsoleLogger 的方法將信息打印出來。

但是,你可以指定super.log調用哪個特質的方法,使用方式是:

super[ConsoleLogger].log(...) // 這里給出的類型必須是直接超類型,你無法使用繼承層級中更遠的特質或類

**注意: **ConsoleLogger的log方法沒有調用super.log,因此會在這里停止向上傳遞msg,而是直接打印:

// 改變繼承順序
val acct1 = new SavingsAccountw with TimestampLogger with ConsoleLogger with ShortLoggerval val acct2 = new SavingsAccountw with ShortLogger  with ConsoleLogger with TimestampLoggerval val acct3 = new SavingsAccountw with ShortLogger with TimestampLogger  with ConsoleLogger

// 輸出
Insufficient...    // acct1
Thu Jun 23 00:26:20 CST 2016 Insufficient founds   // acct2
Insufficient founds     // acct3



在特質中重寫抽象方法

trait Logger {
    def log(msg: String)  // 這是個抽象方法
}

// 擴展這個特質
trait TimestampLogger extends Logger {
    override def log(msg: String) {
        super.log(new java.util.Date() + " " + msg)
    }
}

但是很不幸,這樣會報錯,因為Logger.log方法還沒有實現。但是又和前面一樣,我們沒有辦法知道哪個log方法最終會被調用---這取決于特質被混入的順序。

Error:(67, 11) method log in trait Logger is accessed from super. It may not be abstract unless it is overridden by a member declared `abstract' and `override'
    super.log(new java.util.Date() + " " + msg)
          ^

Scala認為TimestampLogger 依舊是抽象的,正如錯誤提示,它需要一個具體的log方法,你必須給方法打上abstract關鍵字和override關鍵字,以說明它也是個抽象方法:

trait TimestampLogger extends Logger {
   abstract override def log(msg: String) {
        super.log(new java.util.Date() + " " + msg)
    }
}

這樣會按照繼承層級,一直到一個具體的log方法。


當做富接口使用的特質

在Scala中可以在特質中使用具體和抽兩方法:

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 SavingsAccount extends Account with Logger {
    def withdraw(amount: Double) {
        if (amount > balance) severe("Insufficient funds")
        else ...
    }
    override def log(msg: String) { println(msg) }
}

特質中的具體字段

特質中的字段有初始值則就是具體的,否則是抽象的。

trait ShortLogger extends Logged {
  val maxLength = 15   // 具體字段
}

那么繼承該特質的子類是如何獲得這個字段的呢。Scala是直接將該字段放入到繼承該特制的子類中,而不是被繼承。例如:

class SavingsAccount extends Account with ConsoleLogger with ShortLogger {
  var interest = 0.0
  def withdraw(amount: Double) {
    if (amount > balance) log("Insufficient funds")
    else ...
  }
}

SavingsAccount 對象由所有超類的字段和任何它自己的類中定義的字段構成:

| balance | // 超類字段
|-----------|
| interest | // 子類字段
| maxLength |


在JVM中,一個類只能擴展一個超類,因此來自特質的字段不能以相同的方式繼承。


特質中的抽象字段

特質中的抽象字段在具體的子類中必須被重寫:

trait ShortLogger extends Logged {
  val maxLength: Int
  override def log(msg: String) {
    super.log( if (msg.length <= maxLength) msg else msg.substring(0, maxLength - 3)  + "...")
  }
}

class SavingsAccount extends Account with ConsoleLogger with ShortLogger {
  val maxLength = 20   // 不需要寫override
}

為了在構造對象是靈活使用特質參數值,可以使用帶有具體實現的特質:

class SavingsAccount extends Account with Logger { ... }

val acct = new SavingsAccount with ConsoleLogger with ShortLogger {
  val maxLength = 20
}

特質構造順序

特質也是有構造器的,由字段的初始化和其他特質體中的語句構成:

trait FileLogger extends Logger {
  val out = new PrintWriter("app.log")     // 構造器的一部分
  out.println("# " + new Date().toString)  // 也是構造器的一部分

  def log(msg: String) { out.println(msg); out.flush() }
}

這些語句在任何混入了該特質的對象在構造時都會被執行。
構造器的順序:

  • 首先調用超類的構造器
  • 特質構造器在超類構造器之后、類構造器之前執行
  • 特質由左到右被構造
  • 每個特質中,父特質先被構造
  • 如果多個特質共有一個父特質,那么那個父特質已經被構造,則不會被再次構造
  • 所有特質構造完畢后,子類被構造。
    例如:
class SavingsAccount extends Account with FileLogger with ShortLogger

構造器執行順序:

  1. Account (超類)
  2. Logger (第一個特質的父特質)
  3. FileLogger
  4. ShortLogger
  5. SavingsAccount

初始化特質中的字段

特質不能有構造器參數,每個特質都有一個無參構造器。這也是特質和類的唯一的技術差別。除此之外,特質可以具備類的所有特性,比如具體的和抽象的字段,以及超類。
例如: 我們要在構造的時候指定log的輸出文件:

trait FileLogger extends Logger {
  val filename: String                            // 構造器一部分
  val out = new PrintWriter(filename)     // 構造器的一部分
  def log(msg: String) { out.println(msg); out.flush() }
}

val acct = new SavingsAccount extends Account with FileLogger("myapp.log")  //error,特質沒有帶參數的構造器

// 你也許會想到和前面重寫maxLength一樣,在這里重寫filename:
val acct = new SavingsAccount with FileLogger {
  val filename = "myapp.log"   // 這樣是行不通的
}

上述問題出在構造順序上。是否還記得在第8章中提到的構造順序的問題,在這里也是一樣的。FileLogger的構造器先于子類構造器執行。這里的子類其實是一個擴展自SavingsAccount 并混入了FileLogger特質的匿名類。而filename的初始化發生在這個匿名類中,而FileLogger的構造器會先執行,因此new PrintWriter(filename)語句會拋出一個異常。
解決方法也是要么使用提前定義或者使用懶值:

val acct = new {
  val filename = "myapp.log"
} with SavingsAccount with FileLogger

// 對于類同樣:
class SavingsAccount extends {
  val filename = "myapp.log"
} with Account with FileLogger { 
  ...   // SavingsAccount 的實現
}

// 或使用lazy
trait FileLogger extends Logger {
  val filename: String                            // 構造器一部分
  lazy val out = new PrintWriter(filename)     // 構造器的一部分
  def log(msg: String) { out.println(msg); out.flush() }
}

擴展類的特質

特質也可以擴展類,這個類將會自動成為所有混入該特質的超類

trait LoggedException extends Exception with Logged {
  def log() { log(getMessage()) }
}

log方法調用了從Exception超類繼承下來的getMessage 方法。那么混入該特質的類:

class UnhappyException extends LoggedException {
  override def getMessage() = "arggh!"
}

在這里LoggedException的超類Exception 也自動成了UnhappyException 的超類,所以可以重寫getMessage方法。如圖:


trait.png

另一種情況:子類已經擴展了另一個類怎么辦,以為只能有一個超類。在Scala中,只要子類已經擴展的類是那個特質的超類的一個子類即可。例如:

class UnhappyException extends IOException with LoggedException  // right
class UnhappyException extends JFrame with LoggedException         // error

自身類型

在上面提到的,當特質擴展類時,編譯器能夠確保的一件事是所有混入該特質的類都認這個類做超類。Scala還有一套機制可以保證這一點:自身類型。
當特質以如下代碼開始定義時:

this: 類型 =>    // 表明只能被混入指定類型的子類

trait LoggedException extends Logged {
  this: Exception =>
    def log() { log(getMessage()) }
}

注意該特質并沒有擴展Exception類,而是有一個自身類型Exception。這意味著它只能被混入Exception的子類。
在某種情況下,自身類型比超類型版的特質更靈活,如在特質間的循環依賴時。自身類型也用樣可以處理結構類型--只需要給出類必須用有的方法,而不是類名稱。

trait LoggedException extends Logged {
  this: { def getMessage(): String } =>
    def log() { lgo(getMessage() ) }  
}

背后的故事

Scala需要將特質翻譯稱JVM的類和接口。只有抽象方法的特質被簡單的變成一個Java接口:

trait Logger {
  def log(msg: String)
}

// 直接被翻譯成:
public interface Logger {
  void log(String msg);
} 

如果特質有具體的方法,Scala會自動創建一個伴生類,該伴生類用靜態方法存放特質的方法:

trait ConsoleLogger extends Logger {
  def log(msg: String) { println(msg) }
}

// 被翻譯成:
public interface ConsoleLogger extends Logger {
  void log(String msg);
}
// 伴生類
public class ConsoleLogger$class {
  public static void log(ConsoleLogger self, String name) {
    println(msg)
  }
  ...
}

這些半生類不會有任何字段。特質中的字段對應到接口中的抽象的getter和setter方法。

trait ShortLogger extends Logger {
  val maxLength = 15
}

// 被翻譯成:
public interface ShortLogger extends Logger {
  public abstract int maxLength();
  public abstract void weird_prefix$maxLength_$eq(int);  // 以weird開頭的setter方法
  ...
}

// 初始化發生在半生類的一個初始化方法內
public class ShortLogged$class {
  public void $init$(ShortLogger self) {
    self.weird_prefix$maxLength_$eq(15)
  }
}

但是當ShortLogger 被混入類的時候,類將會得到一個帶有getter和setter的maxLength字段,該類的構造器會調用初始化方法。
如果特質擴展自某個超類,則伴生類并不繼承這個超類。該超類會被前面提到過的,任何實現該特質的類繼承。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,565評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,115評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,577評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,514評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,234評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,621評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,641評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,822評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,380評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,128評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,319評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,879評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,548評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,970評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,229評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,048評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,285評論 2 376

推薦閱讀更多精彩內容

  • 讀《快學Scala 》一書的摘要 Scala 運行于JVM之上,擁有海量類庫和工具,兼顧函數式編程和面向對象。 在...
    abel_cao閱讀 1,288評論 0 8
  • 這篇講義只講scala的簡單使用,目的是使各位新來的同事能夠首先看懂程序,因為 scala 有的語法對于之前使用習...
    MrRobot閱讀 2,929評論 0 10
  • Scala學習筆記: 1.1 scala的基礎語法 聲明與定義(賦值):聲明變量時可以指定類型,不指定也可以自動識...
    哎喲喂嘍閱讀 569評論 0 2
  • panda潘達閱讀 800評論 57 48
  • 2016年是我學習的一個飛躍的階段,接觸了好多以前沒有接觸的東西,時間管理,思維導圖,快速閱讀,我變得異常興奮,開...
    陽光小花閱讀 2,599評論 0 0