Skip to content

5 继承、多态与 Trait ​

笔记衔接前序《Scala 自适应类型与函数》,严格匹配课程体系,针对有 Java 基础的大数据专业大学生设计,全程Java 语法对比 + 课程配套源码拆解 + Spark 大数据场景应用 + 避坑指南 + 作业全解析。Scala 的继承、多态与 Trait 是 Spark/Flink 框架源码的核心设计思想,Spark 中几乎所有核心组件(RDD、SparkSession、Streaming 等)均基于 Trait 与继承体系实现,本节内容是后续大数据框架学习的核心基础。

  • 前置知识:Scala 面向对象基础、类与伴生对象、函数式编程基础

  • 开发环境:JDK1.8+、Scala2.12.x、IDEA + Scala 插件

  • 核心学习目标:

    1. 掌握 Scala 继承的核心规则,重点掌握父类构造器的调用机制,明确与 Java 的核心差异
  1. 熟练掌握 Scala 方法重写与多态的实现,理解 Scala 重写的强制规则
  2. 掌握 Scala 抽象类的定义与用法,明确与 Java 抽象类的差异
  3. 深入理解 Scala Trait 特质的核心特性,掌握 Trait 的混入、多继承、构造顺序等核心知识点
  4. 明确 Trait 与抽象类、Java 接口的适用场景,独立完成课程配套作业

第一章 Scala 继承与多态 ​

Scala 的继承与多态核心思想与 Java 一致,但在构造器调用、方法重写规则上有严格的强制限制,是新手高频踩坑点,必须重点掌握。

1.1 继承的核心定义与语法 ​

1.1.1 核心定义 ​

继承是在原有类(父类 / 超类)的基础上定义新类(子类 / 派生类),子类可复用父类的属性与方法,同时可扩展自身的特性,最终实现代码复用与多态。

  • Scala 中使用extends关键字实现继承,与 Java 一致;
  • Scala 与 Java 相同,类只能单继承一个父类,不可多继承类,多继承能力需通过 Trait 实现;
  • 所有类默认继承AnyRef类(对应 Java 的Object类),是 Scala 所有引用类型的顶级父类。

1.1.2 基础语法 ​

scala
// 父类定义
class 父类名(参数列表) {
  // 父类属性与方法
}

// 子类继承父类
class 子类名(子类参数列表) extends 父类名(父类构造参数) {
  // 子类扩展的属性与方法
}

1.2 父类构造器的调用规则(重点难点) ​

Scala 的构造器体系与 Java 完全不同,子类继承父类时,必须明确调用父类的构造器,且有严格的调用规则,这是与 Java 最核心的差异,也是课程重点难点。

1.2.1 核心调用规则 ​

  1. 子类主构造器必须直接调用父类的构造器(主构造器或辅助构造器),调用逻辑写在extends后的父类名参数列表中;
  2. 子类的辅助构造器,绝对不能直接调用父类的构造器,只能调用本类已定义的主构造器 / 其他辅助构造器(最终仍会调用父类构造器);
  3. 父类的辅助构造器,只能被子类的主构造器调用,子类辅助构造器无法直接调用。

与 Java 的核心差异:Java 中,子类的所有构造方法(无论无参 / 有参)都可以通过super()直接调用父类的任意构造方法,无此限制。

1.2.2 课程案例实战 ​

对应配套源码RunInheritPerson.scala,实现 Person 父类与 Employee 子类的构造器调用:

scala
// 父类: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")
  }
}

执行结果:

plaintext
父类构造器执行=>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 对方法重写有严格的强制规则:

  1. 必须使用override关键字修饰重写的方法,否则编译报错;

    与 Java 的核心差异:Java 中重写方法的@Override注解是可选的,可省略,Scala 中override关键字是强制的,不可省略。

  2. 重写的方法名、参数列表、返回值类型必须与父类完全一致;

  3. 父类中用final修饰的方法,子类不可重写;

  4. 父类中用private修饰的方法,子类不可访问,也无法重写。

1.3.2 多态的实现 ​

多态指程序运行期间,根据对象的实际类型,调用对应类型的重写方法,而非编译时的引用类型。

  • 多态的前提:继承关系 + 方法重写 + 父类引用指向子类对象;
  • 多态的作用:提高程序的可扩展性与可维护性,实现面向接口编程。

1.3.3 课程案例实战 ​

对应配套源码RunInheritPerson.scala,实现 Animal 父类与 Dog/Cat 子类的方法重写与多态:

scala
// 父类:动物
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
  }
}

执行结果:

plaintext
-------Dog-----------
run by leg 
dog wangwang  speak 
-------cat-----------
run by leg 
cat miaomiao  speak

第二章 Scala 类层级结构 ​

Scala 中所有类都有共同的顶级父类Any,形成了完整的类层级体系,这是 Scala 类型系统的核心基础。

2.1 完整类层级结构 ​

image-20250315020810960

plaintext
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 核心类型说明 ​

  1. Any:Scala 中所有类型的顶级父类,定义了equals()、hashCode()、toString()等通用方法,所有类型都可赋值给 Any 类型的变量。
  2. AnyVal:所有值类型的父类,共 9 个值类型,均为不可变类型,对应 Java 的 8 种基本类型 + Unit。
  3. AnyRef:所有引用类型的父类,对应 Java 的java.lang.Object,Scala 中所有的类、Trait、数组、字符串都继承自 AnyRef。
  4. Null:所有引用类型的子类,唯一实例是null,仅能赋值给引用类型,不能赋值给值类型。
  5. 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 抽象成员 ​

  1. 抽象方法:仅声明方法名、参数列表、返回值类型,省略方法体,无需abstract关键字;

  2. 抽象属性:仅声明属性名与类型,不进行初始化,分为抽象 val 和抽象 var:

    • 抽象 val:子类必须用override val重写实现;
    • 抽象 var:子类直接赋值即可,无需override关键字。

3.2.2 非抽象成员 ​

抽象类中可定义已初始化的属性、已实现的方法,子类可直接继承使用,也可通过override关键字重写。

3.3 课程案例实战 ​

对应配套源码RunAbstract.scala、RunAOver.scala,实现数据库抽象类与动物抽象类:

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

执行结果:

plaintext
(127.0.0.1,3306)
connect mysql: 127.0.0.1:3306
connect mysql success

3.4 抽象类的适用场景 ​

Scala 官方推荐:当需要给基类定义构造器参数时,使用抽象类;否则优先使用 Trait。

  • 场景 1:基类需要定义构造器参数,子类需传递参数初始化;
  • 场景 2:需要与 Java 代码互相调用,Java 可直接识别抽象类,无法直接识别 Trait;
  • 场景 3:基类需要定义大量非抽象的通用方法,供所有子类复用。

第四章 Scala 内部类、匿名类、内部对象 ​

Scala 支持内部类、匿名类、内部对象,与 Java 有显著差异,是 Scala 面向对象的扩展特性。

4.1 内部类 ​

内部类指定义在一个类 / 对象内部的类,与 Java 的核心差异:

  • Java 内部类:绑定到外部类,同一个外部类的不同对象创建的内部类对象,属于同一类型;
  • Scala 内部类:绑定到外部类的具体对象,同一个外部类的不同对象创建的内部类对象,属于不同的类型。

课程案例实战 ​

对应配套源码RunNi.scala,实现内部类定义与使用:

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 内部对象 ​

内部对象指定义在类 / 对象内部的单例对象,分为两种:

  1. 定义在类内部的内部对象:绑定到外部类的具体对象,每个外部类对象都有一个独立的内部对象实例;
  2. 定义在伴生对象内部的内部对象:全局单例,与外部伴生对象绑定,全局仅有一个实例。

4.3 匿名类 ​

匿名类指没有名字的类,用于快速创建一个抽象类 / 特质的临时实现类,仅使用一次,无需单独定义子类。

  • 适用场景:快速实现抽象类 / 特质的抽象成员,无需定义完整的子类;
  • 语法:new 抽象类/特质名 { 实现抽象成员 }。

课程案例实战 ​

对应配套源码RunNi.scala,实现匿名类:

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 接口一样仅定义抽象规范,也可以像抽象类一样定义具体的属性与方法实现,同时支持多混入(多继承)。

  • 基础语法:

    scala
    trait 特质名 {
      // 抽象成员/非抽象成员
    }
  • 类混入 Trait 的语法:

    scala
    // 混入单个Trait
    class 类名 extends 特质名 { 实现抽象成员 }
    // 混入多个Trait(多继承)
    class 类名 extends 父类名 with 特质1 with 特质2 with 特质3 { 实现抽象成员 }

5.1.2 核心特性 ​

  1. 无构造参数:Trait 的主构造器不能带任何参数,这是与抽象类最核心的区别;

    错误示例:trait Pet(name: String) 编译报错,Trait 不能带构造参数。

  2. 支持多混入(多继承):一个类可以通过with关键字混入多个 Trait,解决了 Java 类单继承的限制;

  3. 既支持抽象成员,也支持非抽象成员:Trait 中可定义抽象方法、抽象属性,也可定义已实现的方法、已初始化的属性;

  4. 不能被实例化:Trait 和抽象类一样,不能直接通过new实例化,只能被类混入 / 继承;

  5. 可混入对象:Trait 不仅可以混入类,还可以在创建对象时动态混入单个对象,实现对象级别的功能扩展。

5.2 Trait 的多种用法 ​

5.2.1 作为纯接口使用 ​

Trait 中仅定义抽象方法和抽象属性,无任何具体实现,完全等同于 Java 的接口,用于定义规范。

课程案例实战 ​

对应配套源码RunTrait.scala,定义播放器基础接口 Trait:

scala
// 特质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 可在创建对象时动态混入,仅给当前对象扩展功能,不影响该类的其他对象,实现灵活的功能扩展。

scala
// 日志特质
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 核心执行规则 ​

  1. 先执行父类的构造器;
  2. 再按照从左到右的顺序,依次执行 Trait 的构造器;
  3. 若 Trait 有父 Trait,先执行父 Trait 的构造器,再执行自身构造器;
  4. 多个 Trait 有共同的父 Trait,该父 Trait 仅会执行一次,不会重复执行;
  5. 最后执行当前子类的构造器。

5.3.2 课程案例实战 ​

对应配套源码RunConOder.scala,演示 Trait 构造器执行顺序:

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

执行结果:

plaintext
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 关键字的使用:

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

执行结果:

plaintext
Animal051->hello->Pet0501->Pet0502

执行顺序说明:

  1. 线性化顺序:Dog0501 → Pet0501 → Pet0502 → Animal051 → AnyRef;
  2. 调用d.speak("hello")时,先执行最右侧的 Pet0501 的 speak 方法;
  3. Pet0501 的super.speak调用左侧的 Pet0502 的 speak 方法;
  4. Pet0502 的super.speak调用父 Trait Animal051 的 speak 方法;
  5. 最终形成输出结果。

5.5 Trait 构造器空指针问题与解决方案 ​

5.5.1 问题产生原因 ​

在 Trait 的构造器中,使用了子类重写的抽象 val 属性,由于 Trait 构造器执行顺序早于子类构造器,此时子类的 val 属性还未初始化,值为 null,调用其方法就会抛出空指针异常。

5.5.2 课程案例实战 ​

对应配套源码RunFile.scala,演示空指针问题与解决方案:

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. 方案 1:懒加载 lazy:给属性添加lazy关键字,属性仅在第一次使用时才会初始化,此时子类构造器已执行完成,val 已赋值,不会出现 null。

    scala
    trait FileLogger extends Logger {
      val filename: String
      // 添加lazy关键字,懒加载
      lazy val fileout = new PrintWriter(filename)
      
      def log(msg: String): Unit = {
        fileout.println(msg)
        fileout.flush()
      }
    }
  2. 方案 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 TraitJava 接口
抽象方法支持,无需 abstract 关键字支持,Java8 前必须 abstract,Java8 + 可 default
具体方法实现完全支持,可定义任意非抽象方法Java8 + 支持 default/static 方法,之前版本不支持
抽象属性支持抽象 val/var不支持抽象属性,仅能定义 public static final 常量
非抽象属性支持 val/var 成员属性仅能定义 public static final 常量,不能定义成员属性
多继承支持多混入,可同时混入多个 Trait支持多实现,一个类可实现多个接口
状态保存可通过成员属性保存对象状态无法保存对象状态,仅能定义常量

6.3 官方推荐使用原则 ​

  1. 如果行为不会被复用,就定义为具体类;
  2. 如果行为会在多个不相关的类中复用,就定义为 Trait(横向复用);
  3. 如果需要在多个相关的类中复用,且需要构建继承体系,就定义为抽象类(纵向复用);
  4. 如果需要与 Java 代码互调,优先使用抽象类,Java 可直接识别抽象类,Trait 需特殊处理。

第七章 课后作业全解析 ​

对应课程配套作业文档,包含简答题与编程题全解析,所有代码可直接复制运行。

一、简答题 ​

(1)Scala 的继承。 ​

答:Scala 的继承是面向对象编程的核心特性,指在原有父类的基础上定义子类,子类可复用父类的属性与方法,同时扩展自身特性,最终实现代码复用与多态,核心要点如下:

  1. 语法规则:使用extends关键字实现继承,类仅能单继承一个父类,多继承需通过 Trait 实现;
  2. 构造器调用:子类主构造器必须直接调用父类的主构造器 / 辅助构造器,子类辅助构造器绝对不能直接调用父类构造器,只能调用本类的其他构造器;
  3. 方法重写:子类重写父类方法时,必须使用override关键字,否则编译报错,重写的方法名、参数列表、返回值必须与父类完全一致;
  4. 继承层级:Scala 所有类默认继承 AnyRef 类,最终顶级父类是 Any,形成了完整的类层级体系;
  5. 多态实现:通过父类引用指向子类对象,运行时根据对象的实际类型调用重写的方法,实现多态。

(2)Scala 的多态。 ​

答:Scala 的多态是面向对象的三大特性之一,指程序运行期间,根据对象的实际类型,调用对应类型的方法,而非编译时的引用类型,核心要点如下:

  1. 多态的前提:必须同时满足三个条件 —— 继承关系、方法重写、父类引用指向子类对象;
  2. 编译与运行特性:编译时,编译器仅识别父类的方法;运行时,JVM 会根据对象的实际类型,调用子类重写的方法,即 “编译看左边,运行看右边”;
  3. 多态的作用:提高程序的可扩展性与可维护性,实现面向接口 / 抽象编程,无需修改原有代码,即可扩展新的子类实现;
  4. 与 Java 的差异:Scala 的多态实现逻辑与 Java 一致,但 Scala 强制要求重写方法必须使用override关键字,避免了 Java 中因方法签名写错导致的重写失效问题。

二、编程题 ​

(1)提供一个 cryptoLogger 类,将日志消息以凯撒密码加密,默认密钥为 3,不过使用者也可重写它,提供默认密钥和 - 3 作为密钥时的使用示例。 ​

scala
/**
 * 日志特质,定义日志规范
 */
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 方法。 ​

scala
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) ​

scala
/**
 * 点类,包含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)
  }
}

学习总结 ​

  1. Scala 继承与构造器:Scala 继承的核心难点是父类构造器的调用规则,子类主构造器必须调用父类构造器,子类辅助构造器无法直接调用父类构造器,这是与 Java 最核心的差异,也是新手高频踩坑点。
  2. 方法重写与多态:Scala 强制要求重写方法必须使用override关键字,相比 Java 更严谨,避免了重写失效的问题,多态的实现逻辑与 Java 一致,是面向对象编程的核心。
  3. 抽象类:Scala 抽象类相比 Java 支持抽象属性,抽象方法无需abstract关键字,适用于构建纵向的父子类继承体系,基类需要构造参数的场景。
  4. Trait 特质:是 Scala 最核心的特色,替代了 Java 的接口,同时兼具抽象类的能力,支持多混入(多继承),实现了代码的横向复用,是 Spark 源码中最核心的设计方式,必须重点掌握。
  5. Trait 的核心难点:Trait 的构造器执行顺序、菱形继承的线性化解析、super 关键字的线性调用、构造器空指针问题的解决方案,是本节的难点,需结合案例反复练习理解。
  6. 适用场景:优先使用 Trait 实现接口规范与横向功能扩展,仅在需要构造参数、构建纵向继承体系时使用抽象类,是 Scala 官方推荐的编码规范。

下一章预习内容:Scala 集合与序列。

基于 Vite 强力驱动 | 纯静态轻量托管