【Kotlin系列02】变量与数据类型:从val/var到空安全的第一课

引言:那个让我损失3小时的NPE

2025年12月的某个晚上,我正在调试一个Android应用的崩溃问题。日志里清楚地写着:java.lang.NullPointerException at line 42。我花了整整3个小时,翻遍了堆栈跟踪,加了无数if (obj != null)判断,最后发现问题竟然是一个简单的变量初始化遗漏。

那一刻我意识到:在Java中,null是一个隐藏的地雷,你永远不知道它会在哪里爆炸。

而Kotlin的设计理念之一,就是要让null变得"明确而安全"。今天这篇文章,我们将学习Kotlin如何通过类型系统,在编译期就帮我们避免90%的空指针异常。

但在此之前,让我们先从最基础的变量声明开始。

val与var:不可变才是王道

为什么Kotlin有两种变量声明?

在Java中,我们用final来声明常量,但实际开发中很少有人主动加final。Kotlin的设计者认为:不可变应该是默认选择,可变才需要明确标注。

kotlin 复制代码
// val = value(不可变引用,类似Java的final)
val name = "Kotlin"
// name = "Java" // ❌ 编译错误:Val cannot be reassigned

// var = variable(可变引用)
var count = 0
count = 1 // ✅ 没问题

val的真正含义

很多初学者误以为val等于"常量",但其实它只是引用不可变

kotlin 复制代码
val list = mutableListOf(1, 2, 3)
list.add(4) // ✅ 可以,list引用没变,但内容变了
// list = mutableListOf(5, 6) // ❌ 不可以,不能重新赋值

val person = Person("Alice", 25)
person.age = 26 // ✅ 可以修改对象属性
// person = Person("Bob", 30) // ❌ 不能重新赋值

**最佳实践**:优先使用`val`,只有确实需要重新赋值时才用`var`。这不仅让代码更安全,还能帮助编译器做更多优化。

var的使用场景

虽然推荐用val,但var在以下场景不可或缺:

kotlin 复制代码
// 1. 循环计数器
var sum = 0
for (i in 1..100) {
    sum += i
}

// 2. 状态追踪
var isLoading = true
fetchData { result ->
    isLoading = false
}

// 3. 惰性初始化(后面会讲更好的方式)
var database: Database? = null
fun initDatabase() {
    database = Database.connect()
}

val vs final:Kotlin的优势

对比一下Java和Kotlin的写法:

java 复制代码
// Java:冗长,容易忘记final
final String name = "Java";
String count = "0"; // 没加final,可能被意外修改
kotlin 复制代码
// Kotlin:简洁,默认鼓励不可变
val name = "Kotlin"
var count = 0 // 明确标注这是可变的

基本数据类型:一切皆对象

Kotlin的类型哲学

在Java中,有基本类型(intboolean)和包装类型(IntegerBoolean)的区别。Kotlin统一了这种设计:所有类型都是对象,但编译器会智能地优化成基本类型。

kotlin 复制代码
val number: Int = 42 // 运行时是基本类型int
val nullableNumber: Int? = null // 运行时是包装类型Integer

数字类型

Kotlin提供了8种数字类型,和Java基本一致:

Kotlin类型 位宽 Java等价类型 取值范围
Byte 8 byte -128 到 127
Short 16 short -32,768 到 32,767
Int 32 int -2³¹ 到 2³¹-1
Long 64 long -2⁶³ 到 2⁶³-1
Float 32 float 单精度浮点数
Double 64 double 双精度浮点数
UByte 8 - 0 到 255(无符号)
UInt 32 - 0 到 2³²-1(无符号)

实际使用示例:

kotlin 复制代码
val age: Int = 25
val distance: Double = 3.14159
val bigNumber: Long = 9_999_999_999L // 下划线提升可读性
val hexValue = 0xFF // 十六进制
val binaryValue = 0b1010 // 二进制

Kotlin不支持八进制字面量(如Java的`0777`),这是为了避免混淆。

