Kotlin 中的继承模式与 Java 有一些不同之处,主要在三个地方:

  1. Kotlin 中所有类的最终父类是 Any,而非 Java 中的 Object;

  2. Kotlin 中 非抽象类默认不可继承

  3. Kotlin 中 非抽象类函数和类属性默认不可覆盖

1. open 关键字和 override 关键字

open 关键字在 Kotlin 中可以用在定义 非抽象的类、类函数和类属性 之前,用来将它们标记为可继承 的:

open class Person(val name: String) {
    open var age = 0
    open fun say() = "My name is $name, $age years old."}

override 关键字用在定义 子类覆盖父类函数和属性 之前,用来标记覆盖了父类的函数和属性:

class Student(name: String) : Person(name) {
    override var age = 0
    override fun say() = "I'm a student named $name, $age years old."}

需要注意的是,如果类不是 open 的,那么它内部的属性和函数都不能是 open 的。理由很简单,不会被继承的类,不可能有属性和函数被覆盖。

虽然 Kotlin 默认没有 open 的属性和函数不能被覆盖,但实际上使用 override 标记和属性和函数可以被覆盖,如果一个 open 类不想让子类覆盖自己 override 的属性和函数,可以使用 final override 来覆盖,这样就可以避免再次被子类覆盖了。

2. 继承类的写法和构造方法

Kotlin 中没有 extends 关键字,声明一个类继承自另一个标记为 open 的类的方法是:

class 子类[(主构造方法参数)]: 父类[(主构造方法参数)] {……}

可以这样理解,冒号 : 在 Kotlin 中表示前者属于后者类型,比如我们定义变量时用冒号声明变量的类型,定义函数时用冒号声明函数的返回类型。而在继承中,子类实际上就属于父类的类型,所以没有必要再专门用一个 extends 关键字声明父类,只需要用冒号。

Java 中,子类在调用自身的构造方法前,会先调用父类的构造方法,所以如果父类有自定义的构造方法,子类就必须显式地定义构造方法,并在构造方法中显式调用父类的构造方法。Kotlin 也是如此,如果父类定义了主构造函数,子类就必须显式地调用父类的主构造函数,原因与 Java 一样。

所以上面 Student 类在定义时,不能这样写:

// 编译错误,Person 类有自定义的主构造函数class Student(name: String): Person {……}

3. 覆盖属性时的细节

在覆盖属性时,有两点需要特别注意:

  1. 不允许用 val 属性覆盖 var 属性。用 val 属性覆盖 val 属性和用 var 属性覆盖 var 属性很好理解,该是什么样还是什么样嘛。此外,Kotlin 还允许用 var 属性覆盖 val 属性(只需给子类中的属性添加一个 setter 方法),但不允许用 val 属性覆盖 var 属性:

    open class Person() {
      var name = ""}class Student(): Person(name) {
      override val name: String = name}

    当我们使用多态时:

    val alex: Person = Student()alex.name = "Alex" // 错误,子类对象的属性没有 setter 方法

  2. 可以在主构造函数中定义要覆盖的属性

    open class Person(open val name: String)class Student(override val name: String): Person(name)

    如上,我们在 Person 类的主构造函数中将 name 定义为 open 的属性,然后在 Student 类中使用 override 覆盖了它。