希望帮你在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 覆盖的三种实现方式
-
直接赋值:赋一个新的常量值。
kotlinclass ElectricCar : Vehicle() { override val maxSpeed: Int = 150 // 直接重新赋值 } -
自定义 Getter (计算属性):不占用内存字段,每次访问时动态计算。
kotlinclass Truck : Vehicle() { override val maxSpeed: Int get() = 90 // 计算属性(无字段存储) } // 带依赖的计算属性 class LoadedTruck : Vehicle() { var loadWeight: Int = 0 override val maxSpeed: Int get() = 120 - (loadWeight / 100) // 载重越大,最高速越低 } -
抽象属性的覆盖:抽象类的抽象属性隐式 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() // 输出:动物进食 → 食草动物吃植物
六、使用注意事项与避坑指南
-
禁止覆盖 private 成员 : 父类
private成员对子类不可见。若子类定义了一个同名函数,这不是覆盖,而是一个全新的、与父类无关的方法。 -
避免过度覆盖: 不要为了使用继承而继承。仅在需要修改/扩展父类行为时进行覆盖。如果只是为了复用代码,优先考虑**组合(Composition)**而非继承。
-
谨慎修改可变性 : 虽然用
var覆盖val在语法上是允许的,但要确保这样做符合业务逻辑,防止外部意外修改了本该是常量的属性。 -
多态场景下的类型转换 : 父类引用调用覆盖成员时,JVM 会自动分发到子类的实现,无需进行类型强转即可执行子类逻辑。
-
异常声明: Kotlin 没有 Checked Exception(受检异常),但覆盖方法时,子类抛出的运行时异常范围不应违背父类的设计契约(遵循 Liskov 替换原则)。
-
接口抽象成员与父类成员冲突
优先覆盖父类成员,若需使用接口逻辑,通过 super<接口名> 调用
-
覆盖方法的异常声明
子类覆盖方法不能抛出比父类更多的 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 大核心规则 :
- 签名必须严格匹配。
- 返回值支持协变(子类类型)。
- 初始化陷阱(父类构造先于子类属性)。
- 冲突解决需指明
super<Type>。