Kotlin 变量

变量是任何程序的基石——它"装着"程序要处理的数据。Kotlin 的变量声明语法和 Java、JavaScript 都不太一样,有自己鲜明的特色。本篇从 val/var 讲到类型推断、字符串模板、惰性初始化。

1. val 与 var:Kotlin 最重要的两个关键字

Kotlin 声明变量必须用 valvar 开头,不像 Java/JS 那样直接 类型 名字 = 值。这两个关键字长得像但语义截然不同:

// val = value(只读),赋值后不能再改
val name = "小明"
// name = "小红"        // 编译错误!val 不可重新赋值

// var = variable(可变)
var age = 25
age = 26                 // OK
age = 27                 // OK

// 经验法则:【能用 val 就不用 var】
// 不可变代码更容易推理、更少 bug、利于并发
// IDE 会提示 "Var could be val",听它的

val(value,只读):赋值后不能再改,类似 Java 的 final、Swift 的 letvar(variable,可变):可以重新赋值。

关键经验法则:能用 val 就不用 var。不可变代码更容易推理(读到 val 你就知道它不会变),更少 race condition(多线程友好),更利于优化。Kotlin 的 IDE 会提示 "Var could be val"——听它的,把能改成 val 的都改掉。

2. 类型声明:写在变量名后面

Kotlin 的类型写在变量名后面,用冒号分隔,这点和 Swift、Rust、Go 一致,和 Java 的 Int a = 42 相反:

// 显式声明类型,类型写在变量名后面,用冒号分隔
val a: Int = 42
val b: Double = 3.14
val c: Boolean = true
val s: String = "hello"
val list: List<Int> = listOf(1, 2, 3)

// 类型推断:省略类型,编译器自动判断(更常用)
val city = "北京"        // String
val count = 100          // Int
val pi = 3.14            // Double
val nums = listOf(1, 2)  // List<Int>

// 推断发生在编译期,运行时性能无差别

为什么类型写后面?因为它常常可以省略——靠右侧的初始值推断。Java 10 之后也学了这招(var a = 1),但 Kotlin 从一开始就有,且更彻底。

什么时候要写显式类型?

3. 字符串模板 —— Kotlin 的甜点

Kotlin 字符串支持模板插值——和 shell 脚本、JavaScript ES6、Swift 类似。这是 Kotlin 比 Java 简洁的核心特性之一:

// 字符串模板:Kotlin 的甜点特性
val name = "小明"
val age = 25

// 简单变量:直接 $ 后跟变量名
println("你好, $name!")
// 输出: 你好, 小明!

// 表达式:用 ${} 包裹任意表达式
println("明年 ${age + 1} 岁")
// 输出: 明年 26 岁

println("$name 的名字有 ${name.length} 个字")
// 输出: 小明 的名字有 2 个字

// 对比 Java: "你好, " + name + "!明年 " + (age+1) + " 岁"
// Kotlin 的模板拼接明显更直观

对比 Java 那种 "你好, " + name + "!明年 " + (age+1) + " 岁" 的字符串拼接,Kotlin 的模板直观得多。再也不需要 String.formatStringBuilder 这种啰嗦写法。

更复杂的表达式也行:

val items = listOf("苹果", "香蕉", "橙子")
println("购物清单有 ${items.size} 项:${items.joinToString { it }}")
// 输出: 购物清单有 3 项:苹果, 香蕉, 橙子

// 多行字符串(原始字符串)用三个引号
val text = """
    |第一行
    |第二行
    |第三行
""".trimMargin()
println(text)

4. const val:编译期常量

对于真正不变的字面量级常量(如最大用户数、应用名、数学常数),用 const val:

// const val:编译期常量,必须放在顶层或 companion object 里
const val MAX_USERS = 100
const val APP_NAME = "MyApp"
const val PI_APPROX = 3.14159

// 编译期常量有严格限制:
// - 必须是 String 或基础类型
// - 必须有字面量初始化(不能是函数返回值)
// - 编译时就会被替换为字面量(类似 C 的 #define)

