24.Kotlin 继承:调用超类实现 (super)

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

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

一、前言

在面向对象设计中,子类往往不是凭空产生的,而是在父类或接口的基础上进行"增量修改"。Kotlin 的 super 关键字正是连接子类与超类(Superclass/Superinterface)的桥梁。

1.1 super 的核心定位

super 的核心作用是复用。它允许子类在覆盖(Override)成员时,依然能够访问超类中被定义的原始逻辑,从而避免代码重复,实现"在原有基础上扩展"而非"推倒重来"。

1.2 Kotlin 的设计价值

与 Java 相比,Kotlin 的 super 更加强大且精准。特别是在多重实现(实现多个接口)的场景下,Kotlin 提供了泛型限定符语法(super<T>),彻底解决了"菱形继承"带来的调用歧义问题。

二、基础用法:调用超类构造函数

子类对象的初始化必须包含父类状态的初始化。因此,子类构造函数必须通过 super 调用父类构造函数。

2.1 主构造函数中调用(隐式 super)

当子类定义了主构造函数时,必须在继承列表中直接调用父类构造函数。

  • 核心规则 :父类构造所需的参数,需从子类主构造函数的参数中传递过去。此处没有显式的 super 关键字,但编译后的字节码中包含 super(...) 调用。
kotlin 复制代码
// 父类:带主构造函数
open class Person(val name: String, val age: Int) {
    init {
        println("Person 初始化:name=$name, age=$age")
    }
}

// 子类:主构造函数中直接调用父类主构造
// 这里的 : Person(name, age) 即隐式 super 调用
class Student(name: String, age: Int, val studentId: String) : Person(name, age) {
    init {
        println("Student 初始化:studentId=$studentId")
    }
}

// 使用
val s = Student("Tom", 20, "S1001")
// 输出顺序:
// Person 初始化...
// Student 初始化...

2.2 次构造函数中调用(显式 super)

如果子类没有主构造函数,所有的次构造函数(Secondary Constructor)都必须显式初始化父类。

  • 方式 A :使用 super(...) 直接调用父类构造。
  • 方式 B :使用 this(...) 调用子类其他构造函数(最终委托给父类)。
kotlin 复制代码
open class Animal(val type: String) {
    constructor(type: String, name: String) : this(type) {
        println("Animal 次构造:name=$name")
    }
}

class Dog : Animal {
    // 方式 A:直接通过 super 调用父类构造
    constructor(name: String) : super("Dog", name) {
        println("Dog 次构造:name=$name")
    }

    // 方式 B:通过 this 委托给自己的第一个构造函数(间接调用 super)
    constructor(name: String, age: Int) : this(name) {
        println("Dog 次构造:age=$age")
    }
}

2.3 特殊说明

  • 抽象类:抽象类可以有构造函数,子类必须调用。
  • 接口 :接口没有 构造函数(这是接口与抽象类的核心区别之一),因此子类不能通过 super 调用接口。

三、核心用法:调用超类方法

这是最常见的场景:子类重写了父类方法,但在新逻辑中需要包含父类的原有逻辑。

3.1 基本语法

仅在被 override 修饰的方法内部,使用 super.方法名() 调用。

kotlin 复制代码
open class Shape {
    open fun draw() {
        println(">>> 准备画笔与画布 (基础逻辑)")
    }
}

class Circle : Shape() {
    override fun draw() {
        super.draw() // 1. 复用父类基础逻辑
        println(">>> 绘制圆形 (子类扩展逻辑)") // 2. 执行子类逻辑
    }
}

3.2 关键规则

  1. 签名一致:调用的父类方法签名(参数、返回值)需与当前方法一致。
  2. 访问限制 :只能调用父类中 open 的方法,无法调用 private 方法。
  3. Final 限制 :如果父类方法是 final(默认),子类无法覆盖,自然也就不存在"在覆盖中调用 super"的场景(可以直接调用,但不是 override 语义)。

四、特殊用法:调用超类属性

Kotlin 的属性不仅仅是字段,更是 Getter 和 Setter。super 同样可以用于访问父类属性的访问器逻辑。

4.1 调用父类 Getter

当子类覆盖父类的属性时,若需要复用父类属性的getter逻辑,可通过"super.属性名"的方式调用父类属性的getter方法,获取父类计算后的值,再基于该值进行子类的扩展计算。

kotlin 复制代码
open class Product {
    // 父类属性(open修饰允许被覆盖)
    open val price: Double get() = 100.0
    // 父类属性:基于price计算折扣价
    open val discountPrice: Double get() = price * 0.9
}

class VIPProduct : Product() {
    // 覆盖父类price属性:VIP商品原价更高
    override val price: Double get() = 150.0
    // 覆盖discountPrice属性:复用父类折扣逻辑,再追加VIP折扣
    override val discountPrice: Double get() {
        val baseDiscount = super.discountPrice // 调用父类discountPrice的getter
        return baseDiscount * 0.8 // VIP额外8折
    }
}

