Overview
Trait 中文名為特質(zhì)消恍。特質(zhì)是字段和行為的集合惰匙,可以擁有抽象成員也可以擁有普通成員。特質(zhì)可以看做是一種特殊形式的接口署恍,但是特質(zhì)主要用于實現(xiàn)多重繼承。
多重繼承雖然便利蜻直,但是會帶來 Diamond Problem盯质,即 B 和 C 都實現(xiàn)了 A 的某個方法,而 D 繼承了 B 和 C 但是沒有重寫該方法概而,此時調(diào)用 D 持有的該方法到底來自于 B 還是 C呼巷。因此過分使用特質(zhì)會讓程序本身難以理解。
Java 篇
Java 誕生時就倡導語法本身應(yīng)該簡單容易學習赎瑰,所以設(shè)計時 Java 就只支持單繼承王悍,因此 Java 并不支持特質(zhì),不過可以通過內(nèi)部類實現(xiàn)類似多重繼承的功能餐曼。
Groovy 篇
創(chuàng)建特質(zhì)
特質(zhì)使用關(guān)鍵字 trait
聲明压储,可以擁有普通成員和抽象成員。
例:
trait MessageHandler {
// 屬性
int minLenght
// 方法
// 普通方法
void echo(String msg) {
println(msg)
}
// 抽象方法
abstract void show(String msg)
}
Groovy 中特質(zhì)本質(zhì)上是運行時對接口的實現(xiàn)源譬,所以其方法的訪問控制符只支持
public
和private
集惋。
使用特質(zhì)
特質(zhì)就像接口一樣使用關(guān)鍵字 implements
進行實現(xiàn)。
例:
class DefaultMessageHandler implements MessageHandler {
@Override
void show(String msg) {
println(msg)
}
}
def handler = new DefaultMessageHandler()
handler.show("foo")
實現(xiàn)接口與特質(zhì)
特質(zhì)可以實現(xiàn)接口踩娘。
例:
interface Named {
String name()
}
trait Greetable implements Named {
String greeting() { "Hello, ${name()}!" }
}
特質(zhì)也可以實現(xiàn)其它特質(zhì)芋膘。
例:
trait OutputLogger implements Logger{
@Override
void log(String msg) {
super.log(msg)
}
}
多重繼承時使用 ,
作為分隔符,這點和接口一樣
例:
class Duck implements FlyingAbility, SpeakingAbility {}
特質(zhì)實現(xiàn)接口和特質(zhì)使用的是關(guān)鍵字
implements
霸饲,而接口實現(xiàn)接口使用的則是關(guān)鍵字extends
为朋。
帶有特質(zhì)的對象
使用 withTraits
可以動態(tài)通過對象實現(xiàn)特質(zhì)而不用在類上定義特質(zhì)。
例:
def logger2 = new BasicLogger().withTraits(OutputLogger)
logger2.log("hello world")
withTraits
中可以包含多個特質(zhì)厚脉,每個特質(zhì)以 ,
分隔习寸。
Diamond Problems
對于 Diamond Problems,Groovy 采用的是最右邊定義的贏的規(guī)則傻工。即一個類實現(xiàn)了多個包含同名方法的特質(zhì)時霞溪,總是調(diào)用最右邊的特質(zhì)定義的方法孵滞。
定義特質(zhì) A,B 和實現(xiàn)特質(zhì)的類 C
trait A {
void echo() {
println("A")
}
}
trait B {
void echo() {
println("B")
}
}
class C {}
調(diào)用 C
def c1 = new C().withTraits(A, B)
c1.echo() // B
def c2 = new C().withTraits(B, A)
c2.echo() // A
鏈式操作
鏈式操作即實現(xiàn)多個特質(zhì)時,每個特質(zhì)可以通過 super
調(diào)用上一個特質(zhì)的實現(xiàn)鸯匹。特質(zhì)的鏈式操作可以說是一個比較復雜的概念坊饶,建議使用 Debug 進行跟蹤來進行理解。簡單來說就是基于 "最右為贏" 的原則殴蓬,總是調(diào)用最右邊的特質(zhì)匿级,在該特質(zhì)調(diào)用 super
后就調(diào)用其左邊的特質(zhì)的實現(xiàn)。
以下是一個鏈式操作的完整的例子
定義一個 Logger 接口
interface Logger {
void log(String msg)
}
鏈式操作時最上層定義的必須為接口染厅,而不能是擁有抽象類的特質(zhì)
trait Logger {abstract void log(String msg}
布蔗。
定義實現(xiàn)該接口的幾個特質(zhì)
trait OutputLogger implements Logger {
@Override
void log(String msg) {
println("--> Seeing msg in OutputLogger.")
println(msg)
}
}
trait ShortLogger implements Logger {
final int maxLength = 15
@Override
void log(String msg) {
println("--> Seeing msg in ShortLogger.")
if (msg.length() <= maxLength)
super.log(msg)
else
super.log(msg.substring(0, maxLength - 3) + "...")
}
}
trait TimeStampLogger implements Logger {
@Override
void log(String msg) {
println("--> Seeing msg in TimeStampLogger.")
super.log(new Date().toString() + " " + msg)
}
}
以上定義了三個特質(zhì)入问,其中 OutputLogger
用于執(zhí)行打印操作,ShortLogger
限制傳入的消息長度為15,TimeStampLogger
用于在消息前追加當前時間骨杂。
執(zhí)行鏈式操作
public class BasicLogger {
}
def loggerX = new BasicLogger().withTraits(OutputLogger, TimeStampLogger, ShortLogger)
loggerX.log("hello world loggerX") // Mon Oct 05 12:01:49 CST 2015 hello world ...
def loggerY = new BasicLogger().withTraits(OutputLogger, ShortLogger, TimeStampLogger)
loggerY.log("hello world loggerY") // Mon Oct 05 1...
以上 loggerX 先進行了截取操作再追加時間虾攻,而 loggerY 先追加時間再進行截取又官。
Scala 篇
創(chuàng)建特質(zhì)
Scala 中特質(zhì)也使用關(guān)鍵字 trait
聲明烫沙,可以擁有普通成員和抽象成員。
例:
trait MessageHandler {
// 屬性
val minLength: Int
// 方法
// 普通方法
def echo(msg: String) {
println(msg)
}
// 抽象方法
def show(msg: String)
}
使用特質(zhì)
Scala 中只實現(xiàn)一個特質(zhì)時使用關(guān)鍵字 extends
魂那。
例:
class DefaultMessageHandler extends MessageHandler {
override val minLength: Int = 100
override def show(msg: String): Unit = {
println(msg)
}
}
val handler = new DefaultMessageHandler
handler.show("foo")
實現(xiàn)特質(zhì)
一個特質(zhì)可以實現(xiàn)其它特質(zhì)蛾号。
例:
trait OutputLogger extends Logger {
override def log(msg: String): Unit = println(msg)
}
實現(xiàn)多個特質(zhì)時使用 with
作為分隔符
例:
class Duck extends FlyingAbility with SpeakingAbility {}
帶有特質(zhì)的對象
Scala 中也可以使用關(guān)鍵字 with
來動態(tài)通過對象實現(xiàn)特質(zhì)而不用在類上定義特質(zhì)。
例:
val logger = new BasicLogger with OutputLogger
logger.log("hello world")
Diamond Problems
對于 Diamond Problems冰寻,Scala 采用的方式非常簡單须教,類必須重寫造成 Diamond Problems 的方法,如果不重寫的會報運行時異常 "Inherits conflicting members"斩芭。
特質(zhì)的構(gòu)造順序
Scala 中特質(zhì)構(gòu)造順序遵循以下原則
- 首先調(diào)用超類的構(gòu)造器
- 特質(zhì)構(gòu)造器在超類構(gòu)造器之后轻腺,類構(gòu)造器之前執(zhí)行
- 特質(zhì)由左至右被構(gòu)造
- 每個特質(zhì)中,父特質(zhì)先被構(gòu)造
- 如果多個特質(zhì)有同一個父特質(zhì)划乖,且父特質(zhì)已被構(gòu)造了贬养,則不會被再次構(gòu)造
- 所有特質(zhì)構(gòu)造完畢,子類才被構(gòu)造
例:
有如下類
trait Logger
trait FileLogger extends Logger
trait ShortLogger extends Logger
class Account
class SavingAccount extends Account with FileLogger with ShortLogger
則 SavingAccount 的構(gòu)造順序為
- Account
- Logger
- FileLogger
- ShortLogger
- SavingAccount
鏈式操作
Scala 中雖然沒有 Groovy 的最右為贏的原則琴庵,但是同樣的 Scala 中右邊的特質(zhì)調(diào)用 super
后會調(diào)用其左邊的特質(zhì)的實現(xiàn)误算。
以下是一個鏈式操作的完整的例子
定義一個 Logger 接口
trait Logger {
def log(msg: String)
}
注意 Groovy 這里使用的是接口,而 Scala 為特質(zhì)
定義實現(xiàn)該接口的幾個特質(zhì)
trait OutputLogger extends Logger {
override def log(msg: String): Unit = println(msg)
}
trait ShortLogger extends Logger {
val maxLength = 15
override def log(msg: String): Unit = {
super.log(
if (msg.length <= maxLength) msg else msg.substring(0, maxLength - 3) + "..."
)
}
}
trait TimeStampLogger extends Logger {
override def log(msg: String): Unit = {
super.log(new Date() + " " + msg)
}
}
以上定義了三個特質(zhì)迷殿,其中 OutputLogger
用于執(zhí)行打印操作儿礼,ShortLogger
限制傳入的消息長度為15,TimeStampLogger
用于在消息前追加當前時間庆寺。
執(zhí)行鏈式操作
class BasicLogger {
}
val loggerX = new BasicLogger with OutputLogger with TimeStampLogger with ShortLogger
loggerX.log("hello world loggerX") // Mon Feb 16 11:46:06 CST 2015 hello world ...
val loggerY = new BasicLogger with OutputLogger with ShortLogger with TimeStampLogger
loggerY.log("hello world loggerY") // Mon Feb 16 1...
以上 loggerX 先進行了截取操作再追加時間蚊夫,而 loggerY 先追加時間再進行截取。
Kotlin 篇
Kotlin 曾經(jīng)支持過特質(zhì)懦尝,不過后來由于多重繼承等特性會把程序搞得過于復雜知纷,而且也不是沒有其它解決方案壤圃,所以 Kotlin 目前的版本已經(jīng)將特質(zhì)廢棄了。
思考
什么時候應(yīng)該使用特質(zhì)而不是抽象類琅轧?
如果你想定義一個類似接口的類型伍绳,你可能會在特質(zhì)和抽象類之間難以取舍。這兩種形式都可以讓你定義一個類型的一些行為乍桂,并要求繼承者定義一些其他行為冲杀。一些經(jīng)驗法則:
- 優(yōu)先使用特質(zhì)。一個類擴展多個特質(zhì)是很方便的模蜡,但卻只能擴展一個抽象類漠趁。
- 如果你需要構(gòu)造函數(shù)參數(shù)扁凛,使用抽象類忍疾。因為抽象類可以定義帶參數(shù)的構(gòu)造函數(shù),而特質(zhì)不行谨朝。
- 如果需要使用的類是從 Java類繼承過來的卤妒,使用抽象類。
- 如果需要考慮效率問題字币,使用抽象類则披。Java的動態(tài)綁定機制決定了直接方法要快于接口方法。而特質(zhì)最終是被編譯成接口洗出。
Summary
- 特質(zhì)最終會被編譯成接口
- 只有 Groovy 和 Scala 支持特質(zhì)
- 對于 Diamond 問題士复,Groovy 的原則是最右為贏,Scala 的原則是必須重寫
文章源碼見 https://github.com/SidneyXu/JGSK 倉庫的 _23_trait
小節(jié)