// 普通 val:运行时常量,可以是任意表达式
val randomNum = (1..100).random()
val today = java.util.Date()

const val 在编译时就会被替换为字面量,运行时不存在变量引用,性能最优。但限制也严格:必须 String 或基础类型、必须有字面量初始化、必须在顶层或 companion object 里。

5. lateinit var:延迟初始化

有些场景变量声明时还不能赋值(比如依赖注入、单元测试的 setUp 阶段),但每次访问都要可空调用 ?. 又太啰嗦。Kotlin 提供 lateinit var:

// lateinit:延迟初始化(用于 var 引用类型)
// 解决"先声明,后赋值"的场景,常见于依赖注入、单元测试
class UserService {
    lateinit var db: Database    // 声明时不赋值

    fun init(database: Database) {
        db = database            // 后面再赋值
    }

    fun use() {
        if (this::db.isInitialized) {   // 检查是否已初始化
            db.query("SELECT 1")
        }
    }
}

// 注意:
// - 只能用于 var(不能 val)
// - 只能用于引用类型(不能 Int/Double 等基础类型,用可空 + null 替代)
// - 访问未初始化的 lateinit 会抛 UninitializedPropertyAccessException

常见用途:JUnit 测试的 @Mock 字段、Spring 的 @Autowired 字段、Android 的 View 引用(在 onCreate 里初始化)。

6. by lazy:惰性初始化

和 lateinit 相反的场景:变量赋值开销大,但可能根本用不到。用 by lazy 让它第一次访问时才初始化,且只初始化一次:

// by lazy:惰性初始化(用于 val)
// 第一次访问时执行 lambda,以后直接返回缓存值
val expensive: String by lazy {
    println("开始计算...")
    Thread.sleep(1000)           // 模拟耗时操作
    "结果"
}

fun main() {
    println("程序启动")
    println(expensive)            // 触发计算,等待 1 秒
    println(expensive)            // 直接返回缓存,无等待
}
// 输出:
// 程序启动
// 开始计算...
// 结果
// 结果

// by lazy 默认线程安全(用 synchronized 锁)

典型用法:昂贵的数据库连接、配置文件解析、需要懒加载的 UI 组件。by lazy 默认线程安全(用 synchronized 锁),可以传 LazyThreadSafetyMode.NONE 关闭。

7. 变量作用域

Kotlin 变量分四种作用域:

// 作用域:变量的可见范围
val global = "顶层,整个文件可见"

class Foo {
    val field = "成员变量,绑定到 Foo 实例"

    companion object {
        val STATIC_LIKE = "伴生对象,类似 Java static"
    }

    fun bar() {
        val local = "局部变量,只在 bar 内可见"
        // 嵌套作用域可以访问外层
        listOf(1, 2, 3).forEach {
            println(local)        // OK,lambda 捕获了 local
        }
    }
}

8. 解构声明

Kotlin 支持把一个对象的字段拆开赋给多个变量,语法很优雅:

data class Point(val x: Int, val y: Int)
val p = Point(10, 20)

// 解构声明
val (x, y) = p
println("$x, $y")        // 10, 20

// 用 _ 忽略不需要的字段
val (_, yy) = p
println(yy)              // 20

// 集合也支持(基于索引)
val (a, b, c) = listOf(1, 2, 3)
println("$a $b $c")      // 1 2 3

// Map 的 entry 解构
for ((key, value) in mapOf("k1" to "v1")) {
    println("$key -> $value")
}

data class 自动生成 componentN() 函数支持解构;普通 class 需要自己用 operator fun component1() 声明。

小结

这一章你掌握了 Kotlin 变量的所有姿势:val/var、显式类型、类型推断、字符串模板、const、lateinit、by lazy、解构。下一篇我们看 Kotlin 的数据类型——这些变量到底能装什么。

← 上一篇 Kotlin 基础语法

下一篇 Kotlin 数据类型

✈️💬