// 使用
val vipProduct = VIPProduct()
println(vipProduct.discountPrice) // 计算过程:150.0 * 0.9(父类逻辑)* 0.8(子类逻辑)= 108.0 → 输出:108.0

从计算过程可以看出,子类VIPProduct的discountPrice属性首先通过super.discountPrice获取父类计算的折扣价(150.0 * 0.9 = 135.0),再乘以0.8得到VIP专属折扣价,实现了属性逻辑的复用与扩展。

4.2 调用父类 Setter (仅 var)

对于用var修饰的可变属性(有setter方法),子类在覆盖该属性时,可通过"super.属性名 = 值"的方式调用父类属性的setter方法,复用父类的属性赋值逻辑(如数据校验、格式处理等),再补充子类的自定义逻辑。需要注意的是,val修饰的只读属性没有setter方法,无法通过super调用。

kotlin 复制代码
open class User {
    // 父类var属性:带自定义setter
    open var username: String = "unknown"
        set(value) {
            field = value.trim() // 父类setter逻辑:去除首尾空格
        }
}

class AdminUser : User() {
    // 覆盖父类username属性
    override var username: String = "admin"
        set(value) {
            if (value.isNotEmpty()) {
                super.username = value // 调用父类setter,复用空格处理逻辑
            } else {
                field = "default_admin" // 子类自定义逻辑:空值时设置默认值
            }
        }
}

// 使用
val admin = AdminUser()
admin.username = "  super_admin  "
println(admin.username) // 输出:super_admin(父类setter处理后去除了空格)

admin.username = ""
println(admin.username) // 输出:default_admin(子类自定义逻辑生效)

该示例中,当给AdminUser的username赋值时,若值非空,则通过super.username = value调用父类的setter方法去除首尾空格;若值为空,则执行子类自定义逻辑设置默认值,既复用了父类的校验逻辑,又满足了子类的特殊需求

4.3 抽象属性的 super 调用

抽象类中的抽象属性(用abstract修饰)没有默认的getter和setter实现,子类必须覆盖该属性并提供具体实现。由于抽象属性没有原始逻辑可复用,因此子类在覆盖抽象属性后,无法通过super调用该属性(编译时会报错)。

而对于接口中的属性,若该属性提供了默认的getter实现(Kotlin 1.0+支持),子类在覆盖该属性时,可以通过super调用接口属性的默认实现。需要注意的是,接口中的属性默认是abstract的,只有显式提供getter实现时才具有可复用的逻辑。

示例代码(接口属性的super调用):

kotlin 复制代码
interface DiscountAble {
    // 接口属性:带默认getter实现
    val discount: Double get() = 0.1
}

class Member : DiscountAble {
    // 覆盖接口属性
    override val discount: Double get() {
        // 调用接口属性的默认实现
        val baseDiscount = super.discount
        return baseDiscount + 0.05 // 会员额外增加5%折扣
    }
}

// 使用
val member = Member()
println(member.discount) // 输出:0.15

五、高级用法:多实现场景下的精准调用

这是 Kotlin 相比 Java 语法糖更甜的地方。当一个类实现了多个接口,或者继承类并实现接口,且它们拥有同名成员 时,编译器会强制要求覆盖。此时,你需要明确指定 super 指向哪一个超类。

语法格式

super<超类名>.成员名

5.1 接口多实现冲突

kotlin 复制代码
interface Swimable {
    fun move() = println("Swim: 划水") // 默认实现
}

interface Runable {
    fun move() = println("Run: 奔跑") // 默认实现
}

// 必须覆盖 move,否则编译报错:Inherited platform declarations clash
class Duck : Swimable, Runable {
    override fun move() {
        // 场景 A:保留两者的能力
        super<Swimable>.move()
        super<Runable>.move()
        println("Duck: 水陆两栖移动中...")
    }
}

5.2 父类与接口冲突

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

interface Herbivore {
    fun eat() = println("Herbivore: 吃草")
}

class Rabbit : Animal(), Herbivore {
    override fun eat() {
        // 场景 B:只选择其中一种实现
        super<Herbivore>.eat()
        println("Rabbit: 嚼胡萝卜")
    }
}

5.3 属性的多实现冲突

属性冲突的解决方式与方法完全一致。

kotlin 复制代码
interface A { val score: Int get() = 10 }
interface B { val score: Int get() = 20 }

class C : A, B {
    override val score: Int
        get() = super<A>.score + super<B>.score // 结果为 30
}

六、实战场景:super 的典型应用

6.1 "装饰器"模式:带缓存的网络请求

