Kotlin 继承
继承是 OOP 的核心机制——子类复用父类的属性和方法,并能扩展或修改行为。Kotlin 的继承设计有一个关键改进:类默认是 final(不能被继承),必须显式用 open 才能继承。这避免了 Java 著名的"继承滥用"问题(Effective Java 的核心建议之一)。
1. 类默认 final,open 才能继承
这是 Kotlin 和 Java 最重要的差异之一:
// Kotlin 类默认【final,不能被继承】(关键差异!)
// 必须【显式加 open】才能被继承
open class Animal(val name: String) {
open fun sound() { // open 才能被重写
println("$name 发出声音")
}
fun description() { // 默认 final,不能 override
println("我是 $name")
}
}
class Dog(name: String) : Animal(name) {
override fun sound() { // override 重写
println("$name 汪汪叫")
}
}
val d = Dog("旺财")
d.sound() // 旺财 汪汪叫
d.description() // 我是 旺财Java 默认类可以被继承,导致库作者无法控制继承行为(子类可能破坏父类不变式)。Kotlin 默认 final 强制你显式设计:一个类开放继承前,你必须想清楚它能不能被正确继承(否则禁止),这是"针对扩展开放,针对修改关闭"原则在语言层面的体现。
同理,成员默认也是 final(不能 override),加 open 才能 override。这让继承设计意图非常清晰。
2. 继承的初始化顺序(重要!)
继承体系下的初始化顺序是面试题和 bug 高发点:
// 继承时的初始化顺序(重要!)
open class Base {
init { println("Base init") }
}
class Derived : Base() {
init { println("Derived init") }
}
val d = Derived()
// 输出:
// Base init (先父类)
// Derived init (后子类)
// 构造调用顺序:父类主构造 -> 父类 init -> 子类属性 -> 子类 init
// 这就是为什么子类 init 块能用父类的属性:那时父类已初始化完成顺序:父类主构造 → 父类 init 块 → 子类属性初始化 → 子类 init 块。这就是为什么子类 init 块里能安全使用父类属性——那时父类已经初始化完成。但反过来,父类 init 块里访问子类属性会得到未初始化的默认值(0 / null),这是常见陷阱。
3. super 调用父类
// super 调用父类
open class Shape {
open fun draw() {
println("Shape.draw")
}
open fun area(): Double = 0.0
}
class Circle(val radius: Double) : Shape() {
override fun draw() {
super.draw() // 调用父类版本
println("画一个半径 $radius 的圆")
}
override fun area(): Double {
return Math.PI * radius * radius
}
}
val c = Circle(5.0)
c.draw()
// Shape.draw
// 画一个半径 5.0 的圆
println("面积: ${c.area()}") // 面积: 78.54...super 引用父类版本,常用于"扩展父类行为而非完全替换"。比如 override fun draw() { super.draw(); /* 加点东西 */ }。
4. 抽象类(abstract)
抽象类不能被实例化,只能被继承,可以包含抽象方法(无实现,子类必须 override):
// 抽象类(abstract):不能实例化,只能被继承
abstract class Vehicle(val brand: String) {
// 普通方法(有实现)
fun start() {
println("$brand 启动")
}
// 抽象方法(无实现,子类必须 override)
abstract fun maxSpeed(): Int
// 抽象属性
abstract val wheelCount: Int
}
class Car(brand: String) : Vehicle(brand) {
override val wheelCount = 4
override fun maxSpeed() = 200
}
class Bicycle(brand: String) : Vehicle(brand) {
override val wheelCount = 2
override fun maxSpeed() = 40
}
val v1: Vehicle = Car("比亚迪")
val v2: Vehicle = Bicycle("捷安特")
println("${v1.brand}: ${v1.maxSpeed()} km/h, ${v1.wheelCount} 轮")
println("${v2.brand}: ${v2.maxSpeed()} km/h, ${v2.wheelCount} 轮")抽象类的特点:
- 用
abstract class声明,不能直接实例化。 - 抽象成员(
abstract fun/abstract val)没有实现,子类必须 override。 - 抽象类默认是 open(因为反正要被继承),不用写 open。
- 抽象方法默认是 open,子类的 override 也默认 open(可被孙子类再 override)。
5. final 阻止重写
open 类的某个 override 方法,默认仍然可以被孙子类再 override。如果想阻止再被重写,加 final:
// final:阻止重写(用在 override 前面)
open class Base {
open fun foo() {}
open fun bar() {}
}
class Mid : Base() {
final override fun foo() {} // final 阻止子类再 override
override fun bar() {} // 仍可被 override
}
// class Sub : Mid() {
// override fun foo() {} // 编译错误!foo 是 final
// override fun bar() {} // OK
// }
// 注意:
// 1. 普通类默认就是 final(不能被继承)
// 2. open class 的成员默认非 open(不能被 override,除非加 open)
// 3. abstract class 的抽象成员默认是 open(必须被 override)final 在框架/库开发里很重要——你实现了关键逻辑,不想让用户继承破坏。
6. 多态(Polymorphism)
多态是 OOP 的精髓——父类引用指向子类对象,调用方法时按实际类型分派:
// 多态(Polymorphism):父类引用指向子类对象
open class Animal(val name: String) {
open fun sound() = println("$name 叫了一声")
}
class Cat(name: String) : Animal(name) {
override fun sound() = println("$name 喵喵")
}
class Dog(name: String) : Animal(name) {
override fun sound() = println("$name 汪汪")
}
// 函数接收父类,运行时根据实际类型调用对应方法
fun makeSound(a: Animal) {
a.sound() // 多态!调用实际类型的方法
}
fun main() {
val animals = listOf(Cat("小花"), Dog("旺财"), Cat("橘猫"))
animals.forEach { makeSound(it) }
}
// 小花 喵喵
// 旺财 汪汪
// 橘猫 喵喵关键:makeSound(a: Animal) 接收任何 Animal 子类,内部调用 a.sound() 时,实际执行的是 Cat/Dog 的版本。这是动态分派(运行时多态),靠虚方法表实现。
7. 子类构造与父类构造
子类构造函数必须委托给父类构造函数,保证父类一定被正确初始化:
// 子类构造与父类构造
open class Person(val name: String, val age: Int) {
init { println("Person init: $name, $age") }
}
// 子类主构造:必须委托给父类主构造(用 : Person(...))
class Student(name: String, age: Int, val school: String) : Person(name, age) {
init { println("Student init: $school") }
}
val s = Student("小明", 20, "清华大学")
// 输出:
// Person init: 小明, 20
// Student init: 清华大学
// 子类有自己的次构造函数
class Teacher(name: String, age: Int, val subject: String) : Person(name, age) {
// 次构造函数:必须先委托给主构造(this) 或父类(super)
constructor(name: String) : this(name, 30, "未指定")
}子类主构造函数用 : 父类名(参数) 委托。次构造函数用 this(...)(委托给主构造)或 super(...)(直接委托父类构造)。规则:任何路径都必须最终调用到父类主构造。
8. 类型转换(is、as)
配合继承常用类型判断和转换:
open class Animal
class Dog : Animal()
class Cat : Animal()
fun describe(a: Animal) {
// is: 类型判断(自动智能转换)
if (a is Dog) {
println("狗") // 这里 a 自动当 Dog 用
} else if (a is Cat) {
println("猫")
}
// as: 显式强转(失败抛 ClassCastException)
// as?: 安全转换(失败返回 null)
val d: Dog? = a as? Dog
println(d)
}
describe(Dog()) // 狗 / null
describe(Cat()) // 猫 / null
// when + is 配合
fun name(a: Animal) = when (a) {
is Dog -> "旺财"
is Cat -> "小花"
else -> "未知"
}注意 Kotlin 的 is 触发智能转换:在 if 分支内,a 自动当作 Dog 类型,无需强转。这是比 Java (instanceof + 强转)优雅的地方。
9. 实战:图形面积计算
abstract class Shape(val name: String) {
abstract fun area(): Double
open fun describe() = "$name 的面积是 ${"%.2f".format(area())}"
}
class Circle(val radius: Double) : Shape("圆") {
override fun area() = Math.PI * radius * radius
}
class Rectangle(val width: Double, val height: Double) : Shape("矩形") {
override fun area() = width * height
}
class Triangle(val a: Double, val b: Double, val c: Double) : Shape("三角形") {
override fun area(): Double {
val s = (a + b + c) / 2
return Math.sqrt(s * (s - a) * (s - b) * (s - c))
}
}
fun main() {
val shapes = listOf(
Circle(5.0),
Rectangle(4.0, 6.0),
Triangle(3.0, 4.0, 5.0)
)
shapes.forEach { println(it.describe()) }
val total = shapes.sumOf { it.area() }
println("总面积: ${"%.2f".format(total)}")
}
// 圆 的面积是 78.54
// 矩形 的面积是 24.00
// 三角形 的面积是 6.00
// 总面积: 108.54这个例子展示了典型 OOP 模式:抽象父类定义接口(area()),子类各自实现,顶层代码用父类引用统一处理(多态)。新加图形(如 Square)只需加一个子类,不动现有代码——这就是"开闭原则"。
10. 继承设计建议
- 优先组合而非继承:Effective Java 的第 18 条。继承破坏封装(子类依赖父类实现细节),库升级时容易破坏子类。能用组合就用组合。
- 类要么设计为继承(文档化),要么标 final:Kotlin 默认 final 强制你做这个决定。
- 抽象类 vs 接口:抽象类用于"is-a"关系且需要共享实现;接口用于"can-do"能力。详见 下一篇。
- 避免深层继承:超过 3 层的继承通常说明设计有问题。
小结
这一章你掌握了 Kotlin 的继承机制:默认 final(必须 open 才能继承)、override 重写、super 调用、abstract 抽象类、final 阻止重写、多态、初始化顺序。最大的差异是默认 final——这强制你显式设计继承边界。下一篇看 Kotlin 的接口,它和 Java 接口有什么不同。
← 上一篇 Kotlin 类与对象
下一篇 Kotlin 接口 →