本章要點
- 類可以實現任意數量的特質
- 特質可以要求實現它們的類具備特定的字段、方法或超類
- 和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
構造器執行順序:
- Account (超類)
- Logger (第一個特質的父特質)
- FileLogger
- ShortLogger
- 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字段,該類的構造器會調用初始化方法。
如果特質擴展自某個超類,則伴生類并不繼承這個超類。該超類會被前面提到過的,任何實現該特質的類繼承。