在不修改父类核心逻辑的前提下,通过 super 包装一层缓存功能。

kotlin 复制代码
open class ApiClient {
    open fun fetchData(url: String): String {
        println("--- 发起真实网络请求: $url ---")
        return "{data: 200}"
    }
}

class CachedApiClient : ApiClient() {
    private val cache = mutableMapOf<String, String>()

    override fun fetchData(url: String): String {
        // 1. 扩展:先查缓存
        return cache.getOrPut(url) {
            // 2. 复用:缓存未命中,才通过 super 调用父类真实请求
            super.fetchData(url)
        }
    }
}

6.2 协同工作:数据验证链

利用多接口实现,将不同维度的逻辑组合在一起。

kotlin 复制代码
interface Validator {
    fun process(input: String): Boolean {
        return input.isNotEmpty()
    }
}

interface Formatter {
    fun process(input: String): String {
        return input.uppercase()
    }
}

class DataHandler : Validator, Formatter {
    // 这里虽不是标准覆盖(返回值不同无法合并),但演示了逻辑组合
    fun handle(input: String) {
        // 调用接口 A 的逻辑
        if (super<Validator>.process(input)) {
            // 调用接口 B 的逻辑
            val result = super<Formatter>.process(input)
            println("Result: $result")
        }
    }
}

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

在使用super关键字时,若不注意初始化顺序、访问权限等细节,容易导致编译错误或运行时异常。下面总结了常见的注意事项和避坑点,帮助开发者规范使用super。

7.1 避免在父类构造中调用可覆盖的方法 / 属性

Kotlin的初始化顺序为:先执行父类的构造函数和init块,再执行子类的构造函数和init块。如果在父类构造中调用了可被子类覆盖的方法或属性,此时子类的成员尚未初始化,调用子类覆盖后的方法时,可能会访问到未初始化的成员,导致逻辑错误或空指针异常。

危险示例代码:

kotlin 复制代码
open class Parent {
    init {
        // 危险:在父类构造中调用可被子类覆盖的方法
        doSomething()
    }

    // 可被子类覆盖的方法
    open fun doSomething() {}
}

class Child : Parent() {
    // 子类成员:在父类构造执行时未初始化
    private val data = "child data"

    // 覆盖父类方法,访问子类成员data
    override fun doSomething() {
        println(data) // 运行时可能输出null或抛出异常(data未初始化)
    }
}

// 使用
val child = Child()
// 可能输出:null 或 抛出未初始化异常

避坑方案:父类构造中仅调用final修饰的方法或属性(不可被覆盖),或避免在构造中调用任何可能被覆盖的成员,将需要执行的逻辑延迟到初始化完成后调用(如通过init块的执行顺序控制,或提供专门的初始化方法供子类调用)。

7.2 多实现时不可省略限定符

当子类同时继承父类并实现多个接口,且存在同名的方法或属性时,必须使用super<限定符>明确指定超类来源,否则编译器无法确定调用哪个超类的实现,会直接报编译错误。

错误示例代码:

kotlin 复制代码
interface A {
    fun foo() { println("A.foo") }
}

interface B {
    fun foo() { println("B.foo") }
}

class C : A, B {
    override fun foo() {
        // 错误:未指定限定符,无法确定调用A还是B的foo方法
        super.foo()
    }
}

正确示例代码:

kotlin 复制代码
class C : A, B {
    override fun foo() {
        super<A>.foo() // 明确调用接口A的foo方法
        super<B>.foo() // 明确调用接口B的foo方法
    }
}

7.3 不可调用父类 private 成员

父类中用private修饰的方法或属性具有私有的访问权限,子类无法访问这些成员,因此也无法通过super调用。若子类需要复用父类的private逻辑,可将其修改为protected或public访问权限(protected仅允许子类访问,更安全)。

错误示例代码:

kotlin 复制代码
open class Parent {
    // 父类private方法
    private fun privateMethod() {
        println("父类私有方法")
    }

    // 父类protected方法
    protected fun protectedMethod() {
        println("父类保护方法")
    }
}

class Child : Parent() {
    fun test() {
        // 错误:无法访问父类private方法
        super.privateMethod()
        // 正确:可以访问父类protected方法
        super.protectedMethod()
    }
}

7.4 接口无构造函数

接口是行为的抽象,不包含状态信息,因此Kotlin的接口不允许定义构造函数。子类实现接口时,无需也不能通过super(参数)调用接口的构造函数,只能调用接口中具有默认实现的方法或属性。

错误示例代码:

kotlin 复制代码
interface MyInterface {
    // 错误:接口不能定义构造函数
    constructor(param: String)
}

class MyClass : MyInterface {
    // 错误:接口无构造函数,无法调用
    constructor() : super("param") {}
}

7.5 协变返回值场景的 super 调用

