美文网首页
Java & Groovy & Scala & Kotlin -

Java & Groovy & Scala & Kotlin -

作者: bookislife | 来源:发表于2017-09-30 11:10 被阅读0次

    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 小节

    相关文章

      网友评论

          本文标题:Java & Groovy & Scala & Kotlin -

          本文链接:https://www.haomeiwen.com/subject/egygextx.html