第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 lazylateinit 有点像,但更安全——它保证只初始化一次,而且是线程安全的。

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 知识体系总览

下面这张图帮你理清这一章的核心脉络:

Kotlin变量与常量 var vs val var: 可变引用 val: 只读引用 原则:优先用val 类型推断 静态类型,自动推断 必要时显式声明 不是动态类型! 空安全机制 ? 可空类型 ?. 安全调用 ?: Elvis运算符 慎用 !! 延迟初始化 lateinit var by lazy (val) 场景不同,选择不同 使用场景 依赖注入 → lateinit 懒加载 → by lazy 按需选择

2.7 避坑总结

最后,把我这些年踩过的坑总结一下:

  1. 别滥用 var——能写成 val 就别写成 var,这是Kotlin的优雅所在。
  2. !! 是定时炸弹——我曾经在代码里用了十几个 !!,后来重构时发现一半都可以用 ?:?.let 替代。
  3. lateinit 不是懒加载——它只是延迟了初始化时机,不是按需计算。需要按需计算用 by lazy
  4. 类型推断不是万能药——复杂表达式还是显式声明类型吧,可读性更重要。

记住一句话:Kotlin的变量设计,核心就是「安全」二字。 类型安全、空安全、初始化安全。你越理解这些设计意图,写出来的代码就越稳。

好了,这一章就到这里。变量与常量看似简单,但用好了能让你的代码质量上一个台阶。下一章咱们聊聊函数——Kotlin的函数可比Java灵活多了。


公众号:蓝海资料掘金营,微信deep3321