Kotlin支持方法返回值的协变,即子类覆盖父类方法时,返回值可以是父类返回值的子类类型。在这种场景下,通过super调用父类方法时,返回的是父类类型,若需要赋值给子类类型变量,需进行安全的类型转换。

示例代码:

kotlin 复制代码
open class Fruit
class Apple : Fruit()

open class FruitShop {
    // 父类方法:返回Fruit类型
    open fun getFruit(): Fruit {
        return Fruit()
    }
}

class AppleShop : FruitShop() {
    // 子类方法:返回Apple类型(Fruit的子类,协变返回值)
    override fun getFruit(): Apple {
        // 调用父类方法,返回Fruit类型
        val parentFruit = super.getFruit()
        // 若需要转换为Apple类型,需进行安全转换
        return parentFruit as? Apple ?: Apple()
    }
}

避坑方案:在协变返回值场景下,明确super调用返回的是父类类型,避免直接将其赋值给子类类型变量,需通过安全转换(as?)确保类型兼容,避免类型转换异常。

八、总结与最佳实践

8.1 核心知识点回顾

本文围绕Kotlin的super关键字,从基础到高级系统讲解了其使用方法,核心知识点总结如下:

  • 核心作用:super关键字的核心价值是复用超类(父类或接口)的构造函数、成员方法和属性的逻辑,避免重复编码,支持子类在复用基础上进行扩展。
  • 基础用法: 构造调用:主构造函数通过"父类名(参数)"隐式调用父类主构造,次构造函数通过super直接调用父类构造或通过this间接调用。
  • 方法调用:在子类覆盖的方法中,通过super.方法名()调用父类原始实现,实现逻辑复用与扩展。
  • 属性调用:通过super.属性名调用父类属性的getter/setter,复用属性逻辑(仅适用于open或override的属性)。

高级用法:在多实现场景下(父类+多接口),通过super<限定符>.成员的语法,精准指定超类来源,解决同名成员的调用歧义。

8.2 最佳实践

为了规范使用super关键字,提升代码质量和可维护性,推荐以下最佳实践:

  1. 最小复用原则:仅复用父类或接口的核心逻辑,子类聚焦于自身的差异化扩展。避免过度依赖super调用,防止子类与父类的耦合度过高,影响代码的灵活性。
  2. 明确调用原则:在多实现场景下,必须使用super<限定符>明确指定超类来源,即使当前只有一个超类提供实现,也建议加上限定符,提升代码的可读性和可维护性,避免后续扩展时出现歧义。
  3. 构造安全原则:严格避免在父类构造函数、init块中调用可被覆盖的方法或属性,防止因初始化顺序问题导致的未初始化异常。若需要执行初始化逻辑,可采用"延迟初始化"或"初始化方法"的方式。
  4. 接口协同原则:在实现多个接口时,充分利用super<接口名>复用接口的默认实现,将多个接口的功能进行协同组合,减少重复编码,实现"接口职责单一,子类组合复用"的设计目标。

8.3 核心记忆法则:"1-2-3-4"

为了方便记忆,我们将 super 的核心机制总结为:

  • 1 个核心目标复用 (Reuse)。super 的存在是为了在扩展的同时保留原有的基础能力。
  • 2 种构造调用 :主构造隐式 委托(class S : P()),次构造显式 调用(: super())。
  • 3 大使用场景
    1. 覆盖方法中复用旧逻辑 (super.func())。
    2. 属性访问器中扩展逻辑 (super.prop)。
    3. 多接口冲突中消除歧义 (super<T>.func())。
  • 4 条避坑铁律
    1. 初始化安全 :父类构造绝对禁止调用 open 成员。
    2. 接口限制 :接口无构造,不可 super(...)
    3. 限定符强制 :多实现同名成员,必须用 <T> 指定来源。
    4. 访问权限 :只能调用 public/protected 且非 final 的父类成员。
相关推荐
蚂蚁背大象15 分钟前
Rust 所有权系统是为了解决什么问题
后端·rust
砖厂小工1 小时前
用 GLM + OpenClaw 打造你的 AI PR Review Agent — 让龙虾帮你审代码
android·github
张拭心2 小时前
春节后,有些公司明确要求 AI 经验了
android·前端·人工智能
子玖2 小时前
go实现通过ip解析城市
后端·go
张拭心2 小时前
Android 17 来了!新特性介绍与适配建议
android·前端
Java不加班2 小时前
Java 后端定时任务实现方案与工程化指南
后端
心在飞扬2 小时前
RAG 进阶检索学习笔记
后端
Moment2 小时前
想要长期陪伴你的助理?先从部署一个 OpenClaw 开始 😍😍😍
前端·后端·github
Das1_2 小时前
【Golang 数据结构】Slice 底层机制
后端·go