继承与接口

Kotlin 支持面向对象编程,但它对继承更保守:类和成员默认不可继承、不可覆盖。这个默认值与 Java 相反,背后的设计目标是让扩展点显式化。

Any:所有类的共同父类

没有显式父类时,Kotlin 类继承自 Any

class User

Any 提供:

  • equals()
  • hashCode()
  • toString()

它类似 Java 的 Object,但方法集合更小。例如 Java 的 wait()notify() 属于 JVM 对象模型,Kotlin 的 Any 不把它们作为跨平台公共 API 暴露。

类默认 final

class Base
// class Child : Base() // 编译错误

允许继承需要显式 open

open class Base

class Child : Base()

Java 默认允许继承,除非写 final。Kotlin 默认禁止继承,除非写 open。这能降低无意间被继承导致的维护风险。

方法覆盖

open class Shape {
    open fun draw() {
        println("shape")
    }

    fun fill() {
        println("fill")
    }
}

class Circle : Shape() {
    override fun draw() {
        println("circle")
    }
}

要覆盖父类成员,父类成员必须是 open,子类成员必须写 override。如果没有 override,编译器会报错。

override 成员默认也是开放的。如果不希望继续被子类覆盖:

open class Rectangle : Shape() {
    final override fun draw() {
        println("rectangle")
    }
}

属性覆盖

open class Shape {
    open val vertexCount: Int = 0
}

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

val 可以被 var 覆盖,因为 var 提供 getter 和 setter;反过来不行,因为父类如果承诺可写,子类不能把可写能力拿掉。

抽象类

abstract class Repository<T> {
    abstract fun findById(id: Long): T?

    fun requireById(id: Long): T =
        findById(id) ?: error("not found: $id")
}

抽象类天然用于继承,不需要额外写 open。抽象成员没有实现,子类必须实现。

接口

interface Clickable {
    fun click()

    fun doubleClick() {
        click()
        click()
    }
}

Kotlin 接口可以有抽象方法,也可以有默认实现。接口不能直接存储状态,但可以声明属性:

interface Named {
    val name: String
}

class User(override val name: String) : Named

接口属性要求实现类提供这个属性,不代表接口里有字段。

接口继承

interface Named {
    val name: String
}

interface Person : Named {
    val firstName: String
    val lastName: String

    override val name: String
        get() = "$firstName $lastName"
}

实现 Person 的类只需要提供缺失的成员:

data class Employee(
    override val firstName: String,
    override val lastName: String,
    val role: String
) : Person

多接口冲突

interface A {
    fun foo() = println("A")
}

interface B {
    fun foo() = println("B")
}

class C : A, B {
    override fun foo() {
        super<A>.foo()
        super<B>.foo()
    }
}

如果多个接口提供同名默认实现,类必须明确选择如何处理冲突。

继承还是组合

Kotlin 语法鼓励你少用继承。继承适合:

  • 真实的 is-a 关系。
  • 框架要求的扩展点。
  • 抽象模板方法。
  • 封闭层级配合密封类。

组合适合:

  • 复用行为。
  • 替换策略。
  • 避免基类状态和初始化顺序复杂化。

如果只是想复用几个工具方法,优先考虑扩展函数、组合对象或委托,而不是创建父类。

实践建议

  • 不要为了“以后可能扩展”随手写 open
  • 覆盖成员时认真考虑是否还允许子类继续覆盖。
  • 接口适合表达能力,抽象类适合共享状态和模板流程。
  • 多接口默认实现冲突时,不要只机械调用两个 super,要明确业务语义。
  • Java 框架需要代理类时,可能需要 Kotlin all-open 插件或框架专用插件。

参考

  • 官方继承文档:https://kotlinlang.org/docs/inheritance.html
  • 官方接口文档:https://kotlinlang.org/docs/interfaces.html