千万不要以为你搞懂了 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。如果想要真正获得数据状态的不可变性,仍然需要依赖不可变类型或在设计上明确约束。

相关推荐
TE-茶叶蛋2 小时前
安卓应用(uniapp开发)分享微信-申请appid
android·微信·uni-app
大白要努力!2 小时前
Android libVLC 3.5.1 实现 RTSP 视频播放完整方案
android·java·音视频
AirDroid_cn3 小时前
手机上看的网页,怎样自动在荣耀 MagicOS 10 平板上接着打开?
android·智能手机·电脑·荣耀手机
帅次3 小时前
WebView 并发初始化竞争风险分析
android·xml·flutter·kotlin·webview·androidx·dalvik
spencer_tseng3 小时前
HTML5 - Android - IOS
android·ios·html·html5
游戏开发爱好者83 小时前
iOS 开发进阶,用 SniffMaster 实现 iPhone 抓包深度分析
android·ios·小程序·https·uni-app·iphone·webview
华科易迅11 小时前
MybatisPlus增删改查操作
android·java·数据库
SHoM SSER12 小时前
MySQL 数据库连接池爆满问题排查与解决
android·数据库·mysql
黄林晴12 小时前
Android 17 取色器 API:无需权限,一行 Intent 跨应用取色
android