类型转换:必须显式

Java允许隐式类型转换(intlong),但Kotlin要求所有数字类型转换都必须显式

kotlin 复制代码
val intValue: Int = 100
// val longValue: Long = intValue // ❌ 编译错误

val longValue: Long = intValue.toLong() // ✅ 显式转换
val doubleValue: Double = intValue.toDouble()

这看起来麻烦,但能避免很多隐式转换导致的精度损失问题:

kotlin 复制代码
val a = 1000000
val b = 1000000
println(a * b) // 溢出!结果是负数

val a = 1000000L
val b = 1000000L
println(a * b) // ✅ 1000000000000

布尔类型

kotlin 复制代码
val isKotlinAwesome: Boolean = true
val hasError = false // 类型推断为Boolean

// 逻辑运算
val result = isKotlinAwesome && !hasError
val flag = isKotlinAwesome || hasError

字符与字符串

kotlin 复制代码
val letter: Char = 'A'
val unicode: Char = '\u0041' // 也是'A'

// 字符串
val greeting: String = "Hello, Kotlin"
val multiline = """
    第一行
    第二行
    第三行
""".trimIndent()

类型推断:让编译器为你工作

智能的类型推断

Kotlin编译器非常聪明,大多数情况下你不需要显式声明类型:

kotlin 复制代码
val name = "Kotlin" // 推断为String
val age = 25 // 推断为Int
val price = 19.99 // 推断为Double
val isValid = true // 推断为Boolean

val items = listOf(1, 2, 3) // 推断为List<Int>
val map = mapOf("key" to "value") // 推断为Map<String, String>

什么时候需要显式类型?

虽然类型推断很强大,但以下情况建议显式声明类型:

kotlin 复制代码
// 1. 公开API(提升可读性)
fun getUserAge(): Int { // 明确返回类型
    return 25
}

// 2. 需要更宽泛的类型
val numbers: List<Int> = mutableListOf(1, 2, 3) // 接口类型
// 而不是 val numbers = mutableListOf(1, 2, 3) // MutableList<Int>

// 3. 初始值类型不明确
val config: Config = loadConfig() // 如果loadConfig返回类型复杂

不要过度使用类型推断!如果代码可读性因此下降,就该显式声明类型。

字符串:模板与多行

字符串模板

Kotlin的字符串模板比Java的String.format+拼接优雅太多:

kotlin 复制代码
val name = "Alice"
val age = 25

// Java方式(Kotlin也支持)
val msg1 = "My name is " + name + " and I'm " + age + " years old."

// Kotlin字符串模板
val msg2 = "My name is $name and I'm $age years old."

// 表达式插值
val msg3 = "Next year I'll be ${age + 1} years old."
val msg4 = "Name length: ${name.length}"

原始字符串(Raw String)

处理正则表达式、JSON、SQL时特别有用:

kotlin 复制代码
// 普通字符串需要转义
val regex1 = "\\d{3}-\\d{4}"

// 原始字符串,无需转义
val regex2 = """\d{3}-\d{4}"""

// 多行JSON
val json = """
{
  "name": "Alice",
  "age": 25,
  "skills": ["Kotlin", "Java", "Python"]
}
""".trimIndent()

字符串常用操作

kotlin 复制代码
val text = "  Kotlin Programming  "

// 长度和索引
println(text.length) // 22
println(text[2]) // 'K'

// 修剪
println(text.trim()) // "Kotlin Programming"

// 分割
val words = text.trim().split(" ") // ["Kotlin", "Programming"]

// 替换
println(text.replace("Kotlin", "Java"))

// 检查
println(text.contains("Kotlin")) // true
println(text.startsWith("  Kotlin")) // true
println(text.endsWith("ing  ")) // true

// 大小写
println(text.uppercase()) // "  KOTLIN PROGRAMMING  "
println(text.lowercase())

数组:固定大小的集合

创建数组

