scala特质
要点如下:
1. Scala中类只能继承一个超类,可以扩展任意数量的特质
2. 与Java接口不同, Scala特质可以提供方法和字段的实现
3. 当将多个特质叠加使用的时候,顺序很重要——其方法先被执行的特质 排在更后面。
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
例:
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的接口不能有具体方法。
扩展:什么时候应该使用特质而不是抽象类?
如果你想定义一个类似接口的类型,你可能会在特质和抽象类之间难以取舍。这两种形式都可以让你定义一个类型的一些行为,并要求继承者定义一些其他行为。
一些经验法则:
Ø 优先使用特质。一个类扩展多个特质是很方便的,但却只能扩展一个抽象类。
Ø 如果你需要构造函数参数,使用抽象类。因为抽象类可以定义带参数的构造函数,而特质不行。
例如,你不能说trait t(i:Int) {},参数i是非法的。
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. 带有特质的对象
Scala可以在创建对象时添加特质,这是Java接口所不具备的特性。
特质可以将对象原本没有的方法与字段加入对象中,如果特质和对象改写了同一超类的方法,则排在右边的先被执行。
例:
// Feline 猫科动物
abstract class Feline {
def say()
}
trait Tiger extends Feline {
// 在特质中重写抽象方法,需要在方法前添加 abstract override 2个关键字
abstract overridedef say() = println("嗷")
def king() =println("I'm king of here")
}
class Cat extends Feline {
override def say() =println("喵")
}
object Test extends App {
val feline = new Catwith Tiger //在对象构造时混入特质
feline.say // Cat 和 Tiger 都与 say 方法,调用时从右往左调用,是 Tiger 在叫
feline.king// 可以看到即使没有 cat 中没有 king 方法, Tiger 特质也能将自己的方法混入 Cat 中
}
输出结果:
6. 特质的叠加
就像Java Class可以实现多个接口一样,Scala Class也可以叠加多个特质。
6.1 with 关键字添加额外特质
类可以通过 extends 关键字继承特质,如果需要的特质不止一个,通过 with 关键字添加额外特质。
例: Class A extends Bwith C with D {…}
6.2特质的处理顺序
一般来说,特质从最后一个开始被处理。这对于需要分阶段加工处理某个值的场景很有用.
例:
class A {…}
trait B {…}
trait C {…}
object test extends App{
val a = new A with Bwith C
}
实际上,一个方法调用的是特质层级中的那一个特质,具体是哪一个,取决于特质添加的顺序。一般来说,特质从最后一个开始被处理。
在上面这个例子中,特质C中的方法首先会先被执行。
7. 特质中的字段
特质中的字段可以是具体的也可以是抽象的.
7.1 具体字段
如果给出了初始值那么字段就是具体的.
trait Ability {
val run ="running" // 具体字段
…
}
7.2抽象字段
如果未出了初始值那么字段就是抽象的。特质中未被初始化的字段在具体的子类中必须被重写。
trait Ability {
val swim: String //抽象字段
def ability(msg: String)= println(msg + swim) //方法用了swim字段
}
class Cat extends Ability {
val swim ="swimming"
}
object Test extends App{
val f = new Cat withAbility
val fish:String ="fish"
f.ability(fish)
}
运行结果:
这种提供特质参数的方式在临时构造某种对象很有利,很灵活,按需定制.
8. 特质的构造顺序
在scala中除了对象(object)以外,其他的单位,例如类,特质等都有构造器。特质的构造器,由字段的初始化和其他特质体中的语句构成。
例:
trait FileLogger extends Logger{
val out =new PrintWriter(“app.log”)//这是特质构造器的一部分
out.println(“#”+newDate().Tostring) //这也是特质构造器的一部分
val filename: String// 构造器一部分
valout = new PrintWriter(filename) // 构造器的一部分
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 令人着迷的类设计中介绍的方法,有兴趣的可以看看.
9.总结Java接口 (interface) 与Scala特质 (trait) :
通过上面对Java接口和Scala特质的学习,我们发现它们之间有很多的相似性,同时也有差别。
9.1相似性:
Java接口和Scala特质都可以包含抽象方法和具体实现(Java8新增了default关键字,可以使接口有自己的默认的实现类,而且还不影响接口的实现类。)
Scala和Java一样都不允许类从多个超类继承,但分别可以叠加多个特质和实现多个接口;
9.2差异性:
Java只能在Class层面添加接口的实现,Scala可以在Class和对象层面“混入”特质。Scala通过在对象层面动态“混入”特质,相比而言具有更大的灵活性。