类与对象

Kotlin 的类语法比 Java 更紧凑,但背后的概念并不简单。主构造函数、属性、默认 final、数据类、对象声明等设计都会影响 API 的可读性和可维护性。

类声明

class User

带主构造函数和属性:

class User(val id: Long, var name: String)

这行代码同时做了几件事:

  • 声明类 User
  • 声明主构造函数参数 idname
  • id 声明为只读属性。
  • name 声明为可变属性。

Java 近似写法:

public class User {
    private final long id;
    private String name;

    public User(long id, String name) {
        this.id = id;
        this.name = name;
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

属性

Kotlin 的属性不是简单字段。属性通常包含:

  • backing field,也就是实际存储值的字段。
  • getter。
  • 可变属性的 setter。
class Rectangle(val width: Int, val height: Int) {
    val area: Int
        get() = width * height
}

area 没有保存字段,每次访问时计算。

初始化块

主构造函数不能直接写代码,初始化逻辑放在 init 块:

class User(val name: String) {
    init {
        require(name.isNotBlank()) { "name 不能为空" }
    }
}

init 块按声明顺序执行。复杂初始化逻辑要注意属性声明顺序,避免在初始化完成前访问尚未准备好的状态。

次构造函数

class User(val name: String) {
    constructor(firstName: String, lastName: String) : this("$firstName $lastName")
}

有主构造函数时,次构造函数必须直接或间接委托给主构造函数。

多数 Kotlin 类不需要多个构造函数。默认参数和工厂函数通常更清晰:

class ServerConfig(
    val host: String,
    val port: Int = 8080
)

继承:默认 final

Kotlin 类默认不可继承:

class User
// class Admin : User() // 编译错误

要允许继承,显式写 open

open class Shape

class Rectangle : Shape()

方法同样默认不可覆盖:

open class Animal {
    open fun speak() {
        println("...")
    }
}

class Dog : Animal() {
    override fun speak() {
        println("woof")
    }
}

Java 默认允许继承和覆盖,除非使用 final。Kotlin 反过来:默认关闭继承,只有明确设计为扩展点时才打开。这能减少脆弱基类问题。

抽象类

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

    fun requireById(id: Long): T =
        findById(id) ?: error("找不到数据:$id")
}

抽象类本身可以被继承,抽象成员必须由子类实现。

接口

interface Clickable {
    fun click()

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

Kotlin 接口可以包含默认方法实现,也可以声明属性:

interface Named {
    val name: String
}

接口属性不一定有字段,它只是要求实现类提供这个属性。

数据类

用于承载数据的类可以声明为 data class

data class User(val id: Long, val name: String)

编译器会基于主构造函数中的属性生成常用方法:

  • equals()
  • hashCode()
  • toString()
  • copy()
  • componentN() 解构函数

示例:

val user = User(1, "Ada")
val renamed = user.copy(name = "Grace")

println(user)
println(renamed)

Java 16+ 的 record 与 Kotlin data class 很像,但 Kotlin 数据类更早出现,也有不同约束和互操作细节。迁移时不要简单认为二者完全等价。

对象声明

Kotlin 没有 Java 风格的 static 关键字。单例可以用 object

object AppConfig {
    val appName = "Demo"
}

println(AppConfig.appName)

这会声明一个线程安全初始化的单例对象。

伴生对象

类内需要类似静态成员时,使用伴生对象:

class User private constructor(val name: String) {
    companion object {
        fun create(name: String): User {
            require(name.isNotBlank())
            return User(name)
        }
    }
}

val user = User.create("Ada")

从 Kotlin 调用时像静态方法;从 Java 调用时会有伴生对象相关的字节码形态。需要更贴近 Java 静态调用时,可以考虑 @JvmStatic

密封类简述

密封类限制继承层级,常用于表达有限状态:

sealed interface UiState

data object Loading : UiState
data class Success(val data: String) : UiState
data class Failure(val message: String) : UiState

fun render(state: UiState): String =
    when (state) {
        Loading -> "加载中"
        is Success -> "成功:${state.data}"
        is Failure -> "失败:${state.message}"
    }

密封层级配合 when 可以得到编译期穷尽检查。新增状态时,遗漏处理的地方会暴露出来。

实践建议

  • 默认使用不可变属性 val,确实需要修改时再用 var
  • 只有为继承设计的类才标记 open
  • 数据承载类型优先考虑 data class
  • 有限状态优先考虑密封类或密封接口,而不是字符串常量。
  • Java 框架需要无参构造、代理或反射时,提前确认 Kotlin 插件和注解配置。

参考

  • 官方类文档:https://kotlinlang.org/docs/classes.html