kotlin 复制代码
// 使用arrayOf创建
val numbers = arrayOf(1, 2, 3, 4, 5)
val names = arrayOf("Alice", "Bob", "Charlie")

// 指定大小和初始化函数
val squares = Array(5) { i -> i * i } // [0, 1, 4, 9, 16]

// 基本类型数组(性能优化)
val intArray = intArrayOf(1, 2, 3) // IntArray
val doubleArray = doubleArrayOf(1.0, 2.0, 3.0) // DoubleArray

数组操作

kotlin 复制代码
val fruits = arrayOf("Apple", "Banana", "Cherry")

// 访问元素
println(fruits[0]) // "Apple"
println(fruits.get(1)) // "Banana"

// 修改元素
fruits[0] = "Avocado"

// 遍历
for (fruit in fruits) {
    println(fruit)
}

// 带索引遍历
for ((index, fruit) in fruits.withIndex()) {
    println("$index: $fruit")
}

// 常用方法
println(fruits.size) // 3
println(fruits.first()) // "Avocado"
println(fruits.last()) // "Cherry"
println(fruits.contains("Banana")) // true

数组 vs 集合

**重要区别**: - **Array**:固定大小,可变内容,性能高 - **List**:大小可变/不可变,更灵活,功能更丰富

实际开发中,优先使用List,只有在性能关键场景或需要与Java互操作时才用Array

kotlin 复制代码
// 数组转列表
val array = arrayOf(1, 2, 3)
val list = array.toList()

// 列表转数组
val list2 = listOf(4, 5, 6)
val array2 = list2.toTypedArray()

空安全:Kotlin最强杀手锏

null的问题

Tony Hoare(null的发明者)曾说:"null引用是我犯过的十亿美元错误"。在Java中:

java 复制代码
String name = null;
System.out.println(name.length()); // 💥 NullPointerException

Kotlin的解决方案:将空性(nullability)纳入类型系统。

可空类型

在Kotlin中,类型默认不能为null

kotlin 复制代码
var name: String = "Kotlin"
// name = null // ❌ 编译错误:Null can not be a value of a non-null type String

如果确实需要null,必须使用可空类型 (在类型后加?):

kotlin 复制代码
var nullableName: String? = "Kotlin"
nullableName = null // ✅ 可以

安全调用操作符 ?.

kotlin 复制代码
val name: String? = null

// Java方式
if (name != null) {
    println(name.length)
}

// Kotlin安全调用
println(name?.length) // null(不会抛出异常)

// 链式调用
val company: Company? = getCompany()
val ceoName = company?.ceo?.name // 任何一环为null,结果就是null

Elvis操作符 ?:

提供默认值:

kotlin 复制代码
val name: String? = null

// Java方式
val length = name != null ? name.length : 0

// Kotlin Elvis操作符
val length = name?.length ?: 0

// 实用示例
fun getUserName(user: User?): String {
    return user?.name ?: "Guest"
}

非空断言 !!

如果你100%确定某个值不是null,可以用!!强制解包:

kotlin 复制代码
val name: String? = "Kotlin"
val length = name!!.length // 如果name是null,抛出NPE

**慎用`!!`**:这是Kotlin中唯一可能导致NPE的操作符。只在你绝对确定不会为null时使用,或者接受NPE的可能性时使用(如单元测试中)。

安全转换 as?

kotlin 复制代码
val obj: Any = "Kotlin"

// 不安全转换(可能抛出ClassCastException)
val str: String = obj as String

// 安全转换(失败返回null)
val num: Int? = obj as? Int // null

let函数:处理可空值的优雅方式

kotlin 复制代码
val name: String? = "Kotlin"

// 传统方式
if (name != null) {
    println(name.length)
    println(name.uppercase())
}

// 使用let
name?.let {
    println(it.length)
    println(it.uppercase())
}

// 实用示例:链式处理
val result = getUserById(123)
    ?.let { user ->
        user.email
    }
    ?.let { email ->
        sendEmail(email)
    }
    ?: println("User not found")

