
在 Kotlin 中,变量可以用 var 或 val 声明,这两种写法的核心差异就在于变量是否可重新赋值。这个区别可以帮助开发者更清晰地表达意图,让代码的语义和变量的实际用途保持一致。
很多开发者刚开始对于重新赋值这个概念比较模糊,甚至会错误的理解成对象是否可以变化。
这里做个最简单的解释:赋值就是使用等号!是否可重新赋值移位置你是否可以再次使用等号!
var 声明的是可变,也就是说,初始化之后它可以重新赋值。这在变量需要随着程序运行不断更新时尤其有用。比如,循环里的计数器通常就适合用 var:
kotlin
// 声明一个可变变量
var counter = 0
counter += 1 // 'counter' 的值已被更新,这里可以重新使用等号
相对地,val 声明的是只读变量。这里的"只读"指的是不能在初始化之后再向其重新赋值。
不过,这并不等于对象一定不可变;val 只能保证引用不变,不能保证引用所指向的数据内容不变。例如:
kotlin
// 声明一个只读变量
val name = "Kotlin"
// name = "Java" // 这将导致编译错误
因此,当 val 指向的是可变对象,比如列表或映射时,对象内部的数据依然可以修改:
kotlin
// 指向可变列表的只读引用
val numbers = mutableListOf(1, 2, 3)
numbers.add(4) // 列表内容被修改了,但引用保持不变
正因为如此,val 很适合那些引用应该保持稳定、但底层数据偶尔仍需调整的场景;而 var 则更适合变量本身需要随时间变化的情况。
进阶:为什么 val 不能确保不可变
很多人会把 val 直接理解成"不可变",但这其实并不准确(如我们开头所说)。
更准确地说,val 代表的是"只读引用":初始化之后,这个引用不能再被重新赋值指向别的对象,但这并不代表它所指向的对象本身不可变,甚至不代表它的值总是固定不变的。
之所以会这样,是因为 val 约束的仅仅是不能重新赋值(没有 Setter),而不是对象本体的状态。
1. 引用不可变,对象状态可变
当你声明一个普通的局部 val 时,编译器只会保证这个引用保持不变。即,你不能在初始化之后再把它赋值为另一个对象。
但如果这个对象本身是可变的,你依然可以修改它的内部状态。这个特性在可变集合中尤其常见:
kotlin
val mutableList = mutableListOf(1, 2, 3)
mutableList.add(4) // 列表内容发生了变化,但引用保持不变
这里不能把 mutableList 重新赋值为另一个列表,但可以继续修改它内部的元素,因为 val 并不会为对象本身施加"不可变"的约束。
所谓真正的不可变,意味着对象状态完全不能改变。要做到这一点,通常需要使用不可变类型,例如 JetBrains 提供的 kotlinx.collections.immutable 库。
2. 自定义 Getter
当我们把 val 用作类的属性时,情况会变得更加有趣。
val 属性意味着它没有 Setter,但你可以为它提供一个自定义的 Getter。在这种情况下,虽然不能对属性直接赋值,但每次读取它的值都可能发生变化:
kotlin
class User {
var firstName = "John"
// greeting 是一个 val 属性,但它的值不是固定的
val greeting: String
get() = "Hello $firstName"
}
val user = User()
println(user.greeting) // 输出 "Hello John"
user.firstName = "Jane"
println(user.greeting) // 输出 "Hello Jane",val 属性的值改变了!
在这个例子中,greeting 是一个 val 属性,但它的返回值会随着 firstName 的改变而改变。对于带有自定义 Getter 的 val 来说,它甚至不存在一个固定的底层引用,每次访问都可能计算并返回一个新的对象。
这就是为什么称 val 为"只读"(Read-only)比"不可变"(Immutable)更加准确。
3. 智能类型转换
正是因为 val 不等于绝对不可变,Kotlin 编译器在进行智能类型转换时非常谨慎。这在实际开发中经常让人感到困惑。
如果是一个局部的 val 变量,编译器确信它一旦赋值就不会再改变,因此可以完美地进行智能转换:
kotlin
fun printLength(obj: Any) {
val localVal = obj
if (localVal is String) {
println(localVal.length) // 局部 val,安全转换,自动识别为 String
}
}
但如果是一个类的公开 val 属性 (特别是带有自定义 Getter,或者处于跨模块调用、属性被 open 修饰时),编译器会拒绝进行智能转换。
因为编译器无法保证在"检查类型"和"使用属性"这极短的时间差内,这个属性的值没有被偷偷改变(比如被其它线程改变,或者因为被子类重写、每次 Getter 都返回不同类型的新对象)。这进一步佐证了"val 属性不等于绝对不变"。
4. Java 字节码视角
知其然更要知其所以然。从底层字节码来看:
- 局部变量 :
val和var编译成 Java 字节码后区别不大,主要是 Kotlin 编译器在前端进行了重新赋值的限制(类似于 Java 的局部final)。 - 类的属性 :
val属性在底层只会生成一个private的幕后字段(Backing Field)和一个public的Getter方法;而var属性会同时生成Getter和Setter。因为没有生成Setter,所以外部无法给它重新赋值,这就是"只读"的底层真相。
5. val 与编译期常量 const val
另一个值得注意的点,是 val 和常量并不相同。val 创建的是只读变量或属性,它的值可以在运行时 动态计算得出(例如 val currentTime = System.currentTimeMillis() 是完全合法的)。
而真正的编译期常量必须使用 const val 修饰,并且只适用于顶层声明或对象声明中的基本类型和字符串。编译器会在底层将 const val 的值直接内联(Inline)替换到所有调用的地方,消除访问开销。
总结而言,更严谨的表述应该是:
val保证的是不可重新赋值(无 Setter),但所代表的值或指向的对象自身是否可变,完全取决于对象的类型设计和属性的实现方式。
总结
var 用于可变变量,允许在后续被重新赋值;val 用于只读变量或属性,确保不能再次直接赋值,但并不自动带来对象不可变性或值的绝对固定。
理解并合理使用这两个关键字,能帮助开发者写出更健壮、也更易维护的 Kotlin 代码。通常应优先使用 val 来减少意外的修改,只有在确实需要重新赋值时才使用 var。如果想要真正获得数据状态的不可变性,仍然需要依赖不可变类型或在设计上明确约束。