要點如下:
1. Scala中類只能繼承一個超類,可以擴展任意數量的特質
2. 與Java接口不同, Scala特質可以提供方法和字段的實現
3. 當將多個特質疊加使用的時候,順序很重要——其方法先被執行的特質 排在更後面。
1. Java 接口和 Scala 特質
1.1 Java 接口
在學習Scala特質之前,我們先來複習一下Java接口。
Java接口是一系列方法的聲明,是一些方法特徵的集合,一個接口只有方法的特徵沒有方法的實現,因此這些方法可以在不同的地方被不同的類實現,而這些實現可以具有不同的行爲(功能)。
Java接口本身沒有任何實現,因爲Java接口不涉及表象,而只描述public行爲,所以Java接口比Java抽象類更抽象化。
Java接口的方法只能是抽象的和公開的,Java接口不能有構造器,Java接口可以有public、靜態的和final屬性。
從java8開始接口裏可以有靜態方式,用static修飾,但是接口裏的靜態方法的修飾符只能是public,且默認是public。java8裏,除了可以在接口裏寫靜態方法,還可以寫非靜態方法,但是必須用default修飾,且只能是public,默認也是public。默認方法可以被繼承。但是要注意,如果繼承了兩個接口裏面的默認方法一樣的話,那麼必須重寫。接口可以被實現,但是無法被實例化。
1.2 Scala特質
Scala Trait(特質) 相當於 Java 的接口,實際上它比接口還功能強大。與接口不同的是,它還可以定義屬性和方法的實現。
Trait定義的方式與類類似,但它使用的關鍵字是 trait
例:
trait A {
val num: Int = 1
}
2. Scala類沒有多繼承
問題:爲什麼Scala不支持多重繼承呢?
接下來我們看一個例題:
class A{
val id: Int = 01
}
class B {
val id: Int = 02
}
假設可以有:
class C extends A, B {
...
}
問題:要求返回id時,該返回哪一個呢?
這就引出了菱形繼承問題:
對於 class A 中的字段, class D 從 B 和 C 都得到了一份,這兩個字段怎麼得到和被構造呢?這樣的情況顯然是不合理的。
如果只是把毫不相關的類組裝在一起,多繼承不會出現問題,但如果這些類具備某些共同的字段或方法,則多繼承就會出現問題,即多重繼承會產生菱形繼承問題。
那麼,如何解決這種問題呢?
在Java中取而代之的是接口,而Scala中則是特質。
一般情況下Scala的類只能夠繼承一個超類,但是如果是Trait的話就可以繼承多個,從結果來看就是實現了多重繼承。
Scala 中類只能繼承一個超類(Java中稱爲父類),可以擴展任意數量的特質,與Java接口相比,Scala 的特質可以有具體方法和抽象方法; Java 的抽象基類中也有具體方法和抽象方法,但Java的接口不能有具體方法。
擴展:什麼時候應該使用特質而不是抽象類?
如果你想定義一個類似接口的類型,你可能會在特質和抽象類之間難以取捨。這兩種形式都可以讓你定義一個類型的一些行爲,並要求繼承者定義一些其他行爲。
一些經驗法則:
Ø 優先使用特質。一個類擴展多個特質是很方便的,但卻只能擴展一個抽象類。
Ø 如果你需要構造函數參數,使用抽象類。因爲抽象類可以定義帶參數的構造函數,而特質不行。
例如,你不能說trait t(i:Int) {},參數i是非法的。
3. 當做接口使用的特質
首先,讓我們從熟悉的內容開始。Scala特質可以完全像Java接口一樣工作。
例子:
trait Logger {
//abstractmethod, but no abstract declare required
def log(msg: String) //沒有實現,這是個抽象方法
}
注意:你不需要將抽象方法聲明爲 abstract,特質中未被實現的方法默認就是抽象方法。
子類可以給出實現:
class ConsoleLogger extends Logger {
//在重寫特質的抽象方法時不需要給出override關鍵字
def log(msg: String){
println(msg)
}
}
4. 帶有具體實現的特質
trait Logger {
def log(msg:String) // 抽象方法
def printAny(k: Any){ // 具體方法
println("具體實現")
}
讓特質混有具體行爲有一個弊端. 當特質改變時,所有混入該特質的類都必須重新編譯。
5. 帶有特質的對象
Scala可以在創建對象時添加特質,這是Java接口所不具備的特性。
特質可以將對象原本沒有的方法與字段加入對象中,如果特質和對象改寫了同一超類的方法,則排在右邊的先被執行。
例:
// Feline 貓科動物
abstract class Feline {
def say()
}
trait Tiger extends Feline {
// 在特質中重寫抽象方法,需要在方法前添加 abstract override 2個關鍵字
abstract overridedef 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 Catwith Tiger //在對象構造時混入特質
feline.say // Cat 和 Tiger 都與 say 方法,調用時從右往左調用,是 Tiger 在叫
feline.king// 可以看到即使沒有 cat 中沒有 king 方法, Tiger 特質也能將自己的方法混入 Cat 中
}
輸出結果:
6. 特質的疊加
就像Java Class可以實現多個接口一樣,Scala Class也可以疊加多個特質。
6.1 with 關鍵字添加額外特質
類可以通過 extends 關鍵字繼承特質,如果需要的特質不止一個,通過 with 關鍵字添加額外特質。
例: Class A extends Bwith C with D {…}
6.2特質的處理順序
一般來說,特質從最後一個開始被處理。這對於需要分階段加工處理某個值的場景很有用.
例:
class A {…}
trait B {…}
trait C {…}
object test extends App{
val a = new A with Bwith C
}
實際上,一個方法調用的是特質層級中的那一個特質,具體是哪一個,取決於特質添加的順序。一般來說,特質從最後一個開始被處理。
在上面這個例子中,特質C中的方法首先會先被執行。
7. 特質中的字段
特質中的字段可以是具體的也可以是抽象的.
7.1 具體字段
如果給出了初始值那麼字段就是具體的.
trait Ability {
val run ="running" // 具體字段
…
}
7.2抽象字段
如果未出了初始值那麼字段就是抽象的。特質中未被初始化的字段在具體的子類中必須被重寫。
trait Ability {
val swim: String //抽象字段
def ability(msg: String)= println(msg + swim) //方法用了swim字段
}
class Cat extends Ability {
val swim ="swimming"
}
object Test extends App{
val f = new Cat withAbility
val fish:String ="fish"
f.ability(fish)
}
運行結果:
這種提供特質參數的方式在臨時構造某種對象很有利,很靈活,按需定製.
8. 特質的構造順序
在scala中除了對象(object)以外,其他的單位,例如類,特質等都有構造器。特質的構造器,由字段的初始化和其他特質體中的語句構成。
例:
trait FileLogger extends Logger{
val out =new PrintWriter(“app.log”)//這是特質構造器的一部分
out.println(“#”+newDate().Tostring) //這也是特質構造器的一部分
val filename: String// 構造器一部分
valout = new PrintWriter(filename) // 構造器的一部分
deflog(msg: String){ out.println(msg);out.flush() }
}
這些語句在任何混入該特質的對象在構造時都會被執行。
在這麼多種的構造器中,在程序執行的過程中,構造器也有其特定的執行過程,具體的執行順序如下:
1. 調用超類的構造器;
2. 特質構造器在超類構造器之後、類構造器之前執行;
3. 特質由左到右被構造;
4. 每個特質當中,父特質先被構造;
5. 如果多個特質共有一個父特質,父特質不會被重複構造
6. 所有特質被構造完畢,子類被構造。
舉例考慮一下下面這個類構造器將按什麼順序執行:
trait Logger{…}
trait ShortLogger extends Logger{…}
trait TimestampLogger extends Logger{…}
class Account{…}
class SavingsAccount extends Account with FileLogger with ShortLogger{…}
構造器將按如下順序執行:
1. Account(超類)。
2. Logger(第一個特質的父特質)
3. FileLogger(第一個特質)
4. ShortLogger(第二個特質)
5. SavingsAccount(類)
注意與疊加特質時,特質被處理的順序區分,一般來說,特質從右向左執行,即從最後一個開始被處理。而特質的構造是從左到右被構造
特質不能有構造器參數. 每個特質都有一個無參構造器. 值得一提的是,缺少構造器參數是特質與類唯一不相同的技術差別. 除此之外,特質可以具有類的所有特性,比如具體的和抽象的字段,以及超類。
特質背後的實現: Scala通過將 trait 翻譯成 JVM 的類和接口,關於通過反編譯的方式查看 Scala 特質的背後工作方式可以參照 Scala 令人着迷的類設計中介紹的方法,有興趣的可以看看.
9.總結Java接口 (interface) 與Scala特質 (trait) :
通過上面對Java接口和Scala特質的學習,我們發現它們之間有很多的相似性,同時也有差別。
9.1相似性:
Java接口和Scala特質都可以包含抽象方法和具體實現(Java8新增了default關鍵字,可以使接口有自己的默認的實現類,而且還不影響接口的實現類。)
Scala和Java一樣都不允許類從多個超類繼承,但分別可以疊加多個特質和實現多個接口;
9.2差異性:
Java只能在Class層面添加接口的實現,Scala可以在Class和對象層面“混入”特質。Scala通過在對象層面動態“混入”特質,相比而言具有更大的靈活性。