1. 程式人生 > >快學Scala第10章----特質

快學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 物件由所有超類的欄位和任何它自己的類中定義的欄位構成:
這裡寫圖片描述

在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方法。如圖:
這裡寫圖片描述

另一種情況:子類已經擴充套件了另一個類怎麼辦,以為只能有一個超類。在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欄位,該類的構造器會呼叫初始化方法。
如果特質擴充套件自某個超類,則伴生類並不繼承這個超類。該超類會被前面提到過的,任何實現該特質的類繼承。

相關推薦

Scala10----特質

本章要點 類可以實現任意數量的特質 特質可以要求實現它們的類具備特定的欄位、方法或超類 和Java介面不同,Scala特質可以提供方法和欄位的實現 當你將多個特質疊加在一起時,順序很重要—-其方法先被執行的特質排在更後面 為什麼沒有多重繼承 S

scala 特質 讀書筆記及習題答案程式碼

chapter 10 特質 標籤:快學scala 一、筆記 scala和java一樣不允許從多個超類繼承,scala提供特質而非介面。特質可以同時擁有抽象方法和具體方法,而類可以實現多個特質。  不需要將方法宣告為abstract,特質中未

Scala #4答案

數組 第三章 給定 arr val scala 一個 filter array 4.給定一個整數數組,產生一個新的數組,包含原數組中的所有正值,按原有順序排序 之後的元素是所有的零或者負值,按原有順序排序 scala> val arr = Array(1, 2, 3

Scala 習題答案

1.編寫一段程式碼,將a設定為一個n個隨機整數的陣列,要求隨機數介於0(包含)和n(不包含)之間。 val n = 100 //n是自己給定的 val a = scala.util.Random val b = new Array[Int](n) //

scala 讀書筆記及習題答案程式碼

chapter 5 類 標籤:快學scala 一、筆記 scala類方法預設是公有的, classCounter{private val value =0def increment(){ value +=1}def current()= value}val = my

Scala習題答案

5.1 改進5.1節的Counter類,讓它不要在Int.MaxValue時變成負數。 class Counter{ private var value = Int.MaxValue def increment(){ if(va

Scala學習筆記

1、固定長度陣列Array,長度變化陣列ArrayBuffer,在陣列緩衝中尾端新增或移除元素是一個高效的操作。也可以在任意位置插入或移除元素,這樣操作不高效--所有在哪個位置的元素都必須被平移。2、for(...) yield 迴圈建立了一個型別與原始集合的相同的新集合。如

Scala13----集合

本章要點 所有集合都擴充套件自Iterable特質 集合有三大類:序列、集、對映 對於幾乎所有集合類,Scala都同時提供了可變的和不可變的版本 Scala列表要麼是空的,要麼擁有一頭一尾,其中尾部本身又是一個列表 集是無先後次序的集合 用Linkedhas

scala 讀書筆記及習題答案程式碼

chapter 3 陣列相關操作 標籤:快學scala 一、筆記 scala的Array以java陣列方式實現,陣列在JVM中的型別為java.lang.String[]. scala>import scala.collection.mutable.Array

03 scala習題答案

\1. 編寫一段程式碼,將a設定為一個n個隨機整數的陣列,要求隨機數介於0和n之間。 1 2 3 4 5 6 7 8 9 10 11 12 objectApp { def main(args:

Scala 習題答案

1.設定一個對映,其中包含你想要的一些裝備,以及它們的價格。然後構建另一個對映,採用同一組鍵,但在價格上打9折。 val item = Map(("computer"->4500.0),("keyboard"->291.0)) val item

scala6習題——物件相關

1.編寫一個conversion物件,加入inchestoCentimeters,gallonstoliters,milestoKilometers方法 object Conversions{ def inchesToCentimeters()

scala 包和引入 讀書筆記及習題答案程式碼

chapter 7 包和引入 標籤:快學scala 一、筆記 scala中的包名是相對的,原始檔的目錄與包之間沒有強制的關聯關係,完全可以在同一檔案中為多個包貢獻內容。  包可以包含類、物件和特質,但是不能包含函式和變數的定義,這是java虛擬機器的侷限,但是包物件

scala 讀書筆記及習題答案程式碼

chapter 4 元組與對映 標籤:快學scala 一、筆記 預設Map為不可變對映,可變對映定義: scala> val scores = scala.collection.mutable.Map("Allic"->1,"Bob"->3,"Ci

Scala》習題詳解 10 特質

1 java.awt.Rectangle類有兩個很有用的方法translate和grow,但可惜的是像java.awt.geom.Ellipse2D這樣的類沒有。在Scala中,你可以解決掉這個問題。定義一個RenctangleLike特質,加入具體的tran

scala 十一 操作符 讀書筆記及習題答案程式碼

標籤:快學scala 一、筆記 scala種可以在反引號中包含幾乎任何字元序列, val 'val'=42 所有的操作符都是左結合的,除了以冒號(:)結尾的操作符,和賦值操作符。用於構造列表的::操作符是又結合的。1::2::Ni1的意思是1::(2::Ni1),先創建出包含2

scala十六習題——XML處理

本章主要講解對XML的處理,要處理xml需要引入scala-xml-x.x.x.x.jar包,建立普通scala 類不會自動新增此jar包,需要手動引入之後就可以使用了 1.(0)得到什麼,(0)(0)又得到什麼,為什麼? 仍然為<fred/>

Scala 六課 (類構造函數)

ora per 如果 輔助 text log ring nbsp string 類 主構造器: class Person (var name: String){ } 主構造參數可以不帶val或者var,如果沒有被其他方法使用,則不保存為字段。 如果被其他方法

Scala 八課 (嵌套類)

str new 外部 style 接收 rgs sca 外部類 logs 嵌套類: class Human { class Student{ val age = 10 } } object ClassDemo { def main(args: Arr

Scala 九課 (伴生對象和枚舉)

over objectc yellow str imp 擴展類 new 伴生對象 ray Scala沒有靜態方法和靜態字段, 你可以用object這個語法結構來達到同樣的目的。 對象的構造器只有在第一次被使用時才調用。 伴生對象apply方法: 類和它的伴生對象可以互相訪問