从 Java 到 Scala(四):Traits

本文由 Rhyme 发表在 ScalaCool 团队博客。前端

Traits特质,一个咱们既熟悉又陌生的特性。熟悉是由于你会发现它和你平时在Java中使用的interface接口有着很大的类似之处,而陌生又是由于Traits的新玩法会让你打破对原有接口的认知,进入一个更具备挑战性,玩法更高级的领域。因此,在一开始,咱们能够对Traits有一个初步的认识:它是一个增强版的interface。以后,随着你对它了解的深刻,你就会发现相比Java接口,Traits跟类更为类似。再以后,你或许会觉察到,Traits在尝试着将抽象更好地融为一个总体。编程

Traits 入门

在Java中为了不多重继承所带来的昂贵代价(方法或字段冲突、菱形继承等问题),Java的设计者们使用了interface接口。而为了解决Java接口没法进行stackable modifications(即没法使用对象状态进行迭代)、没法提供字段等局限,在Scala中,咱们使用Traits特质而非接口。ide

定义一个trait

trait Animal {
  val typeOf: String = "哺乳动物" // 带有默认值的字段

  def move(): Unit = {  // 带有默认实现的方法
    println("walk")
  }

  def eat() //未实现的抽象方法
}
复制代码

以上代码相似于如下的Java代码学习

public interface Animal {
    String typeOf = "哺乳动物";

    default void move() {
        System.out.println("walk");
    }

    void eat();
}
复制代码

在Scala中使用关键字trait而不interface,和Java接口同样,trait也能够有默认方法的实现。也就是说Java接口有的,trait基本上也都有,并且实现起来要优雅许多。 之因此要说相似于以上的Java代码,缘由在于trait拥有的是字段typeOf,而interface拥有的是静态属性typeOf。这是interfacetrait的一点区别。可是再仔细观察思考这一点区别,更好更灵活的字段设计,是否使得trait更好地组织了抽象,使得它们成为了一个更好的总体。spa

mix in trait

和Java同样,Scala只支持单继承,但却能够有任意数量的特质。在Scala中,咱们不称接口被implements实现了,而是traits被mix in混入了类中。scala

class Bird extends Animal {
  override val typeOf: String = "蛋生动物"

  override def eat(): Unit = {
    println("eat bugs")
  }

  override def move(): Unit = {
    println("fly")
  }
}
复制代码

以上代码中,Bird类混入了特质Animal。当类混入了多个特质时,须要使用with关键字设计

trait Egg

class Bird extends Animal with Egg{
  override val typeOf: String = "蛋生动物"

  override def eat(): Unit = {
    println("eat bugs")
  }

  override def move(): Unit = {
    println("fly")
  }
}
复制代码

在Scala中,咱们将extends with的这种语法解读为一个总体,例如在以上代码中,咱们将extends Animal with Egg看作一个总体,而后被Bird类混入。从这里你是否也可以感觉到 trait在尝试着将抽象更好地融为一个总体。指针

到这里,你或许可以发现,相比Java interface,trait和类更加类似。而事实也确实如此,trait能够具有类的全部特性,除了缺乏构造器参数。这一点trait可使用构造器字段来达到一样的效果。也就是说你不能想给类传入构造器参数那样给特质传入参数。具体代码这里就再也不演示。code

其实在这里咱们能够简单地思考一番,为何要把trait设计得这么像一个class,是设计者们有意为之,仍是无心间的巧合。其实,无论怎么样,我的认为,但从设计层面来说,class类的设计就比trait更加具有一致性,class产生的对象就能够被很好的管理,为何咱们不像管理对象同样来管理咱们的抽象呢?cdn

Traits的两大基本应用

Traits最多见的两种使用方式:一种是和Java接口相似,用于设计富接口,另外一种是Traits独有的stackable modifications。这里就说到了interfacetrait的第二个区别,Traits支持stackable modificatio,使它可以使用对象状态,能够对对象状态进行灵活地迭代。

rich interface

富接口的应用要归功于interface中对默认方法这一特性的支持,一方面松绑了类和接口之间实现与被实现之间的强关系,另外一方面为程序的可扩展性代入了很大的灵活性。trait在这一方面的应用和Java的没有很大的区别。而trait中的默认方法的实现背后采用的也是interface中的default默认方法。

trait Hello {
  def hello(): Unit = {println("hello")
  }
}
复制代码
interface Hello2 {
    default void hello() {...}
}
复制代码

stackable modifications

关于stackable modifications,顾名思义,咱们将modification保存在了一个stack栈中。也就是说咱们能够对运算的结果进行不断的迭代处理,已达到咱们想要的结果。这对于想要分布处理并获得某一结果的需求来讲是很是有用的。

这里咱们借用一下programming in scala中的例子

abstract class IntQueue {
  def get(): Int

  def put(x: Int)
}

import scala.collection.mutable.ArrayBuffer

class BasicIntQueue extends IntQueue {
  private val buf = new ArrayBuffer[Int]

  def get() = buf.remove(0)

  def put(x: Int) {
    buf += x
  }
}

trait Doubling extends IntQueue {
  abstract override def put(x: Int) {
    super.put(2 * x)
  }
}

trait Incrementing extends IntQueue {
  abstract override def put(x: Int) {
    super.put(x + 1)
  }
}

trait Filtering extends IntQueue {
  abstract override def put(x: Int) {
    if (x >= 0) super.put(x)
  }
}

复制代码

在以上代码中咱们定义了一个抽象的队列,有putget方法,在类BasicIntQueue中提供了相应的实现方法。同时又定义了三个特质DoublingIncrementingFiltering,它们都继承了IntQueue抽象类(还记得以前讲过的,trait能够具有类的全部特性),并重写了其中的方法。Doubling将处理结果*2,Incrementing特质将处理结果作了+1处理,Filtering将过滤掉<0的值。

