属性¶
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。 - 类型必须是非空引用类型。
- 不能用于主构造函数参数。
- 不能用于
Int、Boolean等原始类型。
可检查是否初始化:
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