在这里,你将学习如何使用特质。一个类扩展自一个或多个特质,以便使用这些特质提供的服务。特质可能会要求使用它的类支持某个特定的特性。不过,和Java接口不同,Scala特质可以给出这些特性的缺省实现。
1. Java 接口和 Scala 特质
1.1 Java 接口
在学习Scala特质之前,我们先来复习一下Java接口。
Java接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为(功能)。
Java接口本身没有任何实现,因为Java接口不涉及表象,而只描述public行为,所以Java接口比Java抽象类更抽象化。
Java接口的方法只能是抽象的和公开的,Java接口不能有构造器,Java接口可以有public、静态的和final属性。
从java8开始接口里可以有静态方式,用static修饰,但是接口里的静态方法的修饰符只能是public,且默认是public。java8里,除了可以在接口里写静态方法,还可以写非静态方法,但是必须用default修饰,且只能是public,默认也是public。默认方法可以被继承。但是要注意,如果继承了两个接口里面的默认方法一样的话,那么必须重写。接口可以被实现,但是无法被实例化。
1.2 Scala特质
Scala Trait(特质) 相当于 Java 的接口,实际上它比接口还功能强大。与接口不同的是,它还可以定义属性和方法的实现。
Trait定义的方式与类类似,但它使用的关键字是 trait
Eg:
trait A {
val num: Int = 1
}
2. Scala类没有多继承
问题:为什么Scala不支持多重继承呢?
接下来我们看一个例题:
class A{
val id: Int = 01
}
class B {
val id: Int = 02
}
假设可以有:
class C extends A, B {
...
}
问题:要求返回id时,该返回哪一个呢?
这就引出了菱形继承问题:
对于 class A 中的字段, class D 从 B 和 C 都得到了一份,这两个字段怎么得到和被构造呢?这样的情况显然是不合理的。
如果只是把毫不相关的类组装在一起,多继承不会出现问题,但如果这些类具备某些共同的字段或方法,则多继承就会出现问题,即多重继承会产生菱形继承问题。
那么,如何解决这种问题呢?
在Java中取而代之的是接口,而Scala中则是特质。
一般情况下Scala的类只能够继承一个超类,但是如果是Trait的话就可以继承多个,从结果来看就是实现了多重继承。
Scala 中类只能继承一个超类(Java中称为父类),可以扩展任意数量的特质,与Java接口相比,Scala 的特质可以有具体方法和抽象方法; Java 的抽象基类中也有具体方法和抽象方法,但Java的接口不能有具体方法。
3. 当做接口使用的特质
首先,让我们从熟悉的内容开始。Scala特质可以完全像Java接口一样工作。
例子:
trait Logger {
//abstractmethod, but no abstract declare required
def log(msg: String) //没有实现,这是个抽象方法
}
注意:你不需要将抽象方法声明为 abstract,特质中未被实现的方法默认就是抽象方法。
子类可以给出实现:
class ConsoleLogger extends Logger {
//在重写特质的抽象方法时不需要给出override关键字
def log(msg: String){
println(msg)
}
}
4. 带有具体实现的特质
trait Logger {
def log(msg:String) // 抽象方法
def printAny(k: Any){ // 具体方法
println("具体实现")
}
让特质混有具体行为有一个弊端. 当特质改变时,所有混入该特质的类都必须重新编译。
5. 类可以通过 extends 关键字继承特质,如果需要的特质不止一个,通过 with 关键字添加额外特质
例: Class A extends B with C with D {…}
6. 带有特质的对象
Scala可以在创建对象时添加特质,这是Java接口所不具备的特性。
特质可以将对象原本没有的方法与字段加入对象中,如果特质和对象改写了同一超类的方法,则排在右边的先被执行.
// Feline 猫科动物
abstract class Feline {
def say()
}
trait Tiger extends Feline {
// 在特质中重写抽象方法,需要在方法前添加 abstract override 2个关键字
abstract override def say() = println("嗷")
def king() = println("I'm king of here")
}
class Cat extends Feline {
override def say() = println("喵")
}
object Test extends App {//APP?
val feline = new Cat with Tiger
feline.say // Cat 和 Tiger 都与 say 方法,调用时从右往左调用,是 Tiger 在叫
feline.king // 可以看到即使没有 cat 中没有 king 方法, Tiger 特质也能将自己的方法混入 Cat 中
}
/*output 嗷 I'm king of here*/
7. 特质的叠加
就像Java Class可以实现多个接口一样,Scala Class也可以叠加多个特质,一般来说,特质从最后一个开始被处理。这对于需要分阶段加工处理某个值的场景很有用.
假设有如下的Logger trait定义,分别有3个trait扩展自Logger。
trait Logger {
def log(msg: String){ }
}
//给日志消息添加时间戳
trait TimestampLogger extends Logger {
overridedef log(msg: String) { super.log(new Date() + “ “ + msg)}
}
Trait ShortLogger extends Logger {
val maxLength =15
overridedeflog(msg: String){
super.log(if(msg.length<= maxLength) msg else msg.substring(0,
maxLength-3)+ “…” )
}
}
上述log方法都将修改过的msg传递给super.log.
实际上,super.log调用的是特质层级中的下一个特质,具体是哪一个,取决于特质添加的顺序。一般来说,特质从最后一个开始被处理。
8. 特质中的字段
8.1 具体字段
特质中的字段可以是具体的也可以是抽象的. 如果给出了初始值那么字段就是具体的.
trait Ability {
val run ="running" // 具体字段
…
}
8.2 初始化特质抽象字段
特质中未被初始化的字段在具体的子类中必须被重写。
trait Ability {
val swim: String // 抽象字段
def ability(msg:String) = println(msg + swim) // 方法用了swim字段
}
class Cat extends Ability {
val swim ="swimming"
}
这种提供特质参数的方式在临时构造某种对象很有利,很灵活,按需定制.
9. 特质的构造顺序
和类一样,特质也可以有构造器,由字段的初始化和其他特质体中的语句构成。
trait FileLogger extends Logger{
val out =new PrintWriter(“app.log”)//这是特质构造器的一部分
out.println(“#”+newDate().Tostring) //这也是特质构造器的一部分
deflog(msg: String){ out.println(msg);out.flush() }
}
这些语句在任何混入该特质的对象在构造时都会被执行。
构造器以如下顺序执行:
1. 调用超类的构造器;
2. 特质构造器在超类构造器之后、类构造器之前执行;
3. 特质由左到右被构造;
4. 每个特质当中,父特质先被构造;
5. 如果多个特质共有一个父特质,父特质不会被重复构造
6. 所有特质被构造完毕,子类被构造。
举例考虑一下下面这个类构造器将按什么顺序执行:
trait Logger{}
trait ShortLogger extends logger{}
trait TimestampLogger extends logger{}
class Account{}
class SavingsAccount extends Account with FileLogger with ShortLogger{
…
}
构造器将按如下顺序执行:
1. Account(超类)。
2. Logger(第一个特质的父特质)
3. FileLogger(第一个特质)
4. ShortLogger(第二个特质)
5. SavingsAccount(类)
特质不能有构造器参数. 每个特质都有一个无参构造器. 值得一提的是,缺少构造器参数是特质与类唯一不相同的技术差别. 除此之外,特质可以具有类的所有特性,比如具体的和抽象的字段,以及超类。
特质背后的实现: Scala通过将 trait 翻译成 JVM 的类和接口,关于通过反编译的方式查看 Scala 特质的背后工作方式可以参照 Scala 令人着迷的类设计中介绍的方法,有兴趣的可以看看.
注:本文内容多摘取自快学scala