前言
变量和常量就像生活中的水杯与玻璃雕塑:水杯能随时装不同饮品(变量),玻璃雕塑一旦定型就永恒不变(常量)。但 val 和 const val 藏着意想不到的玄机——有些"常量"竟会偷偷变化?
本章节将带你捅破变量与常量的窗户纸,避开新手90%的踩坑点。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
变量:var & val 🎭
var: 可变变量
- 核心特性:变量可多次重新赋值,赋予数据 二次生命 的能力。
var score = 90 score = 95 // 分数重生 → ✅ - 声明及初始化:类型一旦确定后不可修改!隐式声明时取决于第一次赋值的类型。
var age: Int = 25 // 显式声明 var age = 18 // 隐式声明 age = "二十六" // ❌ 编译报错!类型已锁定为 Int💡 破解之道:可声明为
Any类型,但需警惕类型安全风险。
val:只读变量(运行时常量)
- 核心特性(
不可变性):只能赋值一次,不可重新赋值。 官方推荐的默认选择。尽可能地使用val可避免意外的状态改变,提高代码的可预测性和线程安全性。 - 声明及初始化:声明时必须初始化,或在类构造函数中初始化,或使用
lateinit或by lazy等委托进行延迟初始化。初始化后,其引用(对于对象类型)或值(对于基本类型)就不能再被改变。 - 颠覆认知(
对象内容可篡改):只读≠内容不可变!val androidVersions = arrayListOf("Oreo", "Pie") androidVersions.add("Q") // ✅ 集合内容被修改! androidVersions = newList // ❌ 引用不可变🧩 本质原因:
val只保证引用地址不变(购物车还是那辆车),但车里的货物(对象内容)随便改! - 智能转换魔法:类型推断升级
val data: Any = "Kotlin" if (data is String) { print(data.length) // ✅ 编译器自动转为 String 类型 }⚡ 编译器秘密:在作用域内自动插入类型转换字节码。
- 延迟初始化:
lateinit var的替代方案val config: Configuration by lazy { loadConfigFromServer() // 首次访问时初始化 }🌟 优势:线程安全 + 按需加载 + 不可变引用
常量:const val(编译期常量) ⏳
const val:真正的化石
核心目的是声明编译期常量。 想要彻底不可变?用 const val:
// 必须定义在顶层或 object 中
const val PI = 3.14159
const val COMPANY_NAME = "AndroidLab"
class MyClass {
companion object {
const val API_KEY = "ABCDEF123456" // 注意:实际API密钥应安全存储,这仅为示例
const val VERSION_CODE = 1
}
}
特性:
- 必须是顶层属性或
object声明的成员。 - 必须在编译时就能确定值及其类型。
- 只能是基本类型或
String。 - 不能有自定义的
getter。
编译期优化:
编译器会在编译期间直接用字面值替换所有使用该常量的地方,类似于宏替换(没有运行时的变量读取开销)。
val vs const val:生死对决
| 特性 | val | const val (编译时常量) |
|---|---|---|
| 关键特性 | 运行时只读变量,引用不可变 | 编译期常量,值内联 |
| 声明位置 | 任何作用域(函数内、类属性等) | 仅顶层或 object(含伴生对象)内 |
| 允许的类型 | 所有类型 | 仅基本类型 (Int等) 和 String |
| 初始化要求 | 运行时初始化 | 编译时初始化(字面量/其他const val) |
自定义 getter | 可以有 | 不能有 |
使用 lateinit | 可以 | 不能 |
by lazy | 可以 | 不能 |
| 内存开销 | 运行时存在变量 | 编译后替换为字面量,无运行时变量 |
| 主要用途 | 表示程序运行期间不变的引用 | 定义字面值的命名常量,用于注解参数等 |
🌰 举个栗子:
val currentTime: Long
get() = System.currentTimeMillis() // 每次调用值都变!
const val MAX_USERS = 1000 // 恒等于 1000
深入特性:藏在代码缝里的魔鬼 😈
类型推断的“障眼法”
Kotlin 能自动推断类型,但玩过头会翻车:
val number = 100 // 自动推断为 Int
number = 10.5 // ❌ 拒绝!Int 怎能变 Double?
急救方案:显式声明类型避免误伤:
val number: Number = 100 // 声明为父类
number = 10.5 // ✅ 此时合法!
幕后字段的“分身术”
自定义 getter/setter 时,偷偷生成幕后字段(field):
var name: String = ""
set(value) {
field = value.trim() // field 是隐藏的存储变量
}
企业级实践:老司机的求生法则 🚀
- 优先
val:
除非明确需要修改,否则全用val——减少意外篡改(官方推荐)。 const val使用场景:- 魔法数字(如超时时间、错误码)。
- 全局配置(API 密钥、常量字符串)。
const val API_TIMEOUT = 30_000- 警惕可变
val!
如果val指向可变对象(如ArrayList),加防御性拷贝:val safeList = Collections.unmodifiableList(rawList) - 变量命名防混淆:
- 变量:驼峰命名(
userName)。 - 常量:全大写
+下划线(USER_LIMIT)。
- 变量:驼峰命名(
总结
Kotlin 用 var 和 val 编织了一张精妙的安全网🕸️——var 给灵活留了门,val 用引用不可变保护了基础安全,而 const val 才是真正的"代码化石"。
val 不等于内容不可变,遇到集合需警惕;const val 才是编译期铁律,零开销且绝对静止。掌握这三者,你的 Kotlin 代码将兼具弹性与稳定。✨
欢迎一键四连(
关注+点赞+收藏+评论)