属性

Kotlin 的属性把“字段 + getter + setter”的常见模式提升成语言概念。对 Java 开发者来说,理解属性非常关键,因为你在 Kotlin 中写的 user.name 不一定只是访问字段,它可能会调用 getter,也可能触发自定义逻辑。

声明属性

只读属性:

val name: String = "Ada"

可变属性:

var age: Int = 36

在类中声明:

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

这会生成:

  • id 的 getter。
  • name 的 getter 和 setter。
  • JVM 上对应 Java 习惯的访问方法。

顶层属性

Kotlin 可以在文件顶层声明属性:

package com.example

const val DEFAULT_PAGE_SIZE = 20
var debugMode = false

Java 中通常会创建 Constants 类;Kotlin 不需要为了常量额外创建工具类。实际项目中,顶层属性仍应按业务语义放在清晰的包和文件中,避免变成无组织的全局变量。

自定义 getter

class Rectangle(val width: Int, val height: Int) {
    val area: Int
        get() = width * height
}

area 每次访问都会计算,不一定有字段存储。Java 近似写法:

int getArea() {
    return width * height;
}

如果计算有成本,或者结果需要缓存,不要随意藏在 getter 中。调用方通常会认为属性访问比较轻量。

自定义 setter

class User {
    var name: String = ""
        set(value) {
            require(value.isNotBlank()) { "name 不能为空" }
            field = value
        }
}

field 是 backing field,只能在属性访问器中使用。它表示属性背后的真实存储字段。

控制 setter 可见性

常见模式:外部可读,内部可改。

class BankAccount(initialBalance: Int) {
    var balance: Int = initialBalance
        private set

    fun deposit(amount: Int) {
        require(amount > 0)
        balance += amount
    }
}

这比 Java 中“public getter + private setter/字段”的表达更直接。

backing property

公开只读集合、内部可变集合时,不要直接暴露 MutableList

class UserDirectory {
    private val _users = mutableListOf<String>()

    val users: List<String>
        get() = _users.toList()

    fun addUser(name: String) {
        _users += name
    }
}

下划线前缀 _users 是 Kotlin 常见约定,用于标识私有 backing property。

显式 backing field

较新的 Kotlin 版本支持为只读属性声明显式 backing field。它适合“对外是只读类型,对内需要更具体可变实现”的场景:

class ShoppingCart {
    val items: List<String>
        field = mutableListOf()

    fun addItem(item: String) {
        items.add(item)
    }

    fun removeItem(item: String) {
        items.remove(item)
    }
}

对外部调用者来说,items 的类型是 List<String>,只能按只读集合使用:

val cart = ShoppingCart()
cart.addItem("Apple")

val view: List<String> = cart.items
// cart.items.add("Banana") // 外部看不到 MutableList API

在类内部,编译器知道显式 backing field 的实际类型是 MutableList<String>,所以 addItem() 可以调用 add()

如果需要明确 backing field 类型,可以写成:

class ShoppingCart {
    val items: List<String>
        field: MutableList<String> = mutableListOf()
}

显式 backing field 和传统 backing property 的区别:

写法 示例 特点
backing property private val _items = mutableListOf<String>() + val items: List<String> 兼容旧 Kotlin,写法明确,多一个私有属性名
显式 backing field val items: List<String> field = mutableListOf() 更贴近“一个公开属性,一个内部存储”的模型

限制:

  • 属性必须是 val
  • 属性不能有自定义 getter。
  • 属性不能是 open
  • 属性不能是委托属性。
  • 属性不能是 const val
  • backing field 类型必须是属性类型的子类型,并且 backing field 本身是私有的。

Java 对比:Java 通常写成 private final List<String> items = new ArrayList<>(); 再暴露 List<String> getItems()。Kotlin 的显式 backing field 把这两个概念放进一个属性声明里,但公共 API 仍然应该只承诺 List<String>

编译期常量:const val

const val API_VERSION = "v1"

const val 要求:

  • 必须是顶层属性、object 成员或伴生对象成员。
  • 类型必须是 String 或基本类型。
  • 不能有自定义 getter。

const val 会在编译期内联。对公共库来说,变更 const val 后调用方可能需要重新编译才能看到新值。

延迟初始化:lateinit

class OrderServiceTest {
    lateinit var service: OrderService

    fun setup() {
        service = OrderService()
    }
}

lateinit 适合测试、依赖注入、框架生命周期等场景,但要谨慎使用。访问未初始化的 lateinit 属性会抛出 UninitializedPropertyAccessException

限制:

  • 只能用于 var
  • 类型必须是非空引用类型。
  • 不能用于主构造函数参数。
  • 不能用于 IntBoolean 等原始类型。

可检查是否初始化:

if (this::service.isInitialized) {
    service.process()
}

lazy

延迟只读初始化更常用的是 lazy

val config: Config by lazy {
    loadConfig()
}

第一次访问 config 时执行初始化。它适合昂贵对象、按需加载对象。与 lateinit 不同,lazy 用于 val,初始化逻辑集中在声明处。

实践建议

  • 默认使用 val,确实需要重新赋值时再用 var
  • 自定义 getter 不要做重 IO、网络请求或复杂副作用。
  • 对外暴露集合时优先暴露只读接口,并考虑是否需要防御性复制。
  • 需要“外部只读、内部可变”时,可以在新项目中考虑显式 backing field;需要兼容旧版本或复杂 getter 时继续使用 backing property。
  • lateinit 是生命周期补丁,不是默认初始化策略。
  • 公共库中谨慎使用 const val 暴露可能变化的值。

参考

  • 官方属性文档:https://kotlinlang.org/docs/properties.html