第2章:变量与常量——var与val的区别、类型推断、空安全机制、lateinit与by lazy
各位同学,咱们今天聊聊Kotlin里最基础、但也最容易踩坑的东西——变量与常量。
你可能觉得,变量嘛,不就是存个数据?嗯,Kotlin在这块确实做了不少设计上的思考。我个人习惯把这一章叫做「Kotlin的变量哲学」,因为搞懂了它,你后面写代码会顺手很多。
2.1 var与val:可变与不可变
先看最直观的区别:
var name = "张三" // 可变
name = "李四" // 没问题
val age = 25 // 不可变
age = 30 // 编译报错!
var 是可变变量,val 是只读变量。注意,我说的是「只读」,不是「不可变对象」。val 保证的是引用不能变,但对象内部的状态可以变。
核心原则:能用val就别用var。
我在项目中遇到过好几次,因为滥用var导致状态难以追踪的bug。你想想看,一个变量到处被改,查起来多痛苦?
举个例子:
val list = mutableListOf("A", "B")
list.add("C") // 可以!list指向没变,但内容变了
// list = mutableListOf("D") // 不行!引用不能改
我的习惯:所有变量先用val声明,编译器报错了再改成var。这样能强制你思考「这个变量真的需要变吗?」
2.2 类型推断:Kotlin的聪明之处
Kotlin是静态类型语言,但你不必每次都写类型。编译器能自动推断:
val count = 10 // 推断为 Int
val name = "Kotlin" // 推断为 String
val price = 19.99 // 推断为 Double
但有些时候,你得显式声明类型:
val number: Long = 42 // 不写Long的话,默认是Int
val result: Any = "Hello" // 指定为Any类型
注意:类型推断不是动态类型。变量一旦确定类型,就不能再赋其他类型的值。
var value = "字符串"
value = 123 // 编译错误!
我曾经见过新手把Kotlin当JavaScript写,结果编译报错一脸懵。记住:类型推断只是省了打字,不是放弃了类型安全。
2.3 空安全机制:Kotlin最让我安心的设计
说实话,Java里最让我头疼的就是NullPointerException。Kotlin直接在类型系统层面解决了这个问题。
Kotlin的类型默认不可为空:
var name: String = "Hello"
name = null // 编译错误!
如果你确实需要空值,用 ? 标记:
var nullableName: String? = "Hello"
nullableName = null // 可以
那怎么用可空变量呢?有三种方式:
| 方式 | 语法 | 说明 |
|---|---|---|
| 安全调用 | nullableName?.length |
为null时返回null,不会崩溃 |
| Elvis运算符 | nullableName?.length ?: 0 |
为null时使用默认值 |
| 非空断言 | nullableName!!.length |
确信不为null,否则抛异常 |
避坑指南:我曾经在项目里用 !! 偷懒,结果线上崩了。从那以后,我给自己定了个规矩:!! 只能用在你100%确定不会为null的地方,比如在 if (x != null) 判断之后。
智能转换也是Kotlin的亮点:
fun printLength(text: String?) {
if (text != null) {
println(text.length) // 这里自动转为非空类型
}
}
2.4 lateinit:延迟初始化的艺术
有些变量不能在构造函数里初始化,比如Android里的视图绑定、依赖注入的字段。这时候 lateinit 就派上用场了。
lateinit var adapter: MyAdapter
fun initAdapter() {
adapter = MyAdapter() // 在使用前初始化
}
fun useAdapter() {
adapter.notifyDataSetChanged() // 安全使用
}
使用限制:
- 只能用
var,不能用val - 不能是基本类型(Int、Double等)
- 不能是可空类型
如果你在初始化前访问了 lateinit 变量,会抛出 UninitializedPropertyAccessException。嗯,这比空指针好理解多了。
我的做法:用 ::adapter.isInitialized 检查是否已初始化,尤其是在单元测试里。
if (::adapter.isInitialized) {
adapter.doSomething()
}
2.5 by lazy:懒加载的优雅方案
by lazy 和 lateinit 有点像,但更安全——它保证只初始化一次,而且是线程安全的。
val config: AppConfig by lazy {
println("初始化配置...")
loadConfigFromFile() // 第一次访问时执行
}
特点:
- 只能用
val - 第一次访问时执行lambda
- 后续访问直接返回缓存的值
- 默认线程安全(LazyThreadSafetyMode.SYNCHRONIZED)
三种模式:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| SYNCHRONIZED | 加锁,线程安全 | 多线程环境(默认) |
| PUBLICATION | 允许并发初始化,但只用一个结果 | 初始化开销小,可接受重复计算 |
| NONE | 不加锁,不保证线程安全 | 单线程环境,性能最优 |
lateinit vs by lazy 怎么选?
我个人的经验法则:
- 依赖外部注入(如Android的onCreate)→
lateinit var - 计算成本高、只需要一次 →
by lazy - 不确定初始化时机 →
lateinit var - 确定第一次访问时初始化 →
by lazy
2.6 知识体系总览
下面这张图帮你理清这一章的核心脉络:
2.7 避坑总结
最后,把我这些年踩过的坑总结一下:
- 别滥用
var——能写成val就别写成var,这是Kotlin的优雅所在。 !!是定时炸弹——我曾经在代码里用了十几个!!,后来重构时发现一半都可以用?:或?.let替代。lateinit不是懒加载——它只是延迟了初始化时机,不是按需计算。需要按需计算用by lazy。- 类型推断不是万能药——复杂表达式还是显式声明类型吧,可读性更重要。
记住一句话:Kotlin的变量设计,核心就是「安全」二字。 类型安全、空安全、初始化安全。你越理解这些设计意图,写出来的代码就越稳。
好了,这一章就到这里。变量与常量看似简单,但用好了能让你的代码质量上一个台阶。下一章咱们聊聊函数——Kotlin的函数可比Java灵活多了。
公众号:蓝海资料掘金营,微信deep3321