【Kotlin系统化精讲:叁】 | 变量与常量:自由与约束的代码博弈

228 阅读5分钟

image.png

前言

变量常量就像生活中的水杯玻璃雕塑​:水杯能随时装不同饮品(变量),玻璃雕塑一旦定型就永恒不变(常量)。但 valconst 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:生死对决

特性valconst 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)。

总结

Kotlinvarval 编织了一张精妙的安全网🕸️——var 给灵活留了门,val 用引用不可变保护了基础安全,而 const val 才是真正的"代码化石"val 不等于内容不可变,遇到集合需警惕;const val 才是编译期铁律,零开销且绝对静止。掌握这三者,你的 Kotlin 代码将兼具弹性与稳定。✨

欢迎一键四连关注 + 点赞 + 收藏 + 评论