23.Kotlin 继承:继承的细节:覆盖方法与属性

希望帮你在Kotlin进阶路上少走弯路,在技术上稳步提升。当然,由于个人知识储备有限,笔记中难免存在疏漏或表述不当的地方,也非常欢迎大家提出宝贵意见,一起交流进步。 ------ Android_小雨

整体目录:Kotlin 进阶不迷路:41 个核心知识点,构建完整知识体系

一、前言

在面向对象编程(OOP)中,继承是实现代码复用和多态性的基石。然而,相较于 Java 宽松的继承机制,Kotlin 引入了更为严格的"显式声明"哲学。

1.1 覆盖的核心价值

子类通过覆盖(Override)父类成员,不仅能够继承父类的能力,更能定制化扩展父类行为。这是实现多态特性的关键:同一个方法调用,在不同的子类实例上表现出不同的行为。

1.2 Kotlin 的设计哲学

Kotlin 遵循 Effective Java 中的名言:"Design and document for inheritance or else prohibit it "(要么为继承做设计并文档化,要么就禁止它)。 因此,Kotlin 的类和成员默认是 final 的。这种设计规避了"脆弱基类"问题,防止开发者在无意中覆盖了父类的重要逻辑,从而提高了代码的安全性和可预测性。

二、覆盖的前置条件:Open 关键字

要实现覆盖,必须打破 Kotlin 默认的"封闭"状态,满足"双重开放"条件:

2.1 类的可继承性

父类必须用 open 修饰。

  • 注意 :抽象类(abstract class)和接口(interface)默认是开放的,无需显式添加 open
  • 禁止场景 :未加 open 的普通类默认为 final,不可被继承。

2.2 成员的可覆盖性

父类的方法或属性必须用 open 修饰,子类才能通过 override 关键字进行覆盖。 以下成员不可覆盖

  • open 修饰的普通成员(隐式 final)。
  • 顶层函数/属性、局部函数/属性。
  • private 成员(对子类不可见)。

2.3 基础示例

kotlin 复制代码
// 1. 类必须是 open 的
open class Vehicle {
    // 2. 属性必须是 open 的
    open val maxSpeed: Int = 120

    // 3. 方法必须是 open 的
    open fun run() {
        println("车辆以 $maxSpeed km/h 行驶")
    }
}

三、方法覆盖(Method Override):规则与实战

3.1 基本语法

Kotlin 强制要求使用 override 关键字,这与 Java 中可选的 @Override 注解有着本质区别。

kotlin 复制代码
class Car : Vehicle() {
    // 显式 override,否则编译报错
    override fun run() {
        println("汽车以 $maxSpeed km/h 匀速行驶")
    }
}

// 多态调用
fun main() {
    val vehicle: Vehicle = Car()
    vehicle.run() // 输出:汽车以 120 km/h 匀速行驶
}

3.2 方法覆盖的核心规则

3.2.1 签名完全兼容

  • 参数:数量、类型、顺序必须严格一致。
  • 返回值协变(Covariant) :子类重写方法时,返回值类型可以是父类方法返回值类型的子类型
kotlin 复制代码
open class Transport
class Train : Transport()

open class Logistics {
    open fun getTransport(): Transport = Transport()
}

class RailwayLogistics : Logistics() {
    // 合法:返回 Train 是 Transport 的子类
    override fun getTransport(): Train = Train()
}

3.2.2 禁止"隐式覆盖"( override 不可省略)

如果不写 override,编译器会直接报错。这一机制明确了开发者的意图,防止因拼写错误或无意中定义了同名方法而导致的逻辑 Bug。

3.2.3 控制后续覆盖权限(Final Override)

子类覆盖后的方法默认依然是 open 的。如果希望继承链到此为止,禁止后续子类继续修改逻辑,可以使用 final override

kotlin 复制代码
class SportCar : Car() {
    // 此方法已被锁定,SportCar 的子类无法再重写 run
    final override fun run() {
        println("跑车以 200 km/h 高速行驶")
    }
}

3.2.4 抽象方法的覆盖

抽象方法默认是 open 的,非抽象子类必须覆盖它们。

kotlin 复制代码
abstract class AbstractMachine {
    abstract fun start()
}

class FactoryMachine : AbstractMachine() {
    override fun start() { // 必须实现
        println("工厂机器启动")
    }
}

3.3 实战技巧:复用与多态(复用父类逻辑(super 关键字))

在重写逻辑时,通常需要保留父类的基础行为,此时使用 super 关键字。

kotlin 复制代码
class Bus : Vehicle() {
    override fun run() {
        super.run() // 复用父类逻辑:输出基础行驶信息
        println("公交车即将停靠站点") // 扩展子类特有逻辑
    }
}

四、属性覆盖(Property Override):特殊规则与陷阱

属性覆盖是 Kotlin 的一大特色(Java 中字段不能被重写)。其核心在于保持类型兼容可变性合理

4.1 基本语法

kotlin 复制代码
class Bicycle : Vehicle() {
    // 覆盖属性,语法与方法类似
    override val maxSpeed: Int = 40
}

4.2 核心规则

4.2.1 可变性兼容规则(关键)

子类在覆盖属性时,可以扩展属性的可变性,但不能限制它。

父类属性 子类覆盖允许 结果 说明
val (只读) val (只读) ✅ 合法 保持只读特性
val (只读) var (读写) ✅ 合法 扩展为可变,兼容父类的读取契约
var (读写) val (只读) 禁止 破坏了父类的"写入"契约
var (读写) var (读写) ✅ 合法 保持读写特性

示例:Var 覆盖 Val

kotlin 复制代码
open class Product {
    open val price: Double = 100.0
}

class MutableProduct : Product() {
    override var price: Double = 120.0 // 合法:子类实例可以修改价格
}

4.2.2 覆盖的三种实现方式

  1. 直接赋值:赋一个新的常量值。

    kotlin 复制代码
    class ElectricCar : Vehicle() {
        override val maxSpeed: Int = 150 // 直接重新赋值
    }
  2. 自定义 Getter (计算属性):不占用内存字段,每次访问时动态计算。

    kotlin 复制代码
    class Truck : Vehicle() {
        override val maxSpeed: Int get() = 90 // 计算属性(无字段存储)
    }
    
    // 带依赖的计算属性
    class LoadedTruck : Vehicle() {
        var loadWeight: Int = 0
        override val maxSpeed: Int get() = 120 - (loadWeight / 100) // 载重越大,最高速越低
    }
  3. 抽象属性的覆盖:抽象类的抽象属性隐式 open,子类需用 override 提供实现(赋值或计算属性)。

kotlin 复制代码
abstract class Device {
    abstract val brand: String // 抽象属性,无实现
}

class Phone : Device() {
    override val brand: String = "Apple" // 赋值实现
}

class Tablet : Device() {
    override val brand: String get() = "Samsung" // 计算属性实现
}

4.3 ⚠️ 致命陷阱

这是 Kotlin 继承中最容易踩的坑。父类构造函数在子类属性初始化之前执行。

4.3.1初始化顺序陷阱

子类覆盖属性的初始化在「父类构造函数执行之后」,避免在父类构造中依赖子类覆盖属性:

kotlin 复制代码
open class Parent {
    open val value: Int = 10
    init {
        println("Parent init: value = $value") // 输出:10(子类覆盖属性未初始化)
    }
}

class Child : Parent() {
    override val value: Int = 20
    init {
        println("Child init: value = $value") // 输出:20(覆盖属性已初始化)
    }
}

val child = Child() // 执行流程:Parent 构造 → Child 覆盖属性初始化 → Child 构造

4.3.2 覆盖 var 属性的 getter/setter

可单独覆盖 var 属性的 getter 或 setter,保留另一部分逻辑:

kotlin 复制代码
open class User {
    open var name: String = "Unknown"
}

class NicknamedUser : User() {
    override var name: String
        get() = super.name + "_nickname" // 覆盖 getter
        set(value) { super.name = value.trim() } // 覆盖 setter
}

val user = NicknamedUser()
user.name = "  Tom  "
println(user.name) // 输出:Tom_nickname

五、覆盖冲突解决(多实现场景)

当类继承了父类并实现了多个接口,且这些父类型中存在签名相同的成员时,编译器无法自动决策,必须由开发者显式解决冲突。

5.1 接口多实现的冲突

多个接口有同名方法时,使用 super<T> 语法指定调用哪个接口的实现。

kotlin 复制代码
interface Walkable {
    fun move() = println("步行")
}

interface Flyable {
    fun move() = println("飞行")
}

class Bird : Walkable, Flyable {
    // 编译器报错,必须显式 override
    override fun move() {
        super<Walkable>.move() // 调用 Walkable 的逻辑
        super<Flyable>.move()  // 调用 Flyable 的逻辑
        println("鸟类扑腾着翅膀")
    }
}

5.2 类继承 + 接口实现的冲突(属性冲突解决)

父类成员优先级高于接口成员。若需调用接口的实现,需通过 super<接口名>.成员() 显式指定:

kotlin 复制代码
open class Animal {
    open fun eat() {
        println("动物进食")
    }
}

interface Herbivore {
    fun eat() {
        println("食草动物吃植物")
    }
}

class Deer : Animal(), Herbivore {
    override fun eat() {
        super<Animal>.eat() // 显式调用父类方法(也可直接 super.eat())
        super<Herbivore>.eat() // 显式调用接口方法
    }
}

val deer = Deer()
deer.eat() // 输出:动物进食 → 食草动物吃植物

六、使用注意事项与避坑指南

  1. 禁止覆盖 private 成员 : 父类 private 成员对子类不可见。若子类定义了一个同名函数,这不是覆盖,而是一个全新的、与父类无关的方法。

  2. 避免过度覆盖: 不要为了使用继承而继承。仅在需要修改/扩展父类行为时进行覆盖。如果只是为了复用代码,优先考虑**组合(Composition)**而非继承。

  3. 谨慎修改可变性 : 虽然用 var 覆盖 val 在语法上是允许的,但要确保这样做符合业务逻辑,防止外部意外修改了本该是常量的属性。

  4. 多态场景下的类型转换 : 父类引用调用覆盖成员时,JVM 会自动分发到子类的实现,无需进行类型强转即可执行子类逻辑。

  5. 异常声明: Kotlin 没有 Checked Exception(受检异常),但覆盖方法时,子类抛出的运行时异常范围不应违背父类的设计契约(遵循 Liskov 替换原则)。

  6. 接口抽象成员与父类成员冲突

    优先覆盖父类成员,若需使用接口逻辑,通过 super<接口名> 调用

  7. 覆盖方法的异常声明

    子类覆盖方法不能抛出比父类更多的 checked 异常(Kotlin 无 checked 异常,但需注意运行时异常的合理性)

七、总结与最佳实践

7.1 核心知识点回顾

  • 覆盖前提:父类 open + 成员 open,子类显式 override
  • 方法覆盖:签名兼容、支持协变返回值、可通过 final 禁止后续覆盖
  • 属性覆盖:支持 val→var 扩展、三种实现方式、注意初始化顺序
  • 冲突解决:多实现同名成员需显式 override,用 super<类型> 复用指定实现

7.2 最佳实践

  • 显式化原则:open 和 override 关键字不可省略,提升代码可读性
  • 最小修改原则:覆盖父类成员时,尽量复用 super 逻辑,仅补充必要的子类差异
  • 兼容性原则:覆盖后的成员需保持与父类的兼容性(不缩小参数范围、不丢失功能)
  • 单一职责原则:避免为了覆盖而覆盖,确保覆盖行为符合子类的业务职责
  • 冲突处理原则:多实现冲突时,优先保留核心逻辑,合理复用不同接口 / 父类的实现

7.3 Kotlin vs Java 覆盖核心区别

对比维度 Kotlin 覆盖 Java 覆盖
显式声明 必须override 可选 @Override
默认状态 默认 final (需 open) 默认可覆盖 (需 final 禁止)
属性覆盖 支持 (val -> var, Getter) 不支持 (仅字段隐藏)
返回值 支持协变 支持协变

8.全文总结

为了方便记忆 Kotlin 的覆盖规则,我们总结为:

  • 1 个关键字override 是强制且核心的。
  • 2 个前提 :父类必须 open + 成员必须 open
  • 3 种属性覆盖 :直接赋值、自定义 Getter、var 覆盖 val(扩展可变性)。
  • 4 大核心规则
    1. 签名必须严格匹配。
    2. 返回值支持协变(子类类型)。
    3. 初始化陷阱(父类构造先于子类属性)。
    4. 冲突解决需指明 super<Type>
相关推荐
泥嚎泥嚎38 分钟前
【Android】RecyclerView 刷新方式全解析:从 notifyDataSetChanged 到 DiffUtil
android·java
Haha_bj42 分钟前
五、Kotlin——条件控制、循环控制
android·kotlin
弥巷43 分钟前
【Android】深入理解Window和WindowManager
android·java
未来之窗软件服务1 小时前
操作系统应用(三十六)golang语言ER实体图开发—东方仙盟筑基期
后端·golang·mermaid·仙盟创梦ide·东方仙盟·操作系统应用
user_永1 小时前
Maven 发包
后端
幌才_loong1 小时前
.NET8 牵手 Log4Net:日志界 “最佳 CP” 出道,调试再也不秃头!
后端
武子康1 小时前
大数据-173 Elasticsearch 映射与文档增删改查实战(基于 7.x/8.x)JSON
大数据·后端·elasticsearch
卡皮巴拉_1 小时前
日志系统最佳实践:我如何用 ELK + Filebeat 做“秒级可观测”
后端
AllBlue1 小时前
安卓调用unity中的方法
android·unity·游戏引擎