5.8.6 继承

原文链接: https://kotlinlang.org/docs/inheritance.html

5.8.6 继承

提示: 在用类构建继承层级之前,请考虑使用抽象类或接口。抽象类和接口默认就允许继承。它们的设计目的就是让其他类继承并实现它们的成员。

Kotlin 中所有类都有一个共同的超类 Any,它是未声明超类型的类的默认超类:

1
class Example // 隐式继承自 Any

Any 有三个方法:equals()、hashCode() 和 toString()。因此,这些方法对所有 Kotlin 类都是可用的。

默认情况下,Kotlin 类是 final 的——不能被继承。要让类可以被继承,请用 open 关键字标记它:

1
open class Base // 该类开放继承

更多信息请参阅 open 关键字。

要声明显式的超类型,请把类型写在类头部的冒号之后:

1
2
3
open class Base(p: Int)

class Derived(p: Int) : Base(p)

如果派生类有主构造函数,那么必须在其主构造函数中根据参数初始化基类。

如果派生类没有主构造函数,则每个次构造函数都必须用 super 关键字初始化基类型,或者委托给另一个这样做的构造函数。注意,在这种情况下,不同的次构造函数可以调用基类的不同构造函数:

1
2
3
4
5
class MyView : View {
    constructor(ctx: Context) : super(ctx)

    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}

open 关键字

在 Kotlin 中,open 关键字表示类或成员(函数或属性)可以在子类中被重写。默认情况下,Kotlin 的类及其成员都是 final 的,也就是说除非你显式把它们标记为 open,否则类不能被继承、成员不能被重写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 基类使用 open 关键字以允许继承
open class Person(
    val name: String
) {
    // 可在子类中重写的 open 函数
    open fun introduce() {
        println("Hello, my name is $name.")
    }
}

// 子类继承 Person 并重写 introduce() 函数
class Student(
    name: String,
    val school: String
) : Person(name) {
    override fun introduce() {
        println("Hi, I'm $name, and I study at $school.")
    }
}

如果你重写了基类的某个成员,该重写成员默认也是 open 的。如果你希望改变这一点并禁止你所在类的子类继续重写你的实现,可以显式把该重写成员标记为 final:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 基类使用 open 关键字以允许继承
open class Person(val name: String) {
    // 可在子类中重写的 open 函数
    open fun introduce() {
        println("Hello, my name is $name.")
    }
}

// 继承 Person 并重写 introduce() 函数的子类
class Student(name: String, val school: String) : Person(name) {
    // final 关键字阻止子类继续重写
    final override fun introduce() {
        println("Hi, I'm $name, and I study at $school.")
    }
}

重写方法

Kotlin 要求对可被重写的成员和重写都使用显式修饰符:

1
2
3
4
5
6
7
8
open class Shape {
    open fun draw() { /*...*/ }
    fun fill() { /*...*/ }
}

class Circle() : Shape() {
    override fun draw() { /*...*/ }
}

Circle.draw() 必须使用 override 修饰符。如果缺少它,编译器就会报错。如果函数上没有 open 修饰符(例如 Shape.fill()),则在子类中声明同签名的方法是不允许的,无论是否带 override。把 open 修饰符加到 final 类(没有 open 修饰符的类)的成员上没有任何效果。

被标记为 override 的成员本身就是 open 的,因此可以在子类中再次被重写。如果要禁止再次重写,请使用 final:

1
2
3
open class Rectangle() : Shape() {
    final override fun draw() { /*...*/ }
}

重写属性

重写机制对属性的作用与对方法相同。在超类中声明、随后在派生类中重新声明的属性必须以 override 开头,并且必须有兼容的类型。每个被声明的属性都可以由带初始化器的属性或带 get 方法的属性重写:

1
2
3
4
5
6
7
open class Shape {
    open val vertexCount: Int = 0
}

class Rectangle : Shape() {
    override val vertexCount = 4
}

你也可以用 var 属性重写 val 属性,但反过来不行。这是允许的,因为 val 属性本质上声明了一个 get 方法,而把它重写为 var 会在派生类中额外声明一个 set 方法。

注意,你可以在主构造函数中把 override 关键字作为属性声明的一部分:

1
2
3
4
5
6
7
8
9
interface Shape {
    val vertexCount: Int
}

class Rectangle(override val vertexCount: Int = 4) : Shape // 始终有 4 个顶点

class Polygon : Shape {
    override var vertexCount: Int = 0 // 之后可以设置为任意数值
}

派生类的初始化顺序

在构造派生类的新实例时,基类的初始化是第一步(在此之前只会对基类构造函数的实参求值),也就是说它发生在派生类的初始化逻辑运行之前。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
open class Base(val name: String) {

    init { println("Initializing a base class") }

    open val size: Int =
        name.length.also { println("Initializing size in the base class: $it") }
}

class Derived(
    name: String,
    val lastName: String,
) : Base(name.replaceFirstChar { it.uppercase() }.also { println("Argument for the base class: $it") }) {

    init { println("Initializing a derived class") }

    override val size: Int =
        (super.size + lastName.length).also { println("Initializing size in the derived class: $it") }
}

fun main() {
    println("Constructing the derived class(\"hello\", \"world\")")
    Derived("hello", "world")
}

这意味着,当基类构造函数执行时,派生类中声明或重写的属性尚未初始化。在基类初始化逻辑中使用这些属性(无论是直接使用,还是通过另一个被重写的 open 成员实现间接使用)都可能导致错误行为或运行时失败。因此,在设计基类时,应避免在构造函数、属性初始化器和 init 块中使用 open 成员。

调用超类的实现

派生类中的代码可以用 super 关键字调用其超类的函数和属性访问器实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
open class Rectangle {
    open fun draw() { println("Drawing a rectangle") }
    val borderColor: String get() = "black"
}

class FilledRectangle : Rectangle() {
    override fun draw() {
        super.draw()
        println("Filling the rectangle")
    }

    val fillColor: String get() = super.borderColor
}

在内部类中,访问外部类的超类需要用外部类名限定 super 关键字:super@Outer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
open class Rectangle {
    open fun draw() { println("Drawing a rectangle") }
    val borderColor: String get() = "black"
}

class FilledRectangle: Rectangle() {
    override fun draw() {
        val filler = Filler()
        filler.drawAndFill()
    }

    inner class Filler {
        fun fill() { println("Filling") }
        fun drawAndFill() {
            super@FilledRectangle.draw() // 调用 Rectangle 的 draw() 实现
            fill()
            println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}") // 使用 Rectangle 的 borderColor get() 实现
        }
    }
}

fun main() {
    val fr = FilledRectangle()
        fr.draw()
}

重写规则

在 Kotlin 中,实现继承遵循以下规则:如果一个类从其直接超类继承了同一个成员的多个实现,它必须重写该成员并提供自己的实现(也可以使用其中一个继承来的实现)。

要指定继承的实现来自哪个超类型,请使用尖括号中的超类型名限定 super,例如 super<Base>:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
open class Rectangle {
    open fun draw() { /* ... */ }
}

interface Polygon {
    fun draw() { /* 接口成员默认是 'open' 的 */ } // interface members are 'open' by default
}

class Square() : Rectangle(), Polygon {
    // 编译器要求重写 draw():
    override fun draw() {
        super<Rectangle>.draw() // 调用 Rectangle.draw()
        super<Polygon>.draw() // 调用 Polygon.draw()
    }
}

同时继承 Rectangle 和 Polygon 本身没有问题,但它们都有各自的 draw() 实现,所以需要在 Square 中重写 draw() 并为其提供单独的实现,以消除歧义。