希望帮你在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 关键规则
- 签名一致:调用的父类方法签名(参数、返回值)需与当前方法一致。
- 访问限制 :只能调用父类中
open的方法,无法调用private方法。 - 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关键字,提升代码质量和可维护性,推荐以下最佳实践:
- 最小复用原则:仅复用父类或接口的核心逻辑,子类聚焦于自身的差异化扩展。避免过度依赖super调用,防止子类与父类的耦合度过高,影响代码的灵活性。
- 明确调用原则:在多实现场景下,必须使用super<限定符>明确指定超类来源,即使当前只有一个超类提供实现,也建议加上限定符,提升代码的可读性和可维护性,避免后续扩展时出现歧义。
- 构造安全原则:严格避免在父类构造函数、init块中调用可被覆盖的方法或属性,防止因初始化顺序问题导致的未初始化异常。若需要执行初始化逻辑,可采用"延迟初始化"或"初始化方法"的方式。
- 接口协同原则:在实现多个接口时,充分利用super<接口名>复用接口的默认实现,将多个接口的功能进行协同组合,减少重复编码,实现"接口职责单一,子类组合复用"的设计目标。
8.3 核心记忆法则:"1-2-3-4"
为了方便记忆,我们将 super 的核心机制总结为:
- 1 个核心目标 :复用 (Reuse)。
super的存在是为了在扩展的同时保留原有的基础能力。 - 2 种构造调用 :主构造隐式 委托(
class S : P()),次构造显式 调用(: super())。 - 3 大使用场景 :
- 覆盖方法中复用旧逻辑 (
super.func())。 - 属性访问器中扩展逻辑 (
super.prop)。 - 多接口冲突中消除歧义 (
super<T>.func())。
- 覆盖方法中复用旧逻辑 (
- 4 条避坑铁律 :
- 初始化安全 :父类构造绝对禁止调用
open成员。 - 接口限制 :接口无构造,不可
super(...)。 - 限定符强制 :多实现同名成员,必须用
<T>指定来源。 - 访问权限 :只能调用
public/protected且非final的父类成员。
- 初始化安全 :父类构造绝对禁止调用