Java & Groovy & Scala & Kotlin - 23.Trait

Overview

Trait 中文名为特质。特质是字段和行为的集合,可以拥有抽象成员也可以拥有普通成员。特质可以看做是一种特殊形式的接口,但是特质主要用于实现多重继承。

多重继承虽然便利,但是会带来 Diamond Problem,即 B 和 C 都实现了 A 的某个方法,而 D 继承了 B 和 C 但是没有重写该方法,此时调用 D 持有的该方法到底来自于 B 还是 C。因此过分使用特质会让程序本身难以理解。

Java 篇

Java 诞生时就倡导语法本身应该简单容易学习,所以设计时 Java 就只支持单继承,因此 Java 并不支持特质,不过可以通过内部类实现类似多重继承的功能。

Groovy 篇

创建特质

特质使用关键字 trait 声明,可以拥有普通成员和抽象成员。

例:

trait MessageHandler {
    //  属性
    int minLenght

    //  方法
    //  普通方法
    void echo(String msg) {
        println(msg)
    }

    //  抽象方法
    abstract void show(String msg)
}

Groovy 中特质本质上是运行时对接口的实现,所以其方法的访问控制符只支持 publicprivate

使用特质

特质就像接口一样使用关键字 implements 进行实现。

例:

class DefaultMessageHandler implements MessageHandler {

    @Override
    void show(String msg) {
        println(msg)
    }
}

def handler = new DefaultMessageHandler()
handler.show("foo")

实现接口与特质

特质可以实现接口。

例:

interface Named {
    String name()
}
trait Greetable implements Named {
    String greeting() { "Hello, ${name()}!" }
}

特质也可以实现其它特质。

例:

trait OutputLogger implements Logger{
    @Override
    void log(String msg) {
        super.log(msg)
    }
}

多重继承时使用 , 作为分隔符,这点和接口一样

例:

class Duck implements FlyingAbility, SpeakingAbility {}

特质实现接口和特质使用的是关键字 implements,而接口实现接口使用的则是关键字 extends

带有特质的对象

使用 withTraits 可以动态通过对象实现特质而不用在类上定义特质。

例:

def logger2 = new BasicLogger().withTraits(OutputLogger)
logger2.log("hello world")

withTraits 中可以包含多个特质,每个特质以 , 分隔。

Diamond Problems

对于 Diamond Problems,Groovy 采用的是最右边定义的赢的规则。即一个类实现了多个包含同名方法的特质时,总是调用最右边的特质定义的方法。

定义特质 A,B 和实现特质的类 C

trait A {
    void echo() {
        println("A")
    }
}

trait B {
    void echo() {
        println("B")
    }
}

class C {}

调用 C

def c1 = new C().withTraits(A, B)
c1.echo()   //  B
def c2 = new C().withTraits(B, A)
c2.echo()   //  A

链式操作

链式操作即实现多个特质时,每个特质可以通过 super 调用上一个特质的实现。特质的链式操作可以说是一个比较复杂的概念,建议使用 Debug 进行跟踪来进行理解。简单来说就是基于 "最右为赢" 的原则,总是调用最右边的特质,在该特质调用 super 后就调用其左边的特质的实现。

以下是一个链式操作的完整的例子

定义一个 Logger 接口

interface Logger {
    void log(String msg)
}

链式操作时最上层定义的必须为接口,而不能是拥有抽象类的特质 trait Logger {abstract void log(String msg}

定义实现该接口的几个特质

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)
    }

}

以上定义了三个特质,其中 OutputLogger 用于执行打印操作,ShortLogger 限制传入的消息长度为15,TimeStampLogger 用于在消息前追加当前时间。

执行链式操作

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 篇

创建特质

Scala 中特质也使用关键字 trait 声明,可以拥有普通成员和抽象成员。

例:

trait MessageHandler {
    //  属性
    val minLength: Int

    //  方法
    //  普通方法
    def echo(msg: String) {
        println(msg)
    }

    //  抽象方法
    def show(msg: String)
}

使用特质

Scala 中只实现一个特质时使用关键字 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")

实现特质

一个特质可以实现其它特质。

例:

trait OutputLogger extends Logger {
    override def log(msg: String): Unit = println(msg)
}

实现多个特质时使用 with 作为分隔符

例:

class Duck extends FlyingAbility with SpeakingAbility {}

带有特质的对象

Scala 中也可以使用关键字 with 来动态通过对象实现特质而不用在类上定义特质。

例:

val logger = new BasicLogger with OutputLogger
logger.log("hello world")

Diamond Problems

对于 Diamond Problems,Scala 采用的方式非常简单,类必须重写造成 Diamond Problems 的方法,如果不重写的会报运行时异常 "Inherits conflicting members"。

特质的构造顺序

Scala 中特质构造顺序遵循以下原则

  • 首先调用超类的构造器
  • 特质构造器在超类构造器之后,类构造器之前执行
  • 特质由左至右被构造
  • 每个特质中,父特质先被构造
  • 如果多个特质有同一个父特质,且父特质已被构造了,则不会被再次构造
  • 所有特质构造完毕,子类才被构造

例:

有如下类

trait Logger
trait FileLogger extends Logger
trait ShortLogger extends Logger
class Account
class SavingAccount extends Account with FileLogger with ShortLogger

则 SavingAccount 的构造顺序为

  1. Account
  2. Logger
  3. FileLogger
  4. ShortLogger
  5. SavingAccount

链式操作

Scala 中虽然没有 Groovy 的最右为赢的原则,但是同样的 Scala 中右边的特质调用 super 后会调用其左边的特质的实现。

以下是一个链式操作的完整的例子

定义一个 Logger 接口

trait Logger {
    def log(msg: String)
}

注意 Groovy 这里使用的是接口,而 Scala 为特质

定义实现该接口的几个特质

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)
    }
}

以上定义了三个特质,其中 OutputLogger 用于执行打印操作,ShortLogger 限制传入的消息长度为15,TimeStampLogger 用于在消息前追加当前时间。

执行链式操作

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 曾经支持过特质,不过后来由于多重继承等特性会把程序搞得过于复杂,而且也不是没有其它解决方案,所以 Kotlin 目前的版本已经将特质废弃了。

思考

什么时候应该使用特质而不是抽象类?

如果你想定义一个类似接口的类型,你可能会在特质和抽象类之间难以取舍。这两种形式都可以让你定义一个类型的一些行为,并要求继承者定义一些其他行为。一些经验法则:

  • 优先使用特质。一个类扩展多个特质是很方便的,但却只能扩展一个抽象类。
  • 如果你需要构造函数参数,使用抽象类。因为抽象类可以定义带参数的构造函数,而特质不行。
  • 如果需要使用的类是从 Java类继承过来的,使用抽象类。
  • 如果需要考虑效率问题,使用抽象类。Java的动态绑定机制决定了直接方法要快于接口方法。而特质最终是被编译成接口。

Summary

  • 特质最终会被编译成接口
  • 只有 Groovy 和 Scala 支持特质
  • 对于 Diamond 问题,Groovy 的原则是最右为赢,Scala 的原则是必须重写

文章源码见 https://github.com/SidneyXu/JGSK 仓库的 _23_trait 小节

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,513评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,312评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,124评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,529评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,937评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,913评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,084评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,816评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,593评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,788评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,267评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,601评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,265评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,158评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,953评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,066评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,852评论 2 277

推荐阅读更多精彩内容