Scala之特质

一个类扩展自一个或多个特质,以便使用这些特质提供的服务。特质可能要求使用它的类支持某个特定的特性。不过,与Java的接口不同,scala特质可以给出这些特性的缺省实现,因此,与接口相比,特质要有用的多。
point:
1.类可以实现任意数量的特质。
2.特质可以要求实现它们的类具备特定的字段,方法和超类。
3.和Java接口不同,scala特质可以提供方法和字段的实现。
4.当多个特质叠加在一起时,顺序很重要,其方法先被执行的特质排在更后面。

1.为什么没有多重继承
scala和Java一样不允许类从多个超类继承。如果只是把毫不相干的类组装在一起,多重继承没有什么问题,但如果这些类具有共通的方法或字段就会很麻烦。Java采取了非常强的限制策略,类只能扩展自一个超类,它可以实现任意数量的接口,但接口只能包含抽象方法,不能包含字段。我们通常想调用其他方法来实现其他方法,但在Java接口中,做不到,因此我们在java中经常看到同事提供接口和抽象基类的做法,但治标不治本。Scala提供特质而非接口。特质可以同时拥有抽象方法和具体方法,而类可以实现多个特质。

2.特质当做接口使用
scala特质完全可以像接口那样工作,如:

trait Logger {
  def log(msg: String)  //抽象方法
}

我们不需要将方法声明为abstract ——特质中未被实现的方法默认就是抽象方法。
子类可以给出实现:

class ConsoleLogger extends Logger {   //使用extends而不是implements
	def log(msg: String) {println(msg)}  //不需要写override
}

在重写特质的抽象方法时不需要给出override关键字。
scala并没有一个特殊的关键字来标记对特质的实现,相比于Java的接口,特质跟类更为相像。
如果需要的特质不止一个,可以用with关键字来添加额外的特质:

class ConsoleLogger extends Logger with Cloneable with Serializable {...}

这里使用了Java类库的Cloneable和Serializable接口,所有的接口都可以作为Scala的特质使用。
和Java一样,scala类只能有一个超类,但可以有任意多的特质。

3.带有具体实现的特质
在scala中,特质的方法并不需要一定是抽象的。我们可以把ConsoleLogger变成一个特质:

trait ConsoleLogger {
  def log(msg: String) {println(msg)}
}

ConsoleLgger特质提供了一个带有实现的方法——在本例中,该方法将信息打印在控制台
使用示例:

class SavingAccount extends Account with ConsoleLogger {
  def withdraw(amount: Double) {
    if (amount > balance) log("Insufficient funds")
    else balance -= amount
    ..
  }
}

注意Scala和Java的区别,SavingAccount从ConsoleLogger特质得到了一个具体的log方法实现,用Java接口的话,是没法做到的。
但让特质拥有具体行为存在一个弊端,就是当特质发生改变时,所有混入该特质的类都必须重新编译。

4.带有特质的对象
在构造单个对象时,你可以为它添加特质,我们使用标准scala类库中的Logged特质作为示例,它和我们的Logger很像,但它自带的是一个什么都不做的实现:

trait Logged {
  def log(msg: String)
}

我们在类定义中使用这个特质:

class SavingAccount extends Account with Logged {
  def withdraw(amount: Double) {
    if (amount > balance) log("Insufficient funds")
    else ...
  }
}

现在log方法是不做任何操作的,但我们可以在构造对象的时候 “混入” 一个更好的日志记录器的实现。标准的ConsoleLogger扩展自Logged特质:

trait ConsoleLogger extends Logged {
  override def log(msg: String) {println(msg)}
}

我们可以在构造对象的时候加入这个特质:

val acct = new SavingAccount with ConsoleLgger

当我们在acct对象上调用log方法时,ConsoleLogger特质的log方法会被执行,当然另一个对象可以加入不同的特质`

val acct2 = new SavingAccount with FileLogger

5.叠加在一起的特质
我们可以为类或者对象添加多个互相调用的特质,从最后一个开始。对于需要分阶段加工的某个值的场景很有用。
这是一个给所有日志消息添加时间戳的示例:

trait TimeStampLogger extends Logged {
  override def log(msg: String) {
    super.log(new java.util.Date() + " " + msg)
  }
}

同样的,假如我们想要截断过于冗长的日志消息:

trait ShortLogger extends Logged {
  val maxLength = 15
  override def log(msg: String) {
    super.log{
      if (msg.length <= maxLength) msg else msg.substring(0,maxLength-3) + "..."
    }
  }
}

注意上述的log方法每一个都将修改过的消息传递给super.log。
对特质而言,super.log并不像类那样拥有相同的含义,如果真有相同含义,那这些特质就毫无意义,他们扩展自Logged,log方法啥也不做。
实际上,super.log调用的是特质层级中的下一个特质,具体是哪一个,要根据特质添加的顺序来决定,一般来说,总是从最后一个特质开始处理的。比如:

val acct1 = new SavingAccount with ConsoleLogger with TimeStampLogger with ShortLogger
val acct2 = new SavingAccount with ConsoleLogger with ShortLogger with TimeStampLogger

如果使用acct1,那么得到这么一条信息:
Sun Feb 06 13:33:22 ICT 2020 Insufficient…
可以看到,ShortLogger的log方法是最先被执行的,然后他的super.log调用了TimeStampLogger.
而使用acct2,将会得到:
Sun Feb 06 1…
这样将会首先调用TimeStampLogger,首先添加时间戳在进行长度判断进行截断。

对于特质而言,我们无法从源码判断super.Method会执行哪里的方法,这得需要我们从具体依赖使用这些特质的对象或者类给出的顺序进行判断。如果我们需要控制具体的是哪一个特质的方法被调用,则可以在方括号中给出名称:super[ConsoleLogger].log(…),括号中的类型必须是直接超类型,没办法使用继承层级中更远的特质或类。

6.在特质中重写抽象方法
我们回到自己的Logger特质,这里我们没有提供log方法的具体实现

trait Logger {
  def log(msg: String)  //抽象方法
}

现在,让我们用时间戳特质来扩展它,就像前一节的示例那样

trait TimeStampLogger extends Logger {
  override def log(msg: String) {  //重写抽象方法
    super.log(new java.util.Date() + " " + msg)  //super.log定义了吗?
  }
}

但我们会发现,编译器将会super.log标记为错误
根据正常的继承规则,这个调用永远是错的——Logger.log方法是没有实现的。
就像上面提到的,我们没法知道哪个log方法最终被调用,这取决于特质混入的顺序。
scala认为TimeStampLogger仍然是抽象的,需要被混入一个具体的log方法。我们必须给方法打上abstract关键字和override关键字:

abstract override def log(msg: String) {
  super.log(new java.util.Date() + " " + msg)
}

7.当做富接口使用的特质
特质可以包含大量的工具方法,而这些工具方法可以依赖一些抽象方法来实现,如scala中Interator特质就利用抽象的next和hasNext定义了几十个方法。
我们来丰富下上面的日志特质功能。通常,日志api允许一个日志消息指定一个级别以区分信息类的消息和警告和错误等。

trait Logger {
  def log(msg: String)
  def info(msg: String) { log{"INFO: " + msg} }
  def warn(msg: String) { log{"WARN: " + msg} }
  def severe(msg: String) { log{"SEVERE: " + msg} }
}

注意我们是怎么把抽象方法和具体方法结合在一起的。
这样,使用Logger特质的类可以任意调用这些日志消息方法了,如:

class SavingAccount extends Account with Logger {
  def withdraw(amount: Double) {
     if (amount > balance) severe("Insufficient funds")
     else ...
  }
  ...
  override def log(msg: String) {println(msg)}
}

在scala中像这样在特质中使用具体和抽象方法十分常见,在java中我们就需要声明一个接口和一个额外的扩展该接口的类。

8.特质中的具体字段
特质中的字段可以是具体的也可以是抽象的,如果给出了初始值就是具体的。

trait ShortLogger extends Logger {
  val maxLength = 15 //具体字段
}

混入该特质的类会自动获得一个maxLength字段。通常,对于特质中的每一个具体字段,使用该特质的类都会获得一个字段与之对应。这些字段不是被继承的,只是简单的被到了子类中。这看上去像是一个细微的差别,但这个区别很重要:

class SavingAccount extends Account with ConsoleLogger with ShortLogger {
  var interest = 0.0
  def withdraw(amount: Double) {
    if (amount > balance) log("Insufficient funds")
    else ...
  }
}

注意我们的子类有一个字段interest ,这是子类中一个普通的字段。
假定Account也有一个字段

class Account {
  var balance = 0.0 
}

SavingAccount类按照征程的方式继承了这个字段,SavingAccount对象由超类所有的字段以及任何子类中定义的字段构成。在JVM中,一个类只能扩展一个超类,因此来自特质中的字段不能以相同的方式继承。由于这个限制,maxLength是被直接加到了SavingAccount类中,跟interest排在一起。

9.特质中的抽象字段
特质中未被初始化的字段在具体的子类中必须被重写。
如:

trait ShortLogger extends Logged {
  val maxLength: Int //抽象字段
  override def log(msg: String) {
    super.log(
      if (msg.length <= maxLength) msg else msg.substring(0,maxLength - 3) + "..."
    )  //这个方法中使用到了maxLength字段
  }
}

当在一个具体的类中使用该特质时,必须提供maxLength字段:

class SavingAccount extends Account with ConsoleLogger with ShortLogger {
  val maxLength = 20 //不需要写override
}

这种提供特质参数值的方式在临时构造某种对象时十分方便。

10.特质构造顺序
和类一样,特质也可以有构造器,由字段的初始化和其他特质体的语句构成。
如:

trait FileLogger extends Logger {
  val out = new PrintWriter("app.log") //这是特质构造器的一部分
  out.println("# " + new Date().toString) //也是构造器的一部分

  def log(msg: String) {out.println(msg);out.flush()}
}

这些语句在任何混入该特质的对象在构造时都会被执行。
构造器以如下顺序执行:
1.首先调用超类的构造器。
2.特质构造器在超类构造器之后,类构造器之前执行。
3.特质由左到右被构造。
3.每个特质当中,父特质先被构造。
4.如果多个特质共有一个父特质,而那个父特质已经被构造,则不会再被构造。
5.所有特质被构造完毕后,子类被构造。

举个例子,考虑下面这个类

class SavingsAccount extends Account with FileLogger with ShortLogger

构造器将会按照如下顺序执行:
1.Account——超类
2.Logger——第一个特质的父特质
3.FileLogger——第一个特质
4.ShortLogger——第二个特质,它的父特质Logger已经被构造
5.SavingAccount ——类

11.初始化特质中的字段
特质不能有构造器参数,每个特质都有一个无参数的构造器。
缺少构造器参数是特质与类之间唯一的技术差别,除此之外,特质可以具备所有类的特性,比如具体抽象字段和和超类。这种局限对于那些我们需要定制才有用的特质来说会有一个问题,比如我们想想要指定日志文件,但又不能使用构造参数:

val acct = new SavingAccount with FileLogger("my.log")  //错误,特质没有构造器参数

或者我们有一方案,FileLogger可以有一个抽象字段存放文件名。

trait FileLogger extends Logger {
  val filename: String
  val out = new PrintStream(filename)
  def log(msg: String) {out.println(msg)}
}

使用该特质的类可以重写filename字段,不过这里有一个陷阱,这样是不行的:

val acct = new SavingAccount with FileLogger {
  val filename = "myapp.log"  //这样行不通
}

问题出在构造顺序上,FileLogger构造器先于子类构造器执行,这里的子类并不那么容易分辨:
new 语句构造的其实是一个扩展自SavingAccount并混入了FileLogger特质的匿名类的实例。filename的初始化只发生在这个匿名子类中,实际上,在轮到子类之前初始化根本不会发生——再轮到子类之前,FileLogger构造器就会抛出空指针异常。
解决办法之一就是之前提到的 提前定义,如:

val  acct = new {
  val filename = "myapp.log"
} with SavingAccount with FileLogger

提前定义发生在常规的构造序列之前,在FileLogger被构造时,filename已经是被初始化了的。
在类中做提前定义是这样的:

class SavingAccount extends {   //extends后提前定义块
  val filename = "my.log"
} with Account with FileLogger {
 ...  //SavingAccount的实现
}

另一个解决办法是使用懒值:

trait FileLogger extends Logger {
  val filename: String
  lazy val out = new PrintStrem(filename)
  def log(msg: String) {out.println(msg)} //不需要写override
}

这样out字段在初次使用时才会被初始化,而在那个时候,filename字段应该已经被设好值了,但与与懒值在每次使用前都会检查是否已经初始化,所有用起来效率不是很高。

发布了14 篇原创文章 · 获赞 1 · 访问量 684

猜你喜欢

转载自blog.csdn.net/qq_33891419/article/details/104092581