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 的父类成员。
相关推荐
java干货1 小时前
优雅停机!Spring Boot 应用如何使用 Hook 线程完成“身后事”?
java·spring boot·后端
鹿里噜哩1 小时前
Spring Authorization Server 打造认证中心(三)自定义登录页
后端·架构
云技纵横1 小时前
基于Redis键过期实现订单超时自动关闭:一套优雅的事件驱动方案
后端
Cache技术分享1 小时前
260. Java 集合 - 深入了解 HashSet 的内部结构
前端·后端
幌才_loong1 小时前
.NET8+Autofac 实战宝典:从组件拆解到场景落地的依赖注入新范式
后端·.net
狂奔小菜鸡1 小时前
Day23 | Java泛型详解
java·后端·java ee
源代码•宸1 小时前
GoLang并发示例代码1(关于逻辑处理器运行顺序)
开发语言·经验分享·后端·golang
Dolphin_Home1 小时前
接口字段入参出参分离技巧:从注解到DTO分层实践
java·spring boot·后端
程序员Easy哥1 小时前
ID生成器-第二讲:实现一个客户端批量ID生成器?你还在为不了解ID生成器而烦恼吗?本文带你实现一个自定义客户端批量生成ID生成器?
后端·架构