千万不要以为你搞懂了 var 和 val

在 Kotlin 中,变量可以用 varval 声明,这两种写法的核心差异就在于变量是否可重新赋值。这个区别可以帮助开发者更清晰地表达意图,让代码的语义和变量的实际用途保持一致。

很多开发者刚开始对于重新赋值这个概念比较模糊,甚至会错误的理解成对象是否可以变化。

这里做个最简单的解释:赋值就是使用等号!是否可重新赋值移位置你是否可以再次使用等号!

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 的改变而改变。对于带有自定义 Getterval 来说,它甚至不存在一个固定的底层引用,每次访问都可能计算并返回一个新的对象。

这就是为什么称 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 字节码视角

知其然更要知其所以然。从底层字节码来看:

  • 局部变量valvar 编译成 Java 字节码后区别不大,主要是 Kotlin 编译器在前端进行了重新赋值的限制(类似于 Java 的局部 final)。
  • 类的属性val 属性在底层只会生成一个 private 的幕后字段(Backing Field)和一个 publicGetter 方法;而 var 属性会同时生成 GetterSetter。因为没有生成 Setter,所以外部无法给它重新赋值,这就是"只读"的底层真相。

5. val 与编译期常量 const val

另一个值得注意的点,是 val 和常量并不相同。val 创建的是只读变量或属性,它的值可以在运行时 动态计算得出(例如 val currentTime = System.currentTimeMillis() 是完全合法的)。

而真正的编译期常量必须使用 const val 修饰,并且只适用于顶层声明或对象声明中的基本类型和字符串。编译器会在底层将 const val 的值直接内联(Inline)替换到所有调用的地方,消除访问开销。

总结而言,更严谨的表述应该是:val 保证的是不可重新赋值(无 Setter),但所代表的值或指向的对象自身是否可变,完全取决于对象的类型设计和属性的实现方式。

总结

var 用于可变变量,允许在后续被重新赋值;val 用于只读变量或属性,确保不能再次直接赋值,但并不自动带来对象不可变性或值的绝对固定。

理解并合理使用这两个关键字,能帮助开发者写出更健壮、也更易维护的 Kotlin 代码。通常应优先使用 val 来减少意外的修改,只有在确实需要重新赋值时才使用 var。如果想要真正获得数据状态的不可变性,仍然需要依赖不可变类型或在设计上明确约束。

相关推荐
stevenzqzq1 小时前
app性能日志工具
android
DB哥讲数据库1 小时前
【最新】MySQL9.7安装教程:图文详解(附MySQL安装包)
android·mysql·adb
IT乐手2 小时前
Android 写本地日志工具类
android·java
非典型Android程序员3 小时前
Launcher学习总结
android
Coffeeee3 小时前
Android功耗优化,哪些代码会让你的机子发热又耗电
android·架构·kotlin
Android打工仔3 小时前
Kotlin 编译器是如何把 suspend 函数变成状态机的?
kotlin
Android打工仔3 小时前
一个 suspend 函数,为什么会变成一个普通函数?
kotlin
xianrenli385 小时前
Android14自动时区更新
android·学习·framework·aosp14·自动时区
心中有国也有家5 小时前
AtomGit Flutter 鸿蒙客户端:零外部资源的应用打包策略
android·javascript·flutter·华为·harmonyos
-SOLO-5 小时前
解决gralde下载太慢的问题
android