空安全最佳实践

  1. 优先使用非空类型:只有确实需要null时才用可空类型
  2. 避免!!:99%的情况下都有更好的替代方案
  3. ?:提供默认值:让代码更健壮
  4. ?.let处理可空逻辑:比if-not-null更优雅
  5. 函数参数尽量非空:将null检查推到边界
kotlin 复制代码
// ❌ 不好:到处都是null检查
fun processUser(name: String?, email: String?, age: Int?) {
    if (name != null && email != null && age != null) {
        // 实际逻辑
    }
}

// ✅ 好:在边界处理null
fun processUser(name: String, email: String, age: Int) {
    // 直接使用,无需检查
}

fun createUser(name: String?, email: String?, age: Int?): User? {
    if (name == null || email == null || age == null) {
        return null // 或抛出异常
    }
    return User(name, email, age)
}

实战:构建一个用户信息验证器

让我们综合运用本文所学知识,构建一个实用的用户信息验证器:

kotlin 复制代码
data class User(
    val name: String,
    val email: String,
    val age: Int,
    val phone: String?
)

class UserValidator {
    fun validate(
        name: String?,
        email: String?,
        age: Int?,
        phone: String?
    ): Result<User> {
        // 验证姓名
        val validName = name?.trim()?.takeIf { it.isNotEmpty() }
            ?: return Result.failure(Exception("Name cannot be empty"))

        // 验证邮箱
        val validEmail = email?.trim()?.takeIf { it.contains("@") }
            ?: return Result.failure(Exception("Invalid email format"))

        // 验证年龄
        val validAge = age?.takeIf { it in 0..150 }
            ?: return Result.failure(Exception("Age must be between 0 and 150"))

        // 验证电话(可选)
        val validPhone = phone?.trim()?.takeIf { it.length >= 10 }

        return Result.success(User(validName, validEmail, validAge, validPhone))
    }
}

// 使用示例
fun main() {
    val validator = UserValidator()

    // 场景1:所有信息完整
    val result1 = validator.validate("Alice", "alice@example.com", 25, "1234567890")
    result1.onSuccess { user ->
        println("User created: ${user.name}, ${user.email}")
    }.onFailure { error ->
        println("Validation failed: ${error.message}")
    }

    // 场景2:缺少姓名
    val result2 = validator.validate(null, "bob@example.com", 30, null)
    result2.onFailure { error ->
        println("Error: ${error.message}") // "Name cannot be empty"
    }

    // 场景3:年龄无效
    val result3 = validator.validate("Charlie", "charlie@example.com", 200, null)
    result3.onFailure { error ->
        println("Error: ${error.message}") // "Age must be between 0 and 150"
    }
}

代码亮点分析:

  1. 空安全链式调用name?.trim()?.takeIf { it.isNotEmpty() }
  2. Elvis操作符提供错误信息?: return Result.failure(...)
  3. 可选参数处理phone可以为null,不影响用户创建
  4. 函数式错误处理 :使用Result类型而非异常抛出

常见问题

Q1: val和const val有什么区别?

kotlin 复制代码
// val:运行时常量
val runtimeValue = System.currentTimeMillis()

// const val:编译时常量,只能用于顶层或object中
const val COMPILE_TIME_CONSTANT = "Kotlin"

object Config {
    const val MAX_SIZE = 100
    const val API_URL = "https://api.example.com"
}

区别

  • const val必须是编译时就能确定的值(字符串、数字、布尔)
  • const val会被内联到使用处,性能更好
  • val可以是任何类型,可以延迟计算

Q2: 为什么String是不可变的?

kotlin 复制代码
var str = "Hello"
str += " World" // 实际上创建了新字符串对象

原因

  1. 线程安全:不可变对象天然线程安全
  2. 缓存优化:字符串池可以复用相同内容
  3. 安全性:避免意外修改

Q3: 什么时候用Array,什么时候用List?

场景 推荐 原因
一般业务逻辑 List 更灵活,功能丰富
性能关键代码 Array 更接近底层,性能高
与Java互操作 Array Java API通常用数组
不可变集合 List listOf()创建的是不可变List

