Skip to content

类与接口

源:Classes and inheritance

Kotlin 重新审视了传统的面向对象设计,通过引入主构造函数、默认 final 设计以及接口属性,极简地表达了对象模型,同时避免了许多 Java 中常见的继承陷阱。

类的声明与构造

主构造函数

在 Kotlin 中,构造函数直接紧跟在类名后面。

kotlin
class User(val name: String, var age: Int)

这行代码等价于 Java 中:声明字段 + 构造函数赋值 + Getter + Setter。

  • init 初始化块:主构造函数不能包含代码,初始化逻辑需放在 init 关键字包裹的代码块中。
  • 属性声明:如果在主构造函数参数前加上 valvar,该参数将自动成为类的属性;否则它仅仅是一个构造函数参数(除非在 init 或属性初始化中使用了它)。

初始化顺序

在类实例化期间,属性初始化器和 init 块是按照它们在类体中出现的顺序执行的。

kotlin
class InitOrderDemo(name: String) {
    val firstProperty = "First property: $name".also(::println)
    
    init {
        println("First initializer block that prints $name")
    }
    
    val secondProperty = "Second property: ${name.length}".also(::println)
    
    init {
        println("Second initializer block that prints ${name.length}")
    }
}

避免前向引用

确保你的 init 块只访问那些“已经初始化”的属性,否则可能导致空指针异常或未定义行为。

属性与幕后字段

在 Kotlin 中,属性 (Property) 是全功能的:它包含字段 (Field)、Getter 和 Setter。

传统的幕后字段

在自定义访问器时,使用 field 标识符来访问生成的幕后字段,以避免递归调用。

kotlin
var counter = 0
    set(value) {
        if (value >= 0) field = value // 使用 field 更新值
        // counter = value // ❌ 错误:这会递归调用 setter,导致 StackOverflow
    }

显式幕后字段 Kotlin 2.3.0

在 Kotlin 2.3.0 中,引入了显式幕后字段语法,用于解决“对外只读、对内可变”的常见需求,特别是在处理 StateFlow 或集合时。

在此之前,通常需要定义一个私有属性 _prop 和一个公有只读属性 prop。现在可以直接在属性内部声明 field 关键字。

启用配置 该特性目前处于预览阶段,需要在 build.gradle.kts 中配置编译器参数:

kotlin
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xexplicit-backing-fields")
    }
}

语法演进

kotlin
private val _city = MutableStateFlow<String>("")
val city: StateFlow<String> get() = _city

fun updateCity(newCity: String) {
    _city.value = newCity
}
kotlin
val city: StateFlow<String>
    field = MutableStateFlow("")

fun updateCity(newCity: String) {
    // 智能转换自动生效,在类内部 field 被视为 MutableStateFlow
    city.value = newCity
}

核心优势

这种语法消除了为了封装而引入的 _city 命名污染,使逻辑更加内聚,同时编译器能够自动处理属性与其幕后字段之间的类型转换。

属性的高级特性

1. 延迟初始化 (Lateinit)

通常,非空属性必须在构造函数中初始化。但在依赖注入(DI)或单元测试场景中,我们无法立即提供值。此时可以使用 lateinit var

kotlin
class MyService {
    lateinit var provider: Provider // 稍后注入

    fun perform() {
        if (::provider.isInitialized) { // 检查是否已初始化
            provider.action()
        }
    }
}

限制条件

  • 只能用于 var 属性。
  • 属性类型不能是原生类型(如 Int, Boolean),必须是引用类型。
  • 不能有自定义 getter/setter。

2. Setter 可见性控制

如果你不需要显式幕后字段那样复杂的逻辑,仅仅想实现“对外只读,对内可写”,最简单的办法是改变 setter 的可见性。

kotlin
class User {
    var name: String = "Init"
        private set // 外部只能读取,不能修改

    fun changeName(newName: String) {
        name = newName // 类内部可以修改
    }
}

3. 编译时常量 (Const Val)

const val 声明的是编译时常量(Compile-time constant),它的值在编译期就确定了,并会被内联到调用处。

kotlin
const val MAX_COUNT = 100 // 必须位于顶层或 object/companion object 中

class Demo {
    // const val INSIDE = 1 // ❌ 错误:不能在类内部声明
}

const val vs val

  • val:运行时常量(类似于 Java 的 final 变量),可以由函数计算得出。
  • const val:编译时常量(类似于 Java 的 public static final 常量),只能是 String 或原生类型。

继承与多态

Kotlin 遵循“为继承而设计,否则就禁止”的原则,类和成员默认都是 final

open 与 override

  • open:必须显式使用 open 关键字,类才能被继承,函数才能被重写。
  • abstract:抽象类和抽象成员默认是 open 的。
kotlin
open class Base {
    open fun v() {}
    fun nv() {} // 默认为 final,子类不可重写
}

class Derived : Base() {
    override fun v() {}
}

接口

Kotlin 的接口支持抽象方法、默认实现和抽象属性。

kotlin
interface MyInterface {
    val prop: Int // 抽象属性,实现类必须覆盖它并提供存储方式
    
    val propertyWithImplementation: String
        get() = "foo" // 提供访问器的属性,没有幕后字段

    fun foo() {
        print(prop) // 默认方法体
    }
}

接口与抽象类的区别

  • 状态:接口不能保存状态(即不能有幕后字段 field),它的属性要么是抽象的,要么通过自定义 getter 计算得出。抽象类可以保存状态。
  • 继承:类只能继承一个父类,但可以实现多个接口。

解决覆盖冲突

当实现多个接口且存在同名方法时:

kotlin
interface A { fun foo() { print("A") } }
interface B { fun foo() { print("B") } }

class C : A, B {
    override fun foo() {
        super<A>.foo() // 显式调用 A 的实现
        super<B>.foo() // 显式调用 B 的实现
    }
}

函数式接口 (SAM)

只有一个抽象方法的接口称为函数式接口。使用 fun interface 声明后,可以通过 Lambda 表达式进行实例化。

kotlin
fun interface KRunnable {
    fun invoke()
}

// 通过 Lambda 直接实例化
val r = KRunnable { println("Run!") }

类型别名 (Type Aliases)

typealias 用于为现有类型提供一个更短或更具语义的替代名称。它不会创建新类型,编译器会在编译时将其替换为底层类型。

kotlin
// 缩短长泛型
typealias NodeSet = Set<Network.Node>

// 为函数类型提供语义名称
typealias ClickHandler = (View, Int) -> Unit

// 结合 import 使用,解决类名冲突
// import com.example.MyClass as OldClass // 这是 import alias,不同于 typealias

与值类的区别

typealias 不提供类型安全(NodeSet 仅仅是 Set<...> 的别名,可以互换使用)。如果需要强类型安全(防止参数混淆),请使用 值类

可见性修饰符

  • private:类内部可见。
  • protected:类及其子类可见。在 Kotlin 中,此类成员对同包下的其他类不可见。
  • internal:模块(Module)内可见。这是组件化开发的核心控制符。
  • public(默认):随处可见。

总结

  • 构造顺序:属性初始化器与 init 块按声明顺序执行。
  • 显式字段:利用 Kotlin 2.3.0 特性精简代码(需显式开启配置)。
  • 严格继承:默认 final,提倡组合优于继承。
  • Internal:利用模块级可见性实现良好的封装。