五、Scala继承与多态

举报
IvanCodes 发表于 2025/11/08 21:06:18 2025/11/08
【摘要】 继承能让子类直接拿到父类的方法和属性,还能用override改写、super调父类原版,final则用来堵继承的路。抽象类像个契约,子类必须补上没写完的部分。再配合isInstanceOf、asInstanceOf做类型判断和转换,甚至还能整匿名内部类来写个一次性小工具,挺灵活的。

在上一节中,我们掌握了如何定义独立的类和对象。本节,我们将深入探索类与类之间的关系,这是构建复杂、可扩展软件系统的基石。我们将重点学习Scala 中的继承抽象类类型判断,这些概念共同构成了面向对象编程中多态性的核心。

思维导图

在这里插入图片描述
在这里插入图片描述

一、继承

继承面向对象编程中实现代码复用建立类层次结构的核心机制。它允许一个类 (称为子类派生类) 获取另一个类 (称为父类基类) 的非私有成员 (字段和方法)。

1. 基本语法 (extends)
使用 extends 关键字来声明一个类继承自另一个类。

class Person(val name: String) {
  def sayHello(): Unit = println(s"Hello, I am $name.")
}

// Student 类继承自 Person 类
class Student(name: String, val studentId: String) extends Person(name) {
  def study(): Unit = println(s"$name is studying.")
}

val student = new Student("Alice", "S12345")
student.sayHello() // 调用从父类继承的方法
student.study()    // 调用子类自己的方法

2. 方法重写 (override)
子类可以重新定义从父类继承过来的方法,以提供自己特定实现。在 Scala 中,重写父类方法 必须 使用 override 关键字。

3. 调用父类方法 (super)
在子类中,可以使用 super 关键字来调用父类中被重写的方法。

代码案例:

class Employee(name: String) extends Person(name) {
  // 使用 override 关键字重写父类的 sayHello 方法
  override def sayHello(): Unit = {
    super.sayHello() // 使用 super 调用父类的原始方法
    println("I am also an employee.")
  }
}

val employee = new Employee("Bob")
employee.sayHello()

4. 防止继承与重写 (final)

final class ...: 不希望一个类被任何其他类继承,可以将其声明为 final
final def ...: 不希望父类中的某个方法被子类重写,可以将其声明为 final

二、抽象类

抽象类一种不能被直接实例化的类,它存在的目的就是为了被其他类继承。抽象类中可以包含 抽象成员 (没有实现的字段或方法),用于定义子类必须实现“契约”

语法特点:

使用 abstract class 关键字定义
抽象方法:只声明方法签名,没有方法体 (= { ... })
抽象字段:只声明字段名称和类型,不赋初始值

代码案例:

// 定义一个抽象类 Shape
abstract class Shape {
  // 抽象字段
  val name: String

  // 抽象方法
  def area(): Double
  
  // 普通方法
  def printInfo(): Unit = {
    println(s"This is a $name with area ${area()}")
  }
}

// 子类继承抽象类,必须实现所有抽象成员
class Rectangle(val width: Double, val height: Double) extends Shape {
  // 实现抽象字段
  override val name: String = "Rectangle"

  // 实现抽象方法
  override def area(): Double = width * height
}

// val s = new Shape() // 错误:不能实例化抽象类
val rect = new Rectangle(10, 5)
rect.printInfo()

三、类型判断与转换

具有继承关系的类层次中,我们经常需要判断一个父类引用实际指向的是哪个子类的对象,并可能需要将其转换子类类型调用特有的方法。

操作 语法 描述
类型判断 obj.isInstanceOf[Type] 检查 obj 是否是 Type 类型或其子类型的实例。返回一个布尔值
类型转换 obj.asInstanceOf[Type] obj 的引用强制转换Type 类型。如果 obj 不是 Type 类型的实例,会抛出 ClassCastException 异常。

最佳实践: 总是先使用 isInstanceOf 进行检查然后再安全地使用 asInstanceOf 进行转换。在后续章节中,我们会学习更优雅、更安全模式匹配替代这种用法。

代码案例:

class Pet(val name: String)
class Cat(name: String) extends Pet(name) {
  def meow(): Unit = println("Meow!")
}
class Dog(name: String) extends Pet(name) {
  def bark(): Unit = println("Woof!")
}

val myPet: Pet = new Cat("Mimi")

// 类型判断
if (myPet.isInstanceOf[Cat]) {
  println("It's a cat!")
  // 安全地进行类型转换
  val myCat = myPet.asInstanceOf[Cat]
  myCat.meow()
} else if (myPet.isInstanceOf[Dog]) {
  println("It's a dog!")
  val myDog = myPet.asInstanceOf[Dog]
  myDog.bark()
}

四、匿名内部类

有时,我们只需要一个类或抽象类一个临时一次性子类实例不想为其显式地定义一个完整的具名子类。这时,就可以使用匿名内部类

语法:

new SuperClassName/AbstractClassName {
  // 在这里重写或实现需要的方法
}

代码案例:

abstract class Greeter {
  def greet(): Unit
}

// 创建一个 Greeter 的匿名子类实例
val friendlyGreeter = new Greeter {
  override def greet(): Unit = {
    println("Hello and welcome!")
  }
}

friendlyGreeter.greet()

五、综合案例

这个案例将综合运用本节所学的所有知识点

代码实现:

// 1. 定义一个抽象基类
abstract class Animal(val name: String) {
  // 抽象方法,子类必须实现
  def makeSound(): String
  
  // 普通方法
  def info(): Unit = {
    println(s"I am an animal named $name, I make a sound like: ${makeSound()}")
  }
}

// 2. 定义具体的子类
final class Lion(name: String) extends Animal(name) {
  override def makeSound(): String = "Roar!"
  
  // Lion 特有的方法
  def hunt(): Unit = println(s"$name is hunting.")
}

class Seal(name: String) extends Animal(name) {
  override def makeSound(): String = "Arf! Arf!"
}

// 3. 在主程序中使用
object Zoo extends App {
  val leo = new Lion("Leo")
  val happy = new Seal("Happy")
  
  // 4. 使用匿名内部类创建一个特殊的动物
  val tweety = new Animal("Tweety") {
    override def makeSound(): String = "Tweet!"
  }
  
  // 5. 将不同子类对象放入父类类型的集合中 (多态)
  val animals: List[Animal] = List(leo, happy, tweety)
  
  // 遍历集合并调用通用方法
  for (animal <- animals) {
    animal.info()
    
    // 6. 类型判断与转换,调用子类特有方法
    if (animal.isInstanceOf[Lion]) {
      val lion = animal.asInstanceOf[Lion]
      lion.hunt()
    }
    println("---")
  }
}

练习题

题目一:简单继承
创建一个 Vehicle 类,包含一个 speed 成员变量。再创建一个 Car 类,继承自 Vehicle,并添加一个 brand 成员变量。

题目二:方法重写
Vehicle 类添加一个 describe() 方法,打印 “This is a generic vehicle.”。在 Car 类中重写 describe() 方法,使其打印 “This is a car of brand [brand].”。

题目三:调用父类方法
修改 Car 类重写的 describe() 方法,使其在打印自己的信息前,首先调用父类的 describe() 方法。

题目四:final 关键字
如何修改 Car 类的定义,使其不能被任何其他类继承?

题目五:抽象类定义
定义一个抽象类 Shape,包含一个抽象方法 perimeter(): Double (计算周长) 和一个抽象字段 color: String

题目六:实现抽象类
创建一个 Square 类,继承自 Shape。它需要一个 side (边长) 作为构造器参数,并实现 perimeter 方法和 color 字段 (假设颜色固定为 “Red”)。

题目七:类型判断 isInstanceOf
创建一个 Square 实例和一个 Circle 实例 (假设 Circle 也继承自 Shape),将它们都赋值给 Shape 类型的变量。然后编写代码判断哪个变量实际上是 Square 的实例。

题目八:类型转换 asInstanceOf
在上一题的基础上,对于那个被确认为 Square 实例的变量,将其安全地转换Square 类型,并调用一个 Square 特有的方法 (例如 diagonal(),计算对角线,你需要自己添加这个方法)。

题目九:匿名内部类
创建一个 Shape匿名子类实例,用于表示一个边长为 5 的等边三角形。你需要即时实现perimeter 方法和 color 字段。

题目十:构造器与继承
创建一个父类 Person(val name: String) 和一个子类 Worker(name: String, val salary: Double) extends Person(name)。子类的构造器是如何将 name 参数传递给父类构造器的?

题目十一:受保护成员 (protected)
Person 类中添加一个 protected var age: Int。在 Worker 子类中添加一个方法 setAge(newAge: Int),证明子类可以访问并修改父类的 protected 成员。

题目十二:final 方法
Person 类中添加一个 final def getIdentity(): String = "A human being"。尝试在 Worker 子类中重写 getIdentity() 方法,观察会发生什么。

题目十三:类型判断的陷阱
如果一个对象 objCar 的实例,那么 obj.isInstanceOf[Vehicle] 的结果是 true 还是 false?为什么?

题目十四:多态的应用
创建一个 List[Shape],其中包含一个 Square 实例和一个 Triangle (可以是匿名类) 实例。遍历这个列表,并对每个元素调用 perimeter() 方法,打印其周长。

题目十五:综合案例扩展
Zoo 案例的 for 循环中,添加一个 else if 分支,用于判断 animal 是否是 Seal 的实例,如果是,则调用一个 Seal 特有的方法,例如 swim() (你需要为 Seal 类添加此方法)。

答案与解析

答案一:

class Vehicle {
  var speed: Double = 0.0
}
class Car(val brand: String) extends Vehicle

解析: extends 关键字用于建立继承关系

答案二:

class Vehicle {
  var speed: Double = 0.0
  def describe(): Unit = println("This is a generic vehicle.")
}
class Car(val brand: String) extends Vehicle {
  override def describe(): Unit = println(s"This is a car of brand $brand.")
}

解析: 子类重写父类方法必须使用 override 关键字

答案三:

// ... Vehicle 类定义 ...
class Car(val brand: String) extends Vehicle {
  override def describe(): Unit = {
    super.describe() // 调用父类方法
    println(s"This is a car of brand $brand.")
  }
}

解析: super.methodName() 用于在子类中调用父类的同名方法。

答案四:

final class Car(val brand: String) extends Vehicle { ... }

解析:class 关键字前加上 final 可以阻止该类被继承。

答案五:

abstract class Shape {
  val color: String
  def perimeter(): Double
}

解析: 抽象方法和抽象字段都不需要实现或赋初始值。

答案六:

class Square(val side: Double) extends Shape {
  override val color: String = "Red"
  override def perimeter(): Double = side * 4
}

解析: 继承抽象类的子类必须使用 override 关键字实现所有抽象成员。

答案七:

val shape1: Shape = new Square(5)
// class Circle(val radius: Double) extends Shape { ... } // 假设 Circle 已定义
val shape2: Shape = new Circle(3)

if (shape1.isInstanceOf[Square]) {
  println("shape1 is a Square.")
}
if (shape2.isInstanceOf[Square]) {
  println("shape2 is a Square.")
} else {
  println("shape2 is not a Square.")
}

解析: isInstanceOf[Type] 用于检查对象的运行时类型。

答案八:

// 首先为 Square 添加 diagonal 方法
// class Square(val side: Double) extends Shape { ... def diagonal(): Double = Math.sqrt(2) * side ... }

val shape1: Shape = new Square(5)
if (shape1.isInstanceOf[Square]) {
  val sq = shape1.asInstanceOf[Square]
  println(s"The diagonal is: ${sq.diagonal()}")
}

解析: 必须先通过 isInstanceOf 检查,再用 asInstanceOf 转换,这是安全的类型转换模式。

答案九:

val triangle = new Shape {
  override val color: String = "Green"
  override def perimeter(): Double = 5 * 3
}
println(s"A ${triangle.color} triangle with perimeter ${triangle.perimeter()}")

解析: new Shape { ... } 创建了一个 Shape 的匿名子类实例,并当场实现了其抽象成员。

答案十:
子类的构造器通过 extends Person(name) 这种语法将参数 name 传递给了父类 Person 的主构造器。

答案十一:

class Person(val name: String) {
  protected var age: Int = 0
}
class Worker(name: String, val salary: Double) extends Person(name) {
  def setAge(newAge: Int): Unit = {
    // 可以访问和修改父类的 protected 成员
    this.age = newAge
  }
  def getAge(): Int = this.age
}
val worker = new Worker("John", 50000)
worker.setAge(35)
println(worker.getAge()) // 输出 35

解析: protected 成员对子类是可见的。

答案十二:
Worker 子类中尝试重写 getIdentity() 方法会导致编译错误
解析: final 关键字修饰的方法不能被子类重写。

答案十三:
结果是 true
解析: isInstanceOf 检查的是“is a”关系。因为 CarVehicle 的一个子类,所以一个 Car 的实例也是一个 Vehicle 的实例。

答案十四:

val shapes: List[Shape] = List(new Square(4), new Shape {
  override val color: String = "Blue"; override def perimeter(): Double = 3 + 4 + 5
})

for (shape <- shapes) {
  println(s"A ${shape.color} shape with perimeter: ${shape.perimeter()}")
}

解析: 这是多态的体现。尽管 shapes 列表的类型是 List[Shape],但当调用 shape.perimeter() 时,程序会自动调用每个对象实际类型 (Square 或匿名类) 的 perimeter 方法实现。

答案十五:

// 首先为 Seal 类添加 swim 方法
// class Seal(name: String) extends Animal(name) { ... def swim(): Unit = println(s"$name is swimming.") ... }

// 在 Zoo 对象的 for 循环中修改
for (animal <- animals) {
  animal.info()
  
  if (animal.isInstanceOf[Lion]) {
    val lion = animal.asInstanceOf[Lion]
    lion.hunt()
  } else if (animal.isInstanceOf[Seal]) { // 添加的分支
    val seal = animal.asInstanceOf[Seal]
    seal.swim()
  }
  println("---")
}

解析: if-else if 结构是处理多种不同子类类型的常用方式。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。