本文由 Rhyme 发表在 ScalaCool 团队博客。前端
Traits特质,一个咱们既熟悉又陌生的特性。熟悉是由于你会发现它和你平时在Java中使用的interface接口有着很大的类似之处,而陌生又是由于Traits的新玩法会让你打破对原有接口的认知,进入一个更具备挑战性,玩法更高级的领域。因此,在一开始,咱们能够对Traits
有一个初步的认识:它是一个增强版的interface
。以后,随着你对它了解的深刻,你就会发现相比Java接口,Traits跟类更为类似。再以后,你或许会觉察到,Traits在尝试着将抽象更好地融为一个总体。编程
在Java中为了不多重继承所带来的昂贵代价(方法或字段冲突、菱形继承等问题),Java的设计者们使用了interface接口。而为了解决Java接口没法进行stackable modifications
(即没法使用对象状态进行迭代)、没法提供字段等局限,在Scala中,咱们使用Traits
特质而非接口。ide
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
。这是interface
和trait
的一点区别。可是再仔细观察思考这一点区别,更好更灵活的字段设计,是否使得trait
更好地组织了抽象,使得它们成为了一个更好的总体。spa
和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
最多见的两种使用方式:一种是和Java接口相似,用于设计富接口,另外一种是Traits独有的stackable modifications
。这里就说到了interface
和trait
的第二个区别,Traits支持stackable modificatio
,使它可以使用对象状态,能够对对象状态进行灵活地迭代。
富接口的应用要归功于interface
中对默认方法这一特性的支持,一方面松绑了类和接口之间实现与被实现之间的强关系,另外一方面为程序的可扩展性代入了很大的灵活性。trait
在这一方面的应用和Java的没有很大的区别。而trait
中的默认方法的实现背后采用的也是interface
中的default
默认方法。
trait Hello {
def hello(): Unit = {println("hello")
}
}
复制代码
interface Hello2 {
default void hello() {...}
}
复制代码
关于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)
}
}
复制代码
在以上代码中咱们定义了一个抽象的队列,有put
和get
方法,在类BasicIntQueue中提供了相应的实现方法。同时又定义了三个特质Doubling
、Incrementing
、Filtering
,它们都继承了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
。
首先,你能够观察到,以上的两段代码总体类似,却获得不一样的运行结果,缘由只是由于特质Filtering
和Incrementing
混入的顺序不一样。咱们仔细查看一下特质中的方法实现,能够发如今特质中都经过super
关键字调用了父类的方法。而以上状况的产生缘由就在于此。trait
中的super
是支持stackable modifications
的根本关键。
在trait
中的super
是动态绑定的,而且super
调用的是另外一个特质中的方法,具体哪一个特质中的方法被调用须要取决于特质被混入的顺序。对于通常的序列,咱们能够采用"从后往前"的顺序来推断super
的调用顺序。
就拿以上的代码而言。
new BasicIntQueue with Incrementing with Filtering
复制代码
代码的super的执行顺序按照从后往前的规则依次是
Filtering -> Incrementing -> BasicIntQueue
复制代码
举个具体的例子
例如这个时候我执行了put(1)
的代码,那么按照上面的执行顺序,
先执行Filtering
的put
方法判断值是否大于1,发现合法,将值1传给Incrementing
中的put
方法,Incrementing
中的put
方法将值加1以后传给BasicIntQueue
而后将最终的值2放入队列中。
以上代码的执行过程就是stackable modifications
的核心。所以到这里,你或许也能理解以上由于混入顺序不一样而出现的不一样结果了吧。
另外,说到动态性,咱们在这里也能够简单地聊几句。在Java中,super
的静态性与trait
中super
的动态性造成了鲜明的对比。而动态性所带来的种种优点与强大,咱们也已经在这一小节的内容中见识了一二。其实动态性抽离出来是一种设计思想,而它也早已在咱们的身边大展拳脚。例如咱们熟知的IOC依赖注入,AOP面向切面编程,以及前端的动态压缩技术等等,可以列举的还有不少,而它们的背后就是动态性的思想,你越是灵活,可以作的事也就越多。
trait Test {
val name:String = "hello" //特质构造器的一部分
println(name); // 特质构造器的一部分
}
复制代码
正如你在以上代码中所见的,在特质大括号中包裹的执行语句均属于特质构造器的一部分。
特质构造器的顺序以下:(参考自《快学Scala》)
extends
以后的类)举个例子
class SavingAccount extends Account with FileLogger with ShortLogger
trait ShortLogger extends Logger
trait FileLogger extends Logger
复制代码
以上构造器将按以下顺序执行
Account
(超类)Logger
(第一个特质的父特质)FileLogger
(第一个特质)ShortLogger
(从左往右第二个特质,它的父特质Logger
已经被构造,再也不重复构造)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
稍微高级一点的用法,自身类型和结构类型。