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 没有 valvar,因为它只是用来传给父类构造函数的参数,不是子类的属性。我在项目中见过有人在这加了个 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