咱们在来看如下的运行结果

scala> val queue = (new BasicIntQueue with Incrementing with Filtering)
queue: BasicIntQueue with Incrementing with Filtering...
scala> queue.put(-1); queue.put(0); queue.put(1)
scala> queue.get()
res15: Int = 1
scala> queue.get()
res16: Int = 2
复制代码
scala> val queue = (new BasicIntQueue with Filtering with Incrementing)
queue: BasicIntQueue with Filtering with Incrementing...
scala> queue.put(-1); queue.put(0); queue.put(1)
scala> queue.get()
res17: Int = 0
scala> queue.get()
res18: Int = 1
scala> queue.get()
res19: Int = 2
复制代码

仔细观察以上的代码,了解了上面的代码,你基本也就了解了stackable modifications

首先,你能够观察到,以上的两段代码总体类似,却获得不一样的运行结果,缘由只是由于特质FilteringIncrementing混入的顺序不一样。咱们仔细查看一下特质中的方法实现,能够发如今特质中都经过super关键字调用了父类的方法。而以上状况的产生缘由就在于此。trait中的super是支持stackable modifications的根本关键。

trait中的super是动态绑定的,而且super调用的是另外一个特质中的方法,具体哪一个特质中的方法被调用须要取决于特质被混入的顺序。对于通常的序列,咱们能够采用"从后往前"的顺序来推断super的调用顺序。

就拿以上的代码而言。

new BasicIntQueue with Incrementing with Filtering
复制代码

代码的super的执行顺序按照从后往前的规则依次是

Filtering -> Incrementing -> BasicIntQueue 
复制代码

举个具体的例子

例如这个时候我执行了put(1)的代码,那么按照上面的执行顺序,

先执行Filteringput方法判断值是否大于1,发现合法,将值1传给Incrementing中的put方法,Incrementing中的put方法将值加1以后传给BasicIntQueue而后将最终的值2放入队列中。

以上代码的执行过程就是stackable modifications的核心。所以到这里,你或许也能理解以上由于混入顺序不一样而出现的不一样结果了吧。

另外,说到动态性,咱们在这里也能够简单地聊几句。在Java中,super的静态性与traitsuper的动态性造成了鲜明的对比。而动态性所带来的种种优点与强大,咱们也已经在这一小节的内容中见识了一二。其实动态性抽离出来是一种设计思想,而它也早已在咱们的身边大展拳脚。例如咱们熟知的IOC依赖注入,AOP面向切面编程,以及前端的动态压缩技术等等,可以列举的还有不少,而它们的背后就是动态性的思想,你越是灵活,可以作的事也就越多。

Traits 探索

Traits构造顺序

trait Test {
    val name:String = "hello" //特质构造器的一部分
    println(name);  // 特质构造器的一部分
}
复制代码

正如你在以上代码中所见的,在特质大括号中包裹的执行语句均属于特质构造器的一部分。

特质构造器的顺序以下:(参考自《快学Scala》)

  1. 首先执行超类的构造器(也就是跟在extends以后的类)
  2. 特质构造器在超类构造器以后、类构造器以前执行
  3. 特质由左到右构造
  4. 父特质先构造
  5. 类构造器

举个例子

class SavingAccount extends Account with FileLogger with ShortLogger

trait ShortLogger extends Logger

trait FileLogger extends Logger
复制代码

以上构造器将按以下顺序执行

  1. Account(超类)
  2. Logger(第一个特质的父特质)
  3. FileLogger(第一个特质)
  4. ShortLogger(从左往右第二个特质,它的父特质Logger已经被构造,再也不重复构造)
  5. SavingAccount(类构造器)

线性化

其实以上构造器顺序实现的背后使用的是一种叫"线性化"的技术。

拿以上的代码做为例子

class SavingAccount extends Account with FileLogger with ShortLogger
复制代码

以上的代码将被线性化解析为

>>的意思是右侧将先被构造

lin(SavingsAccount) = SavingsAccount >> lin(ShortLogger) >> lin(FileLogger) >> lin(Account)

= SavingsAccount >> (ShortLogger >> Logger) >> (FileLogger >> Logger) >> Account

= SavingsAccount >> ShortLogger >> FileLogger >> Logger >> Account
复制代码

仔细观察如下线性化的结果,你会发现,以上的顺序就是构造器执行的顺序。同时,线性化也给出了super的执行顺序,举例来讲,在ShortLogger中调用super将调用右侧的FileLogger中的方法,而FileLogger中的super将调用右侧Logger中的方法,依次类推。

特质字段初始化

所以因为特质构造器的执行时间要早于类构造器的执行,所以在初始化特质中的字段时要额外注意字段的执行时间,避免出现空指针的状况。例如如下代码就会出现错误

trait Hello {
  val name:String
  val out = new PrintStream(name)
}

val test = new Test with Hello {
    val name = "Rhyme" // Error 类构造器晚于特质构造器
}
复制代码

解决方法有提早定义或者懒值

采用提早定义的代码以下所示

val test = new { 
    val name = "Rhyme" //先于全部的构造器执行
}Test with Hello 
复制代码

采用提早定义的方式使得代码不太雅观,咱们还可使用懒值的方式

采用懒值的方式以下

trait Hello {
  val name:String
  lazy val out = new PrintStream(name) // 使用懒值,延迟name的初始化
}
复制代码

懒值在每次使用前都回去检查字段是否已经初始化,存在必定的使用开销。使用前须要仔细考虑

因为篇幅限制,关于trait的探索,咱们就到此为止。但愿本文可以对你学习和了解trait提供一点帮助。在下一章咱们将介绍trait稍微高级一点的用法,自身类型和结构类型。

相关文章
相关标签/搜索