Q4: 如何在Android中避免NPE?

kotlin 复制代码
// 1. 使用ViewModel的LiveData/StateFlow(天然空安全)
class UserViewModel : ViewModel() {
    private val _user = MutableLiveData<User?>()
    val user: LiveData<User?> = _user
}

// 2. 使用let处理可空回调
button.setOnClickListener {
    user.value?.let { safeUser ->
        navigateToProfile(safeUser)
    }
}

// 3. 使用requireContext()而非nullable的context
class MyFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val context = requireContext() // 抛出异常而非返回null
    }
}

总结

今天我们学习了Kotlin变量和类型系统的核心知识:

  1. val vs var :优先使用val(不可变引用),明确可变性
  2. 类型推断:让编译器为你工作,但不要牺牲可读性
  3. 数字类型:显式转换,避免隐式精度损失
  4. 字符串模板$variable${expression},告别拼接地狱
  5. 数组:固定大小的集合,优先用List,性能关键才用Array
  6. 空安全 :可空类型?、安全调用?.、Elvis?:、let函数

最重要的收获 :Kotlin的类型系统不是限制,而是编译期的安全网。它让你在写代码时就发现问题,而不是等到线上崩溃时才发现。

下一篇文章,我们将学习控制流与函数基础 ,包括if/when表达式、循环、函数定义等。这些工具将让你能够编写更复杂的逻辑。

练习题

巩固今天的知识,尝试完成以下练习:

  1. 类型推断练习
kotlin 复制代码
// 不显式声明类型,让编译器推断
val a = ...
val b = ...
val c = ...
// 使得:a是Int,b是Double,c是String?
  1. 字符串模板练习
kotlin 复制代码
// 用字符串模板重写这个函数
fun formatUserInfo(name: String, age: Int, city: String?): String {
    // 返回格式:"Name: Alice, Age: 25, City: Beijing"
    // 如果city为null,显示"Unknown"
}
  1. 空安全练习
kotlin 复制代码
// 重构这个函数,使用?.、?:、let等操作符
fun getUserEmailLength(user: User?): Int {
    if (user != null) {
        if (user.email != null) {
            return user.email.length
        }
    }
    return 0
}

答案在文章评论区,或者你可以在Kotlin Playground中自己尝试!


有疑问?欢迎在评论区留言讨论!如果觉得有帮助,请点赞👍和分享🔗


系列文章导航:


如果这篇文章对你有帮助,欢迎点赞、收藏、分享!有任何问题或建议,欢迎在评论区留言讨论。让我们一起学习,一起成长!

也欢迎访问我的个人主页发现更多宝藏资源

相关推荐
alonewolf_992 小时前
深入理解MySQL事务与锁机制:从原理到实践
android·数据库·mysql
superman超哥2 小时前
Context与任务上下文传递:Rust异步编程的信息高速公路
开发语言·rust·编程语言·context与任务上下文传递·rust异步编程
深海呐2 小时前
Android WebView吊起软键盘遮挡输入框的问题解决
android·webview·android 键盘遮挡·webview键盘遮挡
摘星编程2 小时前
RAG的下一站:检索增强生成如何重塑企业知识中枢?
android·人工智能
fatiaozhang95273 小时前
基于slimBOXtv 9.19 V2(通刷S905L3A/L3AB)ATV-安卓9-通刷-线刷固件包
android·电视盒子·刷机固件·机顶盒刷机·slimboxtv9.19v2·slimboxtv
左绍骏3 小时前
01.学习预备
android·java·学习
鹏程十八少3 小时前
破解Android悬浮窗遮挡无障碍服务难题:我在可见即可说上踩过的坑
android·前端·面试
Kapaseker3 小时前
前端已死...了吗
android·前端·javascript
Winston Wood4 小时前
Android图形与显示系统经典故障解决方案:从源码到实操
android·图形系统·显示系统