5 继承、多态与 Trait
笔记衔接前序《Scala 自适应类型与函数》,严格匹配课程体系,针对有 Java 基础的大数据专业大学生设计,全程Java 语法对比 + 课程配套源码拆解 + Spark 大数据场景应用 + 避坑指南 + 作业全解析。Scala 的继承、多态与 Trait 是 Spark/Flink 框架源码的核心设计思想,Spark 中几乎所有核心组件(RDD、SparkSession、Streaming 等)均基于 Trait 与继承体系实现,本节内容是后续大数据框架学习的核心基础。
前置知识:Scala 面向对象基础、类与伴生对象、函数式编程基础
开发环境:JDK1.8+、Scala2.12.x、IDEA + Scala 插件
核心学习目标:
- 掌握 Scala 继承的核心规则,重点掌握父类构造器的调用机制,明确与 Java 的核心差异
- 熟练掌握 Scala 方法重写与多态的实现,理解 Scala 重写的强制规则
- 掌握 Scala 抽象类的定义与用法,明确与 Java 抽象类的差异
- 深入理解 Scala Trait 特质的核心特性,掌握 Trait 的混入、多继承、构造顺序等核心知识点
- 明确 Trait 与抽象类、Java 接口的适用场景,独立完成课程配套作业
第一章 Scala 继承与多态
Scala 的继承与多态核心思想与 Java 一致,但在构造器调用、方法重写规则上有严格的强制限制,是新手高频踩坑点,必须重点掌握。
1.1 继承的核心定义与语法
1.1.1 核心定义
继承是在原有类(父类 / 超类)的基础上定义新类(子类 / 派生类),子类可复用父类的属性与方法,同时可扩展自身的特性,最终实现代码复用与多态。
- Scala 中使用
extends关键字实现继承,与 Java 一致; - Scala 与 Java 相同,类只能单继承一个父类,不可多继承类,多继承能力需通过 Trait 实现;
- 所有类默认继承
AnyRef类(对应 Java 的Object类),是 Scala 所有引用类型的顶级父类。
1.1.2 基础语法
// 父类定义
class 父类名(参数列表) {
// 父类属性与方法
}
// 子类继承父类
class 子类名(子类参数列表) extends 父类名(父类构造参数) {
// 子类扩展的属性与方法
}1.2 父类构造器的调用规则(重点难点)
Scala 的构造器体系与 Java 完全不同,子类继承父类时,必须明确调用父类的构造器,且有严格的调用规则,这是与 Java 最核心的差异,也是课程重点难点。
1.2.1 核心调用规则
- 子类主构造器必须直接调用父类的构造器(主构造器或辅助构造器),调用逻辑写在
extends后的父类名参数列表中; - 子类的辅助构造器,绝对不能直接调用父类的构造器,只能调用本类已定义的主构造器 / 其他辅助构造器(最终仍会调用父类构造器);
- 父类的辅助构造器,只能被子类的主构造器调用,子类辅助构造器无法直接调用。
与 Java 的核心差异:Java 中,子类的所有构造方法(无论无参 / 有参)都可以通过
super()直接调用父类的任意构造方法,无此限制。
1.2.2 课程案例实战
对应配套源码RunInheritPerson.scala,实现 Person 父类与 Employee 子类的构造器调用:
// 父类:Person
class InheritPerson(val name: String, var age: Int) {
// 父类辅助构造器:无参
def this() = {
this("java", 30) // 辅助构造器必须第一行调用主构造器
}
println(s"父类构造器执行=>name=$name,age=$age")
}
// 子类:Employee,继承Person
// 方式1:子类主构造器调用父类主构造器(推荐)
class Employee(name: String, age: Int, var addr: String) extends InheritPerson(name, age) {
println(s"子类构造器执行=>name=$name,age=$age,addr=$addr")
}
// 方式2:子类主构造器调用父类辅助构造器(无参)
class Employee2(name: String, age: Int, var addr: String) extends InheritPerson() {
println(s"子类构造器执行=>name=$name,age=$age,addr=$addr")
}
// 测试执行
object RunInheritPerson {
def main(args: Array[String]): Unit = {
// 调用方式1:子类调用父类主构造器
new Employee("scala", 5, "amc")
println("------------------------")
// 调用方式2:子类调用父类辅助构造器
new Employee2("python", 10, "beijing")
}
}执行结果:
父类构造器执行=>name=scala,age=5
子类构造器执行=>name=scala,age=5,addr=amc
------------------------
父类构造器执行=>name=java,age=30
子类构造器执行=>name=python,age=10,addr=beijing执行顺序说明:创建子类对象时,先执行父类构造器,再执行子类构造器,与 Java 一致。
1.3 方法重写与多态
1.3.1 方法重写的强制规则
当子类继承的父类方法无法满足业务需求时,子类可对该方法进行重写,实现自身的业务逻辑,Scala 对方法重写有严格的强制规则:
必须使用
override关键字修饰重写的方法,否则编译报错;与 Java 的核心差异:Java 中重写方法的
@Override注解是可选的,可省略,Scala 中override关键字是强制的,不可省略。重写的方法名、参数列表、返回值类型必须与父类完全一致;
父类中用
final修饰的方法,子类不可重写;父类中用
private修饰的方法,子类不可访问,也无法重写。
1.3.2 多态的实现
多态指程序运行期间,根据对象的实际类型,调用对应类型的重写方法,而非编译时的引用类型。
- 多态的前提:继承关系 + 方法重写 + 父类引用指向子类对象;
- 多态的作用:提高程序的可扩展性与可维护性,实现面向接口编程。
1.3.3 课程案例实战
对应配套源码RunInheritPerson.scala,实现 Animal 父类与 Dog/Cat 子类的方法重写与多态:
// 父类:动物
class InheritAnimal {
// 父类方法:叫声
def speak = {
println("animal speak ")
}
// 父类方法:奔跑
def run: Unit = {
println("run by leg ")
}
}
// 子类:狗,重写speak方法,继承run方法
class InheritDog extends InheritAnimal {
// 强制使用override关键字
override def speak = {
println("dog wangwang speak ")
}
}
// 子类:猫,重写speak方法,继承run方法
class InheritCat extends InheritAnimal {
override def speak = {
println("cat miaomiao speak ")
}
}
// 测试多态
object RunInheritPerson {
def main(args: Array[String]): Unit = {
println("-------Dog-----------")
// 父类引用指向子类对象,多态
val d: InheritAnimal = new InheritDog()
d.run // 调用继承的父类方法
d.speak // 运行时调用子类重写的方法
println("-------cat-----------")
val c: InheritAnimal = new InheritCat()
c.run
c.speak
}
}执行结果:
-------Dog-----------
run by leg
dog wangwang speak
-------cat-----------
run by leg
cat miaomiao speak第二章 Scala 类层级结构
Scala 中所有类都有共同的顶级父类Any,形成了完整的类层级体系,这是 Scala 类型系统的核心基础。
2.1 完整类层级结构

Any(所有类型的顶级父类)
├─ AnyVal(值类型,对应Java基本类型)
│ ├─ Byte、Short、Int、Long、Float、Double
│ ├─ Char、Boolean
│ └─ Unit(对应Java的void,唯一实例是())
└─ AnyRef(引用类型,对应Java的Object类)
├─ Java中的所有类(java.lang.String、java.util.List等)
├─ Scala中的所有普通类、抽象类、Trait
│ ├─ String、List、Seq、Iterable等
│ └─ 用户自定义的所有类
├─ Null(所有引用类型的子类,唯一实例是null)
└─ Nothing(所有类型的子类,无实例)2.2 核心类型说明
- Any:Scala 中所有类型的顶级父类,定义了
equals()、hashCode()、toString()等通用方法,所有类型都可赋值给 Any 类型的变量。 - AnyVal:所有值类型的父类,共 9 个值类型,均为不可变类型,对应 Java 的 8 种基本类型 + Unit。
- AnyRef:所有引用类型的父类,对应 Java 的
java.lang.Object,Scala 中所有的类、Trait、数组、字符串都继承自 AnyRef。 - Null:所有引用类型的子类,唯一实例是
null,仅能赋值给引用类型,不能赋值给值类型。 - Nothing:所有类型的子类(包括值类型和引用类型),无任何实例,用于标记程序非正常终止的场景(如抛出异常),辅助编译器进行类型推断。
第三章 Scala 抽象类
Scala 的抽象类与 Java 抽象类核心思想一致,但支持更丰富的特性,如抽象属性,是 Scala 面向对象的重要组成部分。
3.1 抽象类的定义与特性
3.1.1 核心定义
抽象类是不能被实例化的类,只能被子类继承实现,使用abstract关键字修饰,与 Java 一致。
- 抽象类中可包含抽象成员(无实现的方法、无初始化的属性)和非抽象成员(有具体实现的方法、已初始化的属性);
- 子类继承抽象类后,必须实现所有的抽象成员,否则子类也必须声明为抽象类。
3.1.2 与 Java 抽象类的核心差异
| 特性 | Scala 抽象类 | Java 抽象类 |
|---|---|---|
| 抽象方法 | 抽象方法无需使用abstract关键字修饰,仅需省略方法体 | 抽象方法必须使用abstract关键字修饰 |
| 抽象属性 | 支持抽象属性(无初始化的 val/var),子类必须实现 | 不支持抽象属性,仅能定义常量和成员变量 |
| 构造参数 | 抽象类的主构造器可带参数 | 抽象类的构造方法可带参数,但无法通过 new 直接实例化 |
| 多继承 | 类只能单继承一个抽象类,不可多继承 | 类只能单继承一个抽象类,不可多继承 |
3.2 抽象成员与非抽象成员
3.2.1 抽象成员
抽象方法:仅声明方法名、参数列表、返回值类型,省略方法体,无需
abstract关键字;抽象属性:仅声明属性名与类型,不进行初始化,分为抽象 val 和抽象 var:
- 抽象 val:子类必须用
override val重写实现; - 抽象 var:子类直接赋值即可,无需
override关键字。
- 抽象 val:子类必须用
3.2.2 非抽象成员
抽象类中可定义已初始化的属性、已实现的方法,子类可直接继承使用,也可通过override关键字重写。
3.3 课程案例实战
对应配套源码RunAbstract.scala、RunAOver.scala,实现数据库抽象类与动物抽象类:
// 抽象类:数据库
abstract class DataBase {
// 非抽象val:已初始化
val ip = "127.0.0.1"
// 抽象var:无初始化,子类必须实现
var port: Int
// 抽象方法:无方法体,无需abstract关键字
def connect
// 非抽象方法:已实现
def print(): Unit = {
println("connect failed")
}
}
// 子类:MySQL,继承抽象类DataBase,实现所有抽象成员
class mysql extends DataBase {
// 实现抽象var
override var port: Int = 3306
// 实现抽象方法
override def connect: Unit = {
println(s"connect mysql: $ip:$port")
}
// 重写非抽象方法
override def print(): Unit = {
println("connect mysql success")
}
}
// 测试
object RunAbstract {
def main(args: Array[String]): Unit = {
val m = new mysql()
println(m.ip, m.port)
m.connect
m.print()
}
}执行结果:
(127.0.0.1,3306)
connect mysql: 127.0.0.1:3306
connect mysql success3.4 抽象类的适用场景
Scala 官方推荐:当需要给基类定义构造器参数时,使用抽象类;否则优先使用 Trait。
- 场景 1:基类需要定义构造器参数,子类需传递参数初始化;
- 场景 2:需要与 Java 代码互相调用,Java 可直接识别抽象类,无法直接识别 Trait;
- 场景 3:基类需要定义大量非抽象的通用方法,供所有子类复用。
第四章 Scala 内部类、匿名类、内部对象
Scala 支持内部类、匿名类、内部对象,与 Java 有显著差异,是 Scala 面向对象的扩展特性。
4.1 内部类
内部类指定义在一个类 / 对象内部的类,与 Java 的核心差异:
- Java 内部类:绑定到外部类,同一个外部类的不同对象创建的内部类对象,属于同一类型;
- Scala 内部类:绑定到外部类的具体对象,同一个外部类的不同对象创建的内部类对象,属于不同的类型。
课程案例实战
对应配套源码RunNi.scala,实现内部类定义与使用:
class StudentIntenal(var name: String, var age: Int) {
// 定义在类内部的内部类:年级
class Grade(var name: String)
// 定义在类内部的内部对象
object Uilts1 {
def print(name: String): Unit = {
println(name)
}
}
}
// 测试
object RunNi {
def main(args: Array[String]): Unit = {
println("--------------内部类/内部对象--------------------")
// 创建外部类对象
val s = new StudentIntenal("scala", 5)
// 通过外部类对象创建内部类对象
val grade = new s.Grade("1 grade")
println(s"grade=${grade.name}")
// 访问外部类对象的内部对象
s.Uilts1.print("util1")
}
}4.2 内部对象
内部对象指定义在类 / 对象内部的单例对象,分为两种:
- 定义在类内部的内部对象:绑定到外部类的具体对象,每个外部类对象都有一个独立的内部对象实例;
- 定义在伴生对象内部的内部对象:全局单例,与外部伴生对象绑定,全局仅有一个实例。
4.3 匿名类
匿名类指没有名字的类,用于快速创建一个抽象类 / 特质的临时实现类,仅使用一次,无需单独定义子类。
- 适用场景:快速实现抽象类 / 特质的抽象成员,无需定义完整的子类;
- 语法:
new 抽象类/特质名 { 实现抽象成员 }。
课程案例实战
对应配套源码RunNi.scala,实现匿名类:
// 抽象类
abstract class P(var name: String, var age: Int) {
def print
}
object RunNi {
def main(args: Array[String]): Unit = {
// 匿名类:快速实现抽象类P,无需单独定义子类
val p1 = new P("jason", 10) {
override def print: Unit = {
println(s"P($name,$age)")
}
}
p1.print // 输出:P(jason,10)
}
}第五章 Scala 特质 Trait(核心重点)
Trait(特质)是 Scala 中最具特色的特性,替代了 Java 的接口,同时兼具抽象类的能力,支持多继承,是 Scala 实现代码复用、多态、面向接口编程的核心工具,也是 Spark 源码中最常用的设计方式。
5.1 Trait 的定义与核心特性
5.1.1 核心定义
Trait 使用trait关键字定义,既可以像 Java 接口一样仅定义抽象规范,也可以像抽象类一样定义具体的属性与方法实现,同时支持多混入(多继承)。
基础语法:
scalatrait 特质名 { // 抽象成员/非抽象成员 }类混入 Trait 的语法:
scala// 混入单个Trait class 类名 extends 特质名 { 实现抽象成员 } // 混入多个Trait(多继承) class 类名 extends 父类名 with 特质1 with 特质2 with 特质3 { 实现抽象成员 }
5.1.2 核心特性
无构造参数:Trait 的主构造器不能带任何参数,这是与抽象类最核心的区别;
错误示例:
trait Pet(name: String)编译报错,Trait 不能带构造参数。支持多混入(多继承):一个类可以通过
with关键字混入多个 Trait,解决了 Java 类单继承的限制;既支持抽象成员,也支持非抽象成员:Trait 中可定义抽象方法、抽象属性,也可定义已实现的方法、已初始化的属性;
不能被实例化:Trait 和抽象类一样,不能直接通过
new实例化,只能被类混入 / 继承;可混入对象:Trait 不仅可以混入类,还可以在创建对象时动态混入单个对象,实现对象级别的功能扩展。
5.2 Trait 的多种用法
5.2.1 作为纯接口使用
Trait 中仅定义抽象方法和抽象属性,无任何具体实现,完全等同于 Java 的接口,用于定义规范。
课程案例实战
对应配套源码RunTrait.scala,定义播放器基础接口 Trait:
// 特质1:基础播放器接口,仅定义抽象方法
trait BaseSoundPlayer {
def play
def close
// 也可定义非抽象方法
def see(): Unit = {
println("BaseSoundPlayer see ")
}
}
// 特质2:暂停播放器接口
trait BaseSoundPlayer1 {
def pause
def resume
}
// 类混入多个Trait,实现所有抽象方法
class Mp3 extends BaseSoundPlayer1 with BaseSoundPlayer {
override def play: Unit = println("i am Mp3 play")
override def close: Unit = println("i am Mp3 close")
override def pause: Unit = println("i am Mp3 pause")
override def resume: Unit = println("i am Mp3 resume")
}
// 测试
object RunTrait {
def main(args: Array[String]): Unit = {
val mp3 = new Mp3()
mp3.play
mp3.pause
mp3.resume
mp3.close
mp3.see()
}
}5.2.2 作为功能扩展混入
Trait 中定义大量通用的非抽象方法,用于给类扩展功能,实现代码的横向复用,这是 Trait 最核心的价值。
与继承的区别:继承是纵向的代码复用(父子类),Trait 是横向的代码复用(多个无关类可混入同一个 Trait,扩展相同功能)。
5.2.3 动态混入对象
Trait 可在创建对象时动态混入,仅给当前对象扩展功能,不影响该类的其他对象,实现灵活的功能扩展。
// 日志特质
trait Logger {
def log(msg: String): Unit = println(s"日志:$msg")
}
// 普通用户类
class User(val name: String)
object Test {
def main(args: Array[String]): Unit = {
// 创建对象时动态混入Logger特质,给该对象扩展日志功能
val user1 = new User("张三") with Logger
user1.log("用户张三登录") // 输出:日志:用户张三登录
// 普通对象,无日志功能
val user2 = new User("李四")
// user2.log() 编译报错,无该方法
}
}5.3 Trait 的构造器执行顺序
Trait 也有自己的构造器,构造器代码是 Trait 中除了方法定义之外的所有代码,当类混入多个 Trait 时,有严格的构造器执行顺序,是课程重点。
5.3.1 核心执行规则
- 先执行父类的构造器;
- 再按照从左到右的顺序,依次执行 Trait 的构造器;
- 若 Trait 有父 Trait,先执行父 Trait 的构造器,再执行自身构造器;
- 多个 Trait 有共同的父 Trait,该父 Trait 仅会执行一次,不会重复执行;
- 最后执行当前子类的构造器。
5.3.2 课程案例实战
对应配套源码RunConOder.scala,演示 Trait 构造器执行顺序:
object RunConOder {
def main(args: Array[String]): Unit = {
new AB() // 创建对象,查看构造器执行顺序
}
}
// 父Trait A051
trait A051 {
println("invoke Trait A051")
}
// 子Trait AA051,继承A051
trait AA051 extends A051 {
println("invoke Trait AA051")
}
// 子Trait AB051,继承A051
trait AB051 extends A051 {
println("invoke Trait AB051")
}
// 父Trait B051
trait B051 {
println("invoke Trait B051")
}
trait BA051 extends B051 {
println("invoke Trait BA051")
}
trait BB051 extends B051 {
println("invoke Trait BB051")
}
// 子类AB,混入多个Trait
class AB extends AA051 with BA051 with AB051 with BB051 {
println("invoke Class AB")
}执行结果:
invoke Trait A051
invoke Trait AA051
invoke Trait B051
invoke Trait BA051
invoke Trait AB051
invoke Trait BB051
invoke Class AB结果说明:共同父 Trait A051 仅执行一次,后续 AB051 继承 A051 时,不会重复执行。
5.4 Trait 的菱形继承问题与 super 关键字
5.4.1 菱形继承问题
当多个 Trait 继承自同一个父 Trait,并重写了父 Trait 的同一个方法,一个类同时混入这多个 Trait 时,就形成了菱形继承(钻石继承)。
- Java 中接口的菱形继承无冲突,因为接口仅定义方法规范,无具体实现;
- Scala 中 Trait 可定义方法的具体实现,菱形继承会产生方法调用冲突,Scala 通过线性化解析解决该问题。
5.4.2 线性化解析规则
Scala 对混入的多个 Trait 进行从右到左的线性化排序,调用super.方法()时,会按照线性化顺序,调用右侧最近的 Trait的重写方法,而非父 Trait 的方法。
5.4.3 课程案例实战
对应配套源码RunMultiInherit.scala,演示菱形继承与 super 关键字的使用:
object RunMultiInherit {
def main(args: Array[String]): Unit = {
val d = new Dog0501()
d.speak("hello")
}
}
// 父Trait:动物,形成菱形的顶端
trait Animal051 {
def speak(msg: String): Unit = {
val speak = "Animal051"
println(speak + "->" + msg)
}
}
// 子Trait1:宠物1,继承Animal051,重写speak方法
trait Pet0501 extends Animal051 {
override def speak(msg: String): Unit = {
val speak = "Pet0501"
super.speak(msg + "->" + speak) // 调用线性化顺序的上一个Trait的speak方法
}
}
// 子Trait2:宠物2,继承Animal051,重写speak方法
trait Pet0502 extends Animal051 {
override def speak(msg: String): Unit = {
val speak = "Pet0502"
super.speak(msg + "->" + speak)
}
}
// 子类:狗,混入两个Trait,顺序:Pet0502 with Pet0501
class Dog0501 extends Pet0502 with Pet0501 {
override def cry: Unit = println("dog cry")
}执行结果:
Animal051->hello->Pet0501->Pet0502执行顺序说明:
- 线性化顺序:Dog0501 → Pet0501 → Pet0502 → Animal051 → AnyRef;
- 调用
d.speak("hello")时,先执行最右侧的 Pet0501 的 speak 方法;- Pet0501 的
super.speak调用左侧的 Pet0502 的 speak 方法;- Pet0502 的
super.speak调用父 Trait Animal051 的 speak 方法;- 最终形成输出结果。
5.5 Trait 构造器空指针问题与解决方案
5.5.1 问题产生原因
在 Trait 的构造器中,使用了子类重写的抽象 val 属性,由于 Trait 构造器执行顺序早于子类构造器,此时子类的 val 属性还未初始化,值为 null,调用其方法就会抛出空指针异常。
5.5.2 课程案例实战
对应配套源码RunFile.scala,演示空指针问题与解决方案:
// 问题代码:Trait构造器中使用抽象val,导致空指针
trait Logger {
def log(msg: String)
}
trait FileLogger extends Logger {
// 抽象val,由子类重写
val filename: String
// Trait构造器中使用filename,此时子类还未初始化,filename为null
val fileout = new PrintWriter(filename) // 抛出NullPointerException
def log(msg: String): Unit = {
fileout.println(msg)
fileout.flush()
}
}
class Person051 extends FileLogger {
// 子类重写filename,执行时机晚于父Trait构造器
override val filename = "p051.log"
}5.5.3 解决方案
有两种方式解决该空指针问题,课程中重点讲解了懒加载方案:
方案 1:懒加载 lazy:给属性添加
lazy关键字,属性仅在第一次使用时才会初始化,此时子类构造器已执行完成,val 已赋值,不会出现 null。scalatrait FileLogger extends Logger { val filename: String // 添加lazy关键字,懒加载 lazy val fileout = new PrintWriter(filename) def log(msg: String): Unit = { fileout.println(msg) fileout.flush() } }方案 2:提前定义:创建对象时,提前定义重写的 val 属性,优先初始化。
scala// 创建对象时提前定义filename val p = new { override val filename = "p052.log" } with FileLogger
第六章 Trait、抽象类与 Java 接口的对比
6.1 Trait 与抽象类的对比
表格
| 特性 | Trait | 抽象类 |
|---|---|---|
| 构造参数 | 不能带构造参数 | 可以带构造参数 |
| 多继承 | 支持多混入(多继承) | 仅支持单继承 |
| 适用场景 | 横向功能扩展、定义接口规范、多继承场景 | 纵向父子类体系、基类需要构造参数、与 Java 互调 |
| 构造器执行 | 混入时按顺序执行,父 Trait 仅执行一次 | 先执行父类构造器,再执行子类构造器 |
| 菱形继承 | 通过线性化解析解决,支持 super 线性调用 | 不支持多继承,无菱形继承问题 |
6.2 Trait 与 Java 接口的对比
表格
| 特性 | Scala Trait | Java 接口 |
|---|---|---|
| 抽象方法 | 支持,无需 abstract 关键字 | 支持,Java8 前必须 abstract,Java8 + 可 default |
| 具体方法实现 | 完全支持,可定义任意非抽象方法 | Java8 + 支持 default/static 方法,之前版本不支持 |
| 抽象属性 | 支持抽象 val/var | 不支持抽象属性,仅能定义 public static final 常量 |
| 非抽象属性 | 支持 val/var 成员属性 | 仅能定义 public static final 常量,不能定义成员属性 |
| 多继承 | 支持多混入,可同时混入多个 Trait | 支持多实现,一个类可实现多个接口 |
| 状态保存 | 可通过成员属性保存对象状态 | 无法保存对象状态,仅能定义常量 |
6.3 官方推荐使用原则
- 如果行为不会被复用,就定义为具体类;
- 如果行为会在多个不相关的类中复用,就定义为 Trait(横向复用);
- 如果需要在多个相关的类中复用,且需要构建继承体系,就定义为抽象类(纵向复用);
- 如果需要与 Java 代码互调,优先使用抽象类,Java 可直接识别抽象类,Trait 需特殊处理。
第七章 课后作业全解析
对应课程配套作业文档,包含简答题与编程题全解析,所有代码可直接复制运行。
一、简答题
(1)Scala 的继承。
答:Scala 的继承是面向对象编程的核心特性,指在原有父类的基础上定义子类,子类可复用父类的属性与方法,同时扩展自身特性,最终实现代码复用与多态,核心要点如下:
- 语法规则:使用
extends关键字实现继承,类仅能单继承一个父类,多继承需通过 Trait 实现; - 构造器调用:子类主构造器必须直接调用父类的主构造器 / 辅助构造器,子类辅助构造器绝对不能直接调用父类构造器,只能调用本类的其他构造器;
- 方法重写:子类重写父类方法时,必须使用
override关键字,否则编译报错,重写的方法名、参数列表、返回值必须与父类完全一致; - 继承层级:Scala 所有类默认继承 AnyRef 类,最终顶级父类是 Any,形成了完整的类层级体系;
- 多态实现:通过父类引用指向子类对象,运行时根据对象的实际类型调用重写的方法,实现多态。
(2)Scala 的多态。
答:Scala 的多态是面向对象的三大特性之一,指程序运行期间,根据对象的实际类型,调用对应类型的方法,而非编译时的引用类型,核心要点如下:
- 多态的前提:必须同时满足三个条件 —— 继承关系、方法重写、父类引用指向子类对象;
- 编译与运行特性:编译时,编译器仅识别父类的方法;运行时,JVM 会根据对象的实际类型,调用子类重写的方法,即 “编译看左边,运行看右边”;
- 多态的作用:提高程序的可扩展性与可维护性,实现面向接口 / 抽象编程,无需修改原有代码,即可扩展新的子类实现;
- 与 Java 的差异:Scala 的多态实现逻辑与 Java 一致,但 Scala 强制要求重写方法必须使用
override关键字,避免了 Java 中因方法签名写错导致的重写失效问题。
二、编程题
(1)提供一个 cryptoLogger 类,将日志消息以凯撒密码加密,默认密钥为 3,不过使用者也可重写它,提供默认密钥和 - 3 作为密钥时的使用示例。
/**
* 日志特质,定义日志规范
*/
trait Logger {
def log(msg: String): Unit
}
/**
* 凯撒加密日志特质,实现加密逻辑
*/
trait CryptoLogger extends Logger {
// 加密密钥,默认3,子类可重写
val key: Int = 3
/**
* 凯撒加密核心方法
* @param msg 原始日志
* @return 加密后的日志
*/
private def caesarEncrypt(msg: String): String = {
msg.map { char =>
// 仅对字母进行加密,其他字符保持不变
if (char.isLetter) {
val base = if (char.isUpper) 'A' else 'a'
// 计算偏移后的字符,处理循环
((char - base + key) % 26 + 26) % 26 + base toChar
} else {
char
}
}.mkString
}
/**
* 重写日志方法,加密后输出
*/
override def log(msg: String): Unit = {
val encryptedMsg = caesarEncrypt(msg)
println(s"加密日志[密钥=$key]:$encryptedMsg | 原始内容:$msg")
}
}
/**
* 默认密钥3的日志类
*/
class DefaultCryptoLogger extends CryptoLogger
/**
* 自定义密钥-3的日志类
*/
class CustomCryptoLogger extends CryptoLogger {
// 重写密钥为-3
override val key: Int = -3
}
/**
* 测试使用示例
*/
object CryptoLoggerTest {
def main(args: Array[String]): Unit = {
println("===== 默认密钥3 =====")
val defaultLogger = new DefaultCryptoLogger()
defaultLogger.log("Hello Scala")
defaultLogger.log("Spark is good")
println("\n===== 自定义密钥-3 =====")
val customLogger = new CustomCryptoLogger()
customLogger.log("Hello Scala")
customLogger.log("Spark is good")
// 动态混入对象,自定义密钥
println("\n===== 动态混入自定义密钥5 =====")
val dynamicLogger = new {
override val key: Int = 5
} with CryptoLogger
dynamicLogger.log("Hello World")
}
}(2)定义一个抽象类 Item,加入方法 price 和 desc。SimpleItem 是一个在构造器中给出价格和描述的物件。利用 val 也可以重写 def 这个事实。Bundle 是一个可以包含其他物件的物件,其价格是打包中所有物件的价格之和。同时提供一个将物件添加到打包中的机制,以及一个合适的 desc 方法。
import scala.collection.mutable.ArrayBuffer
/**
* 抽象类:商品
*/
abstract class Item {
// 价格方法,子类实现
def price: Double
// 描述方法,子类实现
def desc: String
}
/**
* 简单商品类,继承Item
* @param desc 商品描述
* @param price 商品价格
*/
class SimpleItem(override val desc: String, override val price: Double) extends Item
/**
* 商品打包类,可包含多个商品
*/
class Bundle extends Item {
// 存储打包中的商品
private val items: ArrayBuffer[Item] = ArrayBuffer
/**
* 添加商品到打包中
* @param item 商品对象
*/
def addItem(item: Item): Unit = {
items += item
}
/**
* 重写价格:所有商品价格之和
*/
override def price: Double = items.map(_.price).sum
/**
* 重写描述:打包中所有商品的描述汇总
*/
override def desc: String = {
if (items.isEmpty) {
"空商品打包"
} else {
s"商品打包【包含${items.size}件商品】:\n" + items.map(item => s"- ${item.desc}:¥${item.price}").mkString("\n") +
s"\n总价:¥${price}"
}
}
}
/**
* 测试商品打包
*/
object ItemTest {
def main(args: Array[String]): Unit = {
// 创建简单商品
val phone = new SimpleItem("iPhone 16", 5999.0)
val earphone = new SimpleItem("AirPods Pro", 1899.0)
val charger = new SimpleItem("20W快充头", 149.0)
// 创建打包,添加商品
val bundle = new Bundle()
bundle.addItem(phone)
bundle.addItem(earphone)
bundle.addItem(charger)
// 嵌套打包:配件打包
val accessoryBundle = new Bundle()
accessoryBundle.addItem(earphone)
accessoryBundle.addItem(charger)
val mainBundle = new Bundle()
mainBundle.addItem(phone)
mainBundle.addItem(accessoryBundle)
// 打印结果
println("===== 单个商品 =====")
println(s"${phone.desc}:¥${phone.price}")
println(s"${earphone.desc}:¥${earphone.price}")
println("\n===== 商品打包 =====")
println(bundle.desc)
println("\n===== 嵌套打包 =====")
println(mainBundle.desc)
}
}(3)设计一个 point 类,其 x 和 y 坐标可以通过构造器提供。提供一个子类 LablePoint,其构造器接受一个标签值和 x、y 坐标,比如:New LablePoint(“black”,1929,230.07)
/**
* 点类,包含x、y坐标
* @param x 横坐标
* @param y 纵坐标
*/
class Point(val x: Double, val y: Double) {
// 重写toString,方便打印
override def toString: String = s"Point(x=$x, y=$y)"
}
/**
* 带标签的点类,继承Point
* @param label 标签值
* @param x 横坐标
* @param y 纵坐标
*/
class LabelPoint(val label: String, x: Double, y: Double) extends Point(x, y) {
// 重写toString,包含标签
override def toString: String = s"LabelPoint(label='$label', x=$x, y=$y)"
}
/**
* 测试类
*/
object PointTest {
def main(args: Array[String]): Unit = {
// 创建普通点
val point = new Point(10.5, 20.8)
println(point)
// 创建带标签的点,对应题目示例
val labelPoint = new LabelPoint("black", 1929, 230.07)
println(labelPoint)
// 多态测试
val p: Point = new LabelPoint("red", 0, 0)
println(p)
}
}学习总结
- Scala 继承与构造器:Scala 继承的核心难点是父类构造器的调用规则,子类主构造器必须调用父类构造器,子类辅助构造器无法直接调用父类构造器,这是与 Java 最核心的差异,也是新手高频踩坑点。
- 方法重写与多态:Scala 强制要求重写方法必须使用
override关键字,相比 Java 更严谨,避免了重写失效的问题,多态的实现逻辑与 Java 一致,是面向对象编程的核心。 - 抽象类:Scala 抽象类相比 Java 支持抽象属性,抽象方法无需
abstract关键字,适用于构建纵向的父子类继承体系,基类需要构造参数的场景。 - Trait 特质:是 Scala 最核心的特色,替代了 Java 的接口,同时兼具抽象类的能力,支持多混入(多继承),实现了代码的横向复用,是 Spark 源码中最核心的设计方式,必须重点掌握。
- Trait 的核心难点:Trait 的构造器执行顺序、菱形继承的线性化解析、super 关键字的线性调用、构造器空指针问题的解决方案,是本节的难点,需结合案例反复练习理解。
- 适用场景:优先使用 Trait 实现接口规范与横向功能扩展,仅在需要构造参数、构建纵向继承体系时使用抽象类,是 Scala 官方推荐的编码规范。
下一章预习内容:Scala 集合与序列。