在Scala中,trait是一种特殊概念。trait可以作为接口,同时也可以定义抽象方法。类使用extends继承trait,在Scala中,无论继承类还是继承trait都用extends关键字。在Scala中, 类继承trait后必须实现其中的抽象方法,实现时不需要使用override关键字,同时Scala支持多重继承trait,使用with关键字即可。
1.1Scala的特质
本节通过对Scala的特质概念、作用以及语法的相关阐述来说明Scala中的特质应用。通过与Java 中的接口相互对比可以加深对特质的了解。
1.Scala的特质定义
由于Scala没有Java中接口的概念,所以Scala的特质就相当于Java中的接口,但是Scala的特质比接口的功能强大。Scala的特质定义如下:
Scala
trait identified{
}
其中trait为定义特质的关键字,identified表示一个合法的标识,可以自定义。登中可以定义一些成员、属性或方法等。
2.Scala的特质作用
Scala的特质可以封装成员和方法。Java中的接口不提供具体的实现,Scala的特质同样也是封装一些成员属性和方法。例如定义一个Scala特质,相关代码如下:
Scala
trait Person{
val name="scala"
def a():Unit
}
Scala的特质相当于抽象类和接口的合体。在JDK1.7中,Java的接口只能定义一些没实现的方法体和一些赋值的变量,而Scala的trait相当于接口和抽象类的合体。
3.Scala的特质语法
在Java中实现多接口可以通过implements关键字定义,例如定义一个类P实现多个接口(A和B 为接口),即class P implements A, B。而在Scala中定义特质A和B时,实现特质的语法为:extends A with B,A和B的位置可以互换。例如 class Pextends A with B表示在一个类中实现多个特质。当类P中混入类S时,S的位置必须在extends之后,特质A或B不可以与类S互换位置,但是A和B的位置可以互换,例如class P extends S with A with B。
1.2 Scala的trait的用法
下面主要介绍Scala的trait的用法:
·只有抽象方法的trait。
·只有抽象成员和方法的trait。
·具体成员的变量和方法。
·对象继承特质。
相关代码如下:
Scala
//定义trait
//1.不是类,不能实例化
//2.它的构造器不能带参数!即:不能添加()
trait Shentihao{
// abstract class Shentihao{
//具体属性
var KM_1 = 5
//抽象属性
var sports:String
//具体方法
def say(): Unit = {}
//抽象方法
def run
}
class Student1 extends Shentihao{
var sports = "跳绳"
def run():Unit={
println("1000m 在4.5分钟内跑完")
}
}
object Test20 {
def main(args: Array[String]): Unit = {
val s1 = new Student1()
}
}
对象继承特质是Scala中比较特殊的一点,可以为单独的某一对象继承trait。相关代码如下:
Scala
//多继承
//美貌
trait Beauty {
val leg:Double
}
//智慧
trait Wisdom {
val EQ:Int
}
//一个类,实现了两特质。用 with 隔开
//多个特质可以交换顺序
class Girl extends Beauty with Wisdom {
val leg = 180
val EQ = 180
override def toString: String = s"leg=${leg},eq=${EQ}"
}
object Test20_1 {
def main(args: Array[String]): Unit = {
val girl = new Girl()
println(girl)
}
}
1.3 trait的mix
下面介绍一个类继承了一个特质后,特质中的成员的处理方式。成员分为抽象成员和具体成员。成员包括方法和属性,没有方法体实现的方法称为抽象方法,没有赋值的属性称为抽象属性;方法体中有具体实现的方法称为具体方法,赋予了一个具体值的属性称为具体属性。
抽象成员包括抽象方法和抽象属性。如果一个类继承了特质,那么抽象方法一定要实现方法体。抽象属性可以通过val或var关键字修饰,如果子类要访问由val或var修饰的抽象成员,要求变量修饰必须要对应,不加 override关键字。
具体成员包括具体方法和具体属性。如果是一个具体的方法,那么需要使用关键字override重写方法。使用override重写方法时,方法的名称、参数列表以及返回值必须相同。 具体属性同样使用val或var关键字修饰,val的重写需加上 override关键字,即override val 属性名称。var的重写不需override关键字和var修饰,只需属性名称即可。
1.4 trait的加载顺序
在Java中构造器的调用顺序为先调用父类构造器再调用子类构造器。Scala 中的调用顺序与Java中的十分相似。trait的加载顺序为先执行超类(父类)中的构造器,再调用子类的构造器。如果混入的trait有父类,会按照继承关系先调用父类。如果有多个父类,则按照从左到右的顺序调用,最后才会调用本类构造器。当有超类调用构造器时按照从左到右、从父类到子类的顺序调用即可。
Scala
//继承多个特质时,加载的顺序
//多个特质加载顺序
//1,先父后子
//2.从左到右
trait A051{
println("1")
}
trait B051{
println("2")
}
class AB extends A051 with B051{
println("AB")
}
object Test20_2 {
def main(args: Array[String]): Unit = {
new AB()
}
}
下面定义多个父类和子类演示构造器的执行顺序。相关代码如下:
Scala
trait A051{ println("A051") }
trait AA051 extends A051 {println("AA051")}
trait AB051 extends A051 {println("AA051")}
trait B051 {println("B051")}
trait BA051 extends B051{println("BA051")}
trait BB051 extends B051 {println("BB051")}
class AB extends AA051 with BA051 with AB051 with BB051{
println("AB")
}
object Test21{
def main(args: Array[String]): Unit = {
new AB()
}
}
//A051 AA051 B051 BA051 AA051 BB051 AB
1.5 解决空指针异常问题
通过前面的介绍,了解了Scala构造器的调用顺序并解释了父类构造器打印为0的问题。 下面介绍调用构造器引起的第二个问题,即空指针异常问题。
1.trait的抽象成员父类使用问题
在新定义一个对象时,该对象会先调用父类的构造器。而在父类构造器中由于变量没有赋值,实际相当于null,再通过变量(实际为null)调用方法时就会报空指针异常的问题。
2.解决方法
解空指针异常的方式有两种,分别是提前定义和懒加载。提前定义就是在调用对象之前给变量赋值,即提前定义法。懒加载就是在调用的过程中通过lazy关键字解决问题。
下面定义一个Logger演示构造器执行顺序,造成空指针问题。相关代码如下:
方法1:使用lazy关键字
Scala
import java.io.PrintWriter
//经典错误:空指针异常
trait FileLogger{
//抽象属性,没有=
val filename:String
println("父类",filename)
//lazy表示,不立刻求值,而是等到这个变量被使用的时候,去求值
lazy val fileout = new PrintWriter(filename)
//用来把 msg 写入到对应文件中
def log(msg: String): Unit ={
fileout.println(msg)
fileout.flush()
}
}
class Test211 extends FileLogger{
val filename = "2024-10-28.txt"
println("子类",filename)
}
object Test21_1 {
def main(args: Array[String]): Unit = {
val t1 = new Test211()
t1.log("test!")
}
}
方法2:提前定义
Scala
import java.io.PrintWriter
//经典错误:空指针异常
trait FileLogger{
//抽象属性,没有=
val filename:String
println("父类",filename)
//lazy表示,不立刻求值,而是等到这个变量被使用的时候,去求值
//lazy val fileout = new PrintWriter(filename) 去掉lazy
val fileout = new PrintWriter(filename)
//用来把 msg 写入到对应文件中
def log(msg: String): Unit ={
fileout.println(msg)
fileout.flush()
}
}
class Test211 extends FileLogger{
val filename = "2024-10-28.txt"
println("子类",filename)
}
object Test21_1 {
def main(args: Array[String]): Unit = {
// val t1 = new Test211()
val t1 = new {val filename="2024-10-29.txt"} with FileLogger
t1.log("test!")
}
}
1.6 trait与类的相关特性
通过之前的介绍我们对trait和类都有了一个初步的了解,下面通过trait和类的相同点以及不同点来说明trait的相关特性。
1.trait与类的相同点
类和trait都以定义成员变量和方法,成员变量和方法可以是抽象的也可以是具体的。如果类是抽象类,则可以定义抽象成员和抽象方法,如果类是普通类,则可以定义一些普通的变量和方法。trait相当于抽象类和接口,所以trait 既可以定义抽象成员也可以定义普通成员。在继承方面它们都可以使用extends关键字。
2.trait与类的不同点
定义类或抽象类时可以有构造参数,而trait构造器不能带参数。关于多继承问题,Java中的类不支持多继承,接口支持多实现:而在Scala中trait可以支持多继承,也可以在多继承的同时混入多个特质。
1.7trait多继承
下面从trait多继承的实现方法和混入多trait的语法格式对多继承做一个简单介绍,通过多继承产生的问题进一步加深对trait名继承的了解。下面简单介绍多继承广生的问题以解决方案。
1.trait多继承的定义
可以通过混入多个trait实现多继承。例如,定义特质t1、t2,在类A中混入多个特质,即class A extends tl with t2表示在类A中混入 t1和t2两个特质。
2.混入多trait的语法
混入多trait的语法如下:
Scala
extends A with B with C
其中多个特质可以互换位置,即可以 extends C with A with B。多个特质互换位置不影响实现多继承。
3.多重继承
多重继承容易产生菱形问题。菱形问题可以描述为B和C继承自A,D继承自B和C,如果A有一个方法被B和C重载,而D不对其重载,那么D应该实现谁的方法,B还是C?解决菱形问题的方法是采用最右优先深度遍历进行搜索。
4.多重继承的惰性求值
惰性求值相当于懒加载问题,即当使用时再去求它的值。当使用子类调用父类的方法出现惰性求值的问题时,只有调用父类中真正的方法时才会对子类中的方法求值。