热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

快学Scala第10章特质

本章要点类可以实现任意数量的特质特质可以要求实现它们的类具备特定的字段、方法或超类和Java接口不同,Scala特质可以提供方法和字段的实现当你将多个特质叠加在一起时,顺序

本章要点

  • 类可以实现任意数量的特质
  • 特质可以要求实现它们的类具备特定的字段、方法或超类
  • 和Java接口不同,Scala特质可以提供方法和字段的实现
  • 当你将多个特质叠加在一起时,顺序很重要—-其方法先被执行的特质排在更后面

为什么没有多重继承

Scala和Java一样不允许类从多个超类继承;从多了超类继承可能会导致许多问题,例如两个超类有相同的方法,子类该如何使用和菱形继承。在java 中类只能扩展自一个超类,它可以实现任意数量的接口,但接口只能包含抽象方法,不能包含字段。
Scala提供了特质(trait)而非接口。特质可以同时拥有抽象方法和具体方法,而类可以实现多个特质。


当做接口使用的特质

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

在这里不需要将方法声明为abstract,在特质中,未被实现的方法默认就是抽象的。
子类的实现,使用extends而不是implements

class ConsoleLogger extends Logger {
    def log(msg: String) { println(msg) }
}

在重写特质的抽象方法时不需要给出override关键字。
对于需要多个特质,可以使用with来添加:

class ConsoleLogger extends Logger with Coneable with Serializable

Scala会将 Logger with with Coneable with Serializable首先看成一个整体,然后再由类来扩展。


带有具体实现的特质

在Scala中,特质中的方法并不需要一定是抽象的。

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


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

在Scala中,我们说ConsoleLogger 的功能被“混入”了SavingsAccount 类。但是这样有一个弊端:当特质改变时,所有混入了该特质的类都必须重新编译。


带有特质的对象

在构造单个对象时,你可以为它添加特质:

trait Logged {
    def log(msg: String) { }
}

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

现在,什么都不会被记录到日志。但是你可以在构造具体对象时“混入”一个更好的日志记录器的实现

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

// 构造SavingsAccount 类对象时加入这个特质
val acct = new SavingsAccount with ConsoleLogger
acct.withdraw(10)   // 此时调用的是 ConsoleLogger 类的log方法

// 另一个对象可以加入不同的特质
val acct2 = new SavingsAccount with FileLogger

叠加在一起的特质

你可以为类或对象添加多个互相调用的特质,从最后一个开始。

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

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

对于特质,super.log并不像类那样用于相同的含义,否则就没有什么意义了,因为Logged的log方法什么也没做。实际上,super.log调用的是特质层级中的下一个特质,具体是哪一个,要根据特质添加的顺序来决定。一般来说,特质是从最后一个开始被处理的。例如:

val acct1 = new SavingsAccount with ConsoleLogger with TimestampLogger with ShortLogger
val acct2 = new SavingsAccount with ConsoleLogger with ShortLogger with TimestampLogger

如果从acct1取款,得到的log信息:
Wed Jun 22 23:41:37 CST 2016 Insufficient…
这里是ShortLogger的log方法先被执行,然后它的super.log调用的是TimestampLogger 的log方法,最后调用ConsoleLogger 的方法将信息打印出来
从acct2提款时,输出的log信息
Wed Jun 22 2…
这里先是TimestampLogger 的log方法被执行,然后它的super.log调用的是ShortLogger的log方法,最后调用ConsoleLogger 的方法将信息打印出来。

但是,你可以指定super.log调用哪个特质的方法,使用方式是:

super[ConsoleLogger].log(...) // 这里给出的类型必须是直接超类型,你无法使用继承层级中更远的特质或类

**注意: **ConsoleLogger的log方法没有调用super.log,因此会在这里停止向上传递msg,而是直接打印:

// 改变继承顺序
val acct1 = new SavingsAccountw with TimestampLogger with ConsoleLogger with ShortLoggerval val acct2 = new SavingsAccountw with ShortLogger  with ConsoleLogger with TimestampLoggerval val acct3 = new SavingsAccountw with ShortLogger with TimestampLogger  with ConsoleLogger

// 输出
Insufficient...    // acct1
Thu Jun 23 00:26:20 CST 2016 Insufficient founds   // acct2
Insufficient founds     // acct3


在特质中重写抽象方法

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

// 扩展这个特质
trait TimestampLogger extends Logger {
    override def log(msg: String) {
        super.log(new java.util.Date() + " " + msg)
    }
}

但是很不幸,这样会报错,因为Logger.log方法还没有实现。但是又和前面一样,我们没有办法知道哪个log方法最终会被调用—这取决于特质被混入的顺序。

Error:(67, 11) method log in trait Logger is accessed from super. It may not be abstract unless it is overridden by a member declared `abstract‘ and `override‘
    super.log(new java.util.Date() + " " + msg)
          ^

Scala认为TimestampLogger 依旧是抽象的,正如错误提示,它需要一个具体的log方法,你必须给方法打上abstract关键字和override关键字,以说明它也是个抽象方法:

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

这样会按照继承层级,一直到一个具体的log方法。


当做富接口使用的特质

在Scala中可以在特质中使用具体和抽两方法:

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)}
}

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

特质中的具体字段

特质中的字段有初始值则就是具体的,否则是抽象的。

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

那么继承该特质的子类是如何获得这个字段的呢。Scala是直接将该字段放入到继承该特制的子类中,而不是被继承。例如:

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

SavingsAccount 对象由所有超类的字段和任何它自己的类中定义的字段构成:
技术分享

在JVM中,一个类只能扩展一个超类,因此来自特质的字段不能以相同的方式继承。


特质中的抽象字段

特质中的抽象字段在具体的子类中必须被重写:

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)  + "...")
  }
}

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

为了在构造对象是灵活使用特质参数值,可以使用带有具体实现的特质:

class SavingsAccount extends Account with Logger { ... }

val acct = new SavingsAccount with ConsoleLogger with ShortLogger {
  val maxLength = 20
}

特质构造顺序

特质也是有构造器的,由字段的初始化和其他特质体中的语句构成:

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

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

这些语句在任何混入了该特质的对象在构造时都会被执行。
构造器的顺序:
- 首先调用超类的构造器
- 特质构造器在超类构造器之后、类构造器之前执行
- 特质由左到右被构造
- 每个特质中,父特质先被构造
- 如果多个特质共有一个父特质,那么那个父特质已经被构造,则不会被再次构造
- 所有特质构造完毕后,子类被构造。
例如:

class SavingsAccount extends Account with FileLogger with ShortLogger

构造器执行顺序:
1. Account (超类)
2. Logger (第一个特质的父特质)
3. FileLogger
4. ShortLogger
5. SavingsAccount


初始化特质中的字段

特质不能有构造器参数,每个特质都有一个无参构造器。这也是特质和类的唯一的技术差别。除此之外,特质可以具备类的所有特性,比如具体的和抽象的字段,以及超类。
例如: 我们要在构造的时候指定log的输出文件:

trait FileLogger extends Logger {
  val filename: String                            // 构造器一部分
  val out = new PrintWriter(filename)     // 构造器的一部分
  def log(msg: String) { out.println(msg); out.flush() }
}

val acct = new SavingsAccount extends Account with FileLogger("myapp.log")  //error,特质没有带参数的构造器

// 你也许会想到和前面重写maxLength一样,在这里重写filename:
val acct = new SavingsAccount with FileLogger {
  val filename = "myapp.log"   // 这样是行不通的
}

上述问题出在构造顺序上。是否还记得在第8章中提到的构造顺序的问题,在这里也是一样的。FileLogger的构造器先于子类构造器执行。这里的子类其实是一个扩展自SavingsAccount 并混入了FileLogger特质的匿名类。而filename的初始化发生在这个匿名类中,而FileLogger的构造器会先执行,因此new PrintWriter(filename)语句会抛出一个异常。
解决方法也是要么使用提前定义或者使用懒值:

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

// 对于类同样:
class SavingsAccount extends {
  val filename = "myapp.log"
} with Account with FileLogger { 
  ...   // SavingsAccount 的实现
}

// 或使用lazy
trait FileLogger extends Logger {
  val filename: String                            // 构造器一部分
  lazy val out = new PrintWriter(filename)     // 构造器的一部分
  def log(msg: String) { out.println(msg); out.flush() }
}

扩展类的特质

特质也可以扩展类,这个类将会自动成为所有混入该特质的超类

trait LoggedException extends Exception with Logged {
  def log() { log(getMessage()) }
}

log方法调用了从Exception超类继承下来的getMessage 方法。那么混入该特质的类:

class UnhappyException extends LoggedException {
  override def getMessage() = "arggh!"
}

在这里LoggedException的超类Exception 也自动成了UnhappyException 的超类,所以可以重写getMessage方法。如图:
技术分享

另一种情况:子类已经扩展了另一个类怎么办,以为只能有一个超类。在Scala中,只要子类已经扩展的类是那个特质的超类的一个子类即可。例如:

class UnhappyException extends IOException with LoggedException  // right
class UnhappyException extends JFrame with LoggedException         // error

自身类型

在上面提到的,当特质扩展类时,编译器能够确保的一件事是所有混入该特质的类都认这个类做超类。Scala还有一套机制可以保证这一点:自身类型。
当特质以如下代码开始定义时:

this: 类型 =>    // 表明只能被混入指定类型的子类

trait LoggedException extends Logged {
  this: Exception =>
    def log() { log(getMessage()) }
}

注意该特质并没有扩展Exception类,而是有一个自身类型Exception。这意味着它只能被混入Exception的子类。
在某种情况下,自身类型比超类型版的特质更灵活,如在特质间的循环依赖时。自身类型也用样可以处理结构类型–只需要给出类必须用有的方法,而不是类名称。

trait LoggedException extends Logged {
  this: { def getMessage(): String } =>
    def log() { lgo(getMessage() ) }  
}

背后的故事

Scala需要将特质翻译称JVM的类和接口。只有抽象方法的特质被简单的变成一个Java接口:

trait Logger {
  def log(msg: String)
}

// 直接被翻译成:
public interface Logger {
  void log(String msg);
} 

如果特质有具体的方法,Scala会自动创建一个伴生类,该伴生类用静态方法存放特质的方法:

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

// 被翻译成:
public interface ConsoleLogger extends Logger {
  void log(String msg);
}
// 伴生类
public class ConsoleLogger$class {
  public static void log(ConsoleLogger self, String name) {
    println(msg)
  }
  ...
}

这些半生类不会有任何字段。特质中的字段对应到接口中的抽象的getter和setter方法。

trait ShortLogger extends Logger {
  val maxLength = 15
}

// 被翻译成:
public interface ShortLogger extends Logger {
  public abstract int maxLength();
  public abstract void weird_prefix$maxLength_$eq(int);  // 以weird开头的setter方法
  ...
}

// 初始化发生在半生类的一个初始化方法内
public class ShortLogged$class {
  public void $init$(ShortLogger self) {
    self.weird_prefix$maxLength_$eq(15)
  }
}

但是当ShortLogger 被混入类的时候,类将会得到一个带有getter和setter的maxLength字段,该类的构造器会调用初始化方法。
如果特质扩展自某个超类,则伴生类并不继承这个超类。该超类会被前面提到过的,任何实现该特质的类继承。

快学Scala第10章----特质


推荐阅读
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • Mac OS 升级到11.2.2 Eclipse打不开了,报错Failed to create the Java Virtual Machine
    本文介绍了在Mac OS升级到11.2.2版本后,使用Eclipse打开时出现报错Failed to create the Java Virtual Machine的问题,并提供了解决方法。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 本文介绍了闭包的定义和运转机制,重点解释了闭包如何能够接触外部函数的作用域中的变量。通过词法作用域的查找规则,闭包可以访问外部函数的作用域。同时还提到了闭包的作用和影响。 ... [详细]
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • 本文介绍了使用AJAX的POST请求实现数据修改功能的方法。通过ajax-post技术,可以实现在输入某个id后,通过ajax技术调用post.jsp修改具有该id记录的姓名的值。文章还提到了AJAX的概念和作用,以及使用async参数和open()方法的注意事项。同时强调了不推荐使用async=false的情况,并解释了JavaScript等待服务器响应的机制。 ... [详细]
  • Java实战之电影在线观看系统的实现
    本文介绍了Java实战之电影在线观看系统的实现过程。首先对项目进行了简述,然后展示了系统的效果图。接着介绍了系统的核心代码,包括后台用户管理控制器、电影管理控制器和前台电影控制器。最后对项目的环境配置和使用的技术进行了说明,包括JSP、Spring、SpringMVC、MyBatis、html、css、JavaScript、JQuery、Ajax、layui和maven等。 ... [详细]
  • 原文地址:https:www.cnblogs.combaoyipSpringBoot_YML.html1.在springboot中,有两种配置文件,一种 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • 推荐系统遇上深度学习(十七)详解推荐系统中的常用评测指标
    原创:石晓文小小挖掘机2018-06-18笔者是一个痴迷于挖掘数据中的价值的学习人,希望在平日的工作学习中,挖掘数据的价值, ... [详细]
author-avatar
中孝雪瑶诗涵
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有