快學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欄位,該類的構造器會呼叫初始化方法。
如果特質擴充套件自某個超類,則伴生類並不繼承這個超類。該超類會被前面提到過的,任何實現該特質的類繼承。
相關推薦
快學Scala第10章----特質
本章要點 類可以實現任意數量的特質 特質可以要求實現它們的類具備特定的欄位、方法或超類 和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 迴圈建立了一個型別與原始集合的相同的新集合。如
快學Scala第13章----集合
本章要點 所有集合都擴充套件自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
快學scala第6章習題——物件相關
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方法: 類和它的伴生對象可以互相訪問