10、继承与多态:继承、方法重写、属性重写、抽象类与接口、多态
面向对象编程里,继承和多态是绕不开的两座大山。说实话,我刚学 Kotlin 那会儿,觉得继承不就是「儿子用老子的东西」嘛,多简单。结果真到项目里写起来,各种坑踩得我头皮发麻。今天咱们就把这块彻底捋清楚。
核心一句话:继承是为了复用,多态是为了扩展。两者结合,才能写出真正灵活、可维护的代码。
10.1 继承:从父类到子类
Kotlin 里,所有类默认都是 final 的。什么意思?就是你不能随便继承一个类。这跟 Java 不一样,Java 默认是开放的。我个人习惯是,除非明确要设计成可继承的,否则一律不加 open。这样能避免很多意想不到的副作用。
// 父类必须用 open 标记
open class Animal(val name: String) {
fun eat() {
println("$name 正在吃饭")
}
}
// 子类继承父类
class Dog(name: String, val breed: String) : Animal(name) {
fun bark() {
println("$name 汪汪叫")
}
}
fun main() {
val dog = Dog("旺财", "金毛")
dog.eat() // 继承自父类
dog.bark() // 自己的方法
}
这里有个细节:子类构造函数里的 name 没有 val 或 var,因为它只是用来传给父类构造函数的参数,不是子类的属性。我在项目中见过有人在这加了个 val,结果父类和子类各有一个 name,调试的时候差点没把我绕晕。
小技巧:子类构造函数参数如果只是传递给父类,不要加 val/var。否则会创建同名字段,容易混淆。
10.2 方法重写:子类重新定义行为
子类不满意父类的某个方法,想自己改一版,这就叫重写。Kotlin 里,父类方法必须加 open,子类方法必须加 override。少一个都不行。
open class Animal(val name: String) {
open fun makeSound() {
println("$name 发出声音")
}
}
class Dog(name: String) : Animal(name) {
override fun makeSound() {
println("$name 汪汪叫")
}
}
class Cat(name: String) : Animal(name) {
override fun makeSound() {
println("$name 喵喵叫")
}
}
你想想看,如果没有重写,所有动物都只会「发出声音」,那多没意思。重写让每个子类有了自己的个性。
注意:Kotlin 里重写的方法默认也是 open 的。如果你不想让子类继续重写,可以加 final 关键字。比如 final override fun makeSound()。
10.3 属性重写:不只是方法,属性也能重写
嗯,这里要注意。Kotlin 允许重写属性,但前提是父类属性用 open 标记。子类用 override 来覆盖。
open class Animal {
open val sound: String = "未知声音"
}
class Dog : Animal() {
override val sound: String = "汪汪"
}
class Cat : Animal() {
override val sound: String = "喵喵"
}
我曾经在项目中用属性重写来配置不同子类的默认值,比用方法返回字符串要简洁得多。但要注意,属性重写不能改变类型,只能覆盖值或计算方式。
10.4 抽象类与接口:设计的两把利器
抽象类和接口,说白了都是用来定义「契约」的。但它们的侧重点不同。
| 特性 | 抽象类 | 接口 |
|---|---|---|
| 构造函数 | 可以有 | 不能有 |
| 属性状态 | 可以有状态(字段) | 不能有状态(只能声明属性) |
| 方法实现 | 可以有抽象方法和具体方法 | 可以有抽象方法和默认方法 |
| 多继承 | 只能单继承 | 可以多实现 |
// 抽象类
abstract class Vehicle(val brand: String) {
abstract fun drive()
fun stop() {
println("$brand 已停止")
}
}
// 接口
interface Electric {
fun charge()
fun batteryLevel(): Int = 0 // 默认实现
}
interface GPS {
fun navigate(destination: String)
}
// 一个类可以同时继承抽象类和实现多个接口
class Tesla(brand: String) : Vehicle(brand), Electric, GPS {
override fun drive() {
println("$brand 正在自动驾驶")
}
override fun charge() {
println("$brand 正在充电")
}
override fun navigate(destination: String) {
println("导航到 $destination")
}
}
我个人习惯是:能用接口解决的问题,尽量不用抽象类。因为接口更灵活,一个类可以实现多个接口。但如果你需要共享状态(比如构造函数参数),那就用抽象类。
10.5 多态:同一个接口,不同的实现
多态,说白了就是「父类引用指向子类对象」。你写代码的时候用的是父类类型,但实际跑起来调用的是子类的方法。这就是多态的威力。
fun main() {
val animals: List<Animal> = listOf(Dog("旺财"), Cat("咪咪"))
for (animal in animals) {
animal.makeSound() // 实际调用的是子类的方法
}
// 输出:
// 旺财 汪汪叫
// 咪咪 喵喵叫
}
为什么会这样?因为 Kotlin 在运行时才确定具体调用哪个方法。这叫「动态绑定」。我在项目中用这个特性写过一套消息推送系统,不同的推送渠道(短信、邮件、App推送)都继承同一个抽象类,调用方完全不用关心具体实现,新增渠道只需要加一个子类就行。
多态的核心价值:面向接口编程,而不是面向实现编程。这样你的代码才能「对扩展开放,对修改关闭」。
10.6 知识体系总览
下面这张图把本章的核心知识点串起来了。你看一遍,心里就有谱了。
10.7 避坑指南
最后分享几个我踩过的坑,你遇到了能少走弯路。
- 继承层次不要太深。我曾经接手过一个项目,继承链有6层,改一个父类方法,下面5层全得跟着改。后来我重构了,能用组合就用组合,能用接口就用接口。
- 重写方法时记得调 super。有时候你重写方法是想在父类行为基础上加点东西,结果忘了调
super,父类的逻辑全丢了。我犯过这个错,排查了半天。 - 接口默认方法冲突。如果一个类实现了两个接口,两个接口有同名默认方法,必须手动重写解决冲突。Kotlin 会强制你这么做,别想着蒙混过关。
- 属性重写不能改变类型。父类是
String,子类不能重写成Int。这个编译器会报错,但新手容易不理解为什么。
我的建议:写代码前先画个类图,理清楚继承和接口的关系。别上来就写,写到一半发现设计不合理,改起来很痛苦。
好了,继承与多态这块就聊到这儿。记住一句话:继承是为了复用,多态是为了扩展。把这两个概念吃透了,你的面向对象才算真正入门。
公众号:蓝海资料掘金营,微信deep3321