A
第 42 章KOTLIN45 分钟

Kotlin 速成

面向有部分安卓基础的同学的 Kotlin 速成:变量与类型、函数与默认参数、类与对象、空安全、扩展函数、作用域函数、与 Java 互操作

学习目标

  • 掌握 val/var 与类型推断、字符串模板与可空类型系统
  • 理解函数默认参数、命名参数、扩展函数与高阶函数
  • 熟练使用 constructor、init、data class、sealed class 与 object 单例
  • 掌握 ?. !! ?: let 等空安全操作符与作用域函数
  • 了解 Kotlin 与 Java 互操作的 @JvmStatic / @JvmField / 顶层定义

学习目标

从本章起我们正式进入 Kotlin + Compose 的现代 Android 开发。Kotlin 是 Google 自 2017 年起钦定的 Android 一级开发语言,目前新项目几乎默认使用 Kotlin,Jetpack Compose 也只能用 Kotlin 编写。如果你写过 Java,会发现 Kotlin 在保留 JVM 生态的同时大幅降低了样板代码量、把空安全内建到类型系统里。

本章假设你已经了解 Activity、生命周期、布局等基础概念,但 不要求 你精通 Java。读完本章你将能够:

  • 看懂 Android Studio 模板生成的 Kotlin 版 MainActivity;
  • 用 val / var、字符串模板、可空类型写出符合 Kotlin 习惯的代码;
  • 用默认参数、命名参数、扩展函数简化 API;
  • 用 data class / sealed class / object 替代 Java 中冗长的等价写法;
  • 用 ?. ?: !! 与作用域函数优雅地处理可空值;
  • 知道 Kotlin 与 Java 互操作时需要加哪些注解。

变量与数据类型

1. val 与 var

Kotlin 用 val(value,只读引用)和 var(variable,可变引用)取代 Java 的 final 与非 final:

val name = "Alice"      // 推断为 String,不可重新赋值
var age = 18            // 推断为 Int,可重新赋值
age = 19

// name = "Bob"         // ❌ 编译错误:val 不能重新赋值

经验法则:默认全部用 val,确实需要可变时再改 var。这能让代码更容易推理、并发更安全。注意 val 只保证引用不变,引用指向的对象内部状态仍可变:

val list = mutableListOf(1, 2, 3)
list.add(4)             // ✅ list 引用本身不变,但内容变了

2. 基本类型与类型推断

Kotlin 中一切皆对象,没有 Java 的“基本类型 vs 包装类”二分。常见类型:

类型 示例 说明
Int 42 等价 Java int,编译后在 JVM 上自动拆为基本类型
Long 42L 长整型
Float / Double 1.5f / 1.5 浮点
Boolean true / false 布尔
Char 'A' 单字符
String "hello" 字符串
Array<Int> arrayOf(1, 2, 3) 数组

数字字面量支持下划线分隔与十六进制/二进制:1_000_000、0xFF、0b1010。Int 在 JVM 上编译为基本 int,但声明为 Int? 时会装箱为 java.lang.Integer。

3. 字符串模板

Kotlin 支持 $ 模板与 ${} 表达式:

val name = "Alice"
val age = 18
println("姓名:$name,明年 ${age + 1} 岁")

// 多行字符串
val sql = """
    SELECT * FROM user
    WHERE age > $age
""".trimIndent()

trimIndent() 会去掉每行的公共前缀空白,常用于写 SQL / JSON / 正则。

4. 类型转换

Kotlin 不支持 Java 的隐式数字转换,必须显式:

val a: Int = 10
// val b: Long = a          // ❌ 类型不匹配
val b: Long = a.toLong()    // ✅ 显式转换
val c: String = a.toString()
val d: Int = "42".toInt()

这避免了 byte + byte = int 这类隐式扩位带来的坑。

控制流

1. if 是表达式

Kotlin 的 if 有返回值,可以直接代替三元运算符:

val grade = if (score >= 60) "及格" else "不及格"

2. when(强化版 switch)

when 既能做值匹配,也能做范围、类型、条件匹配,且是表达式:

fun describe(obj: Any): String = when (obj) {
    1, 2, 3       -> "小数字"
    in 4..10      -> "中数字"
    is String     -> "字符串长度 ${obj.length}"
    is Int        -> "大整数"
    else          -> "未知"
}

when 必须穷尽所有可能(编译器会在需要时强制写 else),这是 sealed class 模式匹配的基础。

3. for 与 while

for (i in 0..5) print(i)              // 01234 闭区间
for (i in 0 until 5) print(i)         // 01234 半开区间
for (i in 6 downTo 0 step 2) print(i) // 6420
for (item in list) println(item)
for ((index, value) in list.withIndex()) println("$index: $value")

函数

1. 基本语法

fun add(a: Int, b: Int): Int {
    return a + b
}

// 单表达式函数
fun add2(a: Int, b: Int): Int = a + b

// 返回 Unit(无返回值)可省略
fun log(msg: String) { println(msg) }

2. 默认参数与命名参数

Kotlin 直接支持默认值,不必像 Java 那样写一堆重载:

fun greet(name: String, greeting: String = "Hello", punct: Char = '!') {
    println("$greeting, $name$punct")
}

greet("Alice")                                // Hello, Alice!
greet("Bob", greeting = "Hi")                 // Hi, Bob!
greet("Carol", punct = '?')                   // Hello, Carol?
greet(name = "Dave", greeting = "Hey")        // Hey, Dave!

命名参数在参数较多时非常实用。注意:从 Kotlin 1.4 起,调用 Java 方法时也可使用命名参数(之前不行)。

3. 可变参数 vararg

fun sum(vararg nums: Int): Int = nums.sum()

sum(1, 2, 3)              // 6
val arr = intArrayOf(4, 5, 6)
sum(*arr)                 // 用 * 展开

4. 顶层函数与局部函数

Kotlin 允许把函数直接写在文件顶层(不在任何类里),编译后变成 Java 的静态方法:

// Utils.kt
fun isEmail(s: String): Boolean = s.contains("@")

也可以在函数内部定义局部函数,便于拆分逻辑又不污染外部:

fun process(data: String) {
    fun validate(input: String) { require(input.isNotEmpty()) }
    validate(data)
    // ...
}

类与对象

1. 主构造方法与 init

Kotlin 把“构造方法”放在类头,叫主构造方法,配合 init 块做初始化:

class User(val name: String, var age: Int) {
    init {
        require(age >= 0) { "age 不能为负" }
        println("创建 User: $name")
    }
}

val u = User("Alice", 18)
println(u.name)        // 主构造参数前加 val/var 即自动生成属性
u.age = 19

2. 次构造方法

如果需要重载,写 constructor 委托主构造:

class User(val name: String, var age: Int) {
    constructor(name: String) : this(name, 0)
    constructor() : this("unknown", 0)
}

实际项目里优先用默认参数替代次构造方法,可读性更好。

3. 继承与重写

Kotlin 类默认是 final 的,要被继承必须加 open;方法同理:

open class Animal(val name: String) {
    open fun speak() = "$name: ..."
}

class Dog(name: String) : Animal(name) {
    override fun speak() = "$name: Woof"
}

Kotlin 用 : 同时表达继承和实现,没有 Java 的 extends / implements。

4. data class

data class 自动生成 equals / hashCode / toString / copy / componentN:

data class Point(val x: Int, val y: Int)

val p1 = Point(1, 2)
val p2 = Point(1, 2)
println(p1 == p2)            // true(自动 equals)
println(p1)                 // Point(x=1, y=2)

val (x, y) = p1             // 解构声明
val p3 = p1.copy(y = 5)     // copy 时只改部分字段

限制:data class 不能被继承(默认 final)、不能有 var 属性以外的可变集合语义重写、不适合做层级继承。

5. sealed class

sealed class 表示受限的类层级,子类只能定义在同一文件(或同一包的同一模块)内。它的最大价值是与 when 配合做穷尽匹配:

sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String) : UiState<Nothing>()
}

fun render(state: UiState<String>): String = when (state) {
    is UiState.Loading       -> "加载中"
    is UiState.Success       -> "成功:${state.data}"
    is UiState.Error         -> "失败:${state.message}"
    // 不需要 else:新增子类时编译器会强制补全分支
}

Compose 中的状态管理大量用 sealed class 表达“加载中 / 成功 / 失败”三态。

6. object 单例与伴生对象

Kotlin 用 object 一行声明单例(线程安全初始化):

object AppConfig {
    val version = "1.0.0"
    fun isDebug() = true
}
// AppConfig.version 直接使用

// 伴生对象:相当于 Java 的 static
class User(val name: String) {
    companion object {
        const val KEY = "user"
        fun fromJson(json: String): User = User(json)
    }
}
User.KEY            // 等价 Java 静态字段
User.fromJson("{}")

7. 枚举

enum class Direction(val degree: Int) {
    NORTH(0), EAST(90), SOUTH(180), WEST(270);

    fun isHorizontal() = this == EAST || this == WEST
}

枚举和 sealed class 都用于“有限取值”,区别:枚举所有实例同类型,sealed 子类可各自有不同字段。

空安全

1. 可空类型 ?

Kotlin 把“是否可空”放进类型系统:String 不可空,String? 可空:

var a: String = "abc"
// a = null           // ❌ 编译错误

var b: String? = "abc"
b = null              // ✅

// val len = b.length  // ❌ 编译错误:b 可能为 null

2. 安全调用 ?.

?. 在接收者为 null 时直接返回 null,否则继续调用:

val name: String? = getNullableName()
val len = name?.length          // Int? :name 为 null 时 len 为 null
val upper = name?.uppercase()   // String?

链式调用尤其方便:user?.profile?.email。

3. Elvis ?:

?: 提供“为 null 时的默认值”:

val display = name ?: "匿名"      // name 非 null 取 name,否则取 "匿名"
val len = name?.length ?: 0

4. 非空断言 !!

!! 强制把可空转为非空,若为 null 则抛 NullPointerException:

val forced = name!!

只在确认不会为 null 时用,否则视为代码异味。生产代码尽量用 ?: 处理默认值。

5. let 与可空

name?.let {
    // 仅在 name 非 null 时执行,块内 it 自动非空
    println(it.length)
}

6. 与 Java 互操作时的空安全

Java 代码返回的对象在 Kotlin 中是平台类型 String!(不知道是否为 null),需谨慎处理:

val text: String = javaMethod()    // 风险:可能实际为 null
val safe: String? = javaMethod()  // 显式声明可空更安全

AndroidX 大量使用 @Nullable / @NonNull 注解,Kotlin 编译器会据此推断可空性。

扩展函数与扩展属性

Kotlin 允许给已有类“追加”方法,无需继承:

fun String.shout() = uppercase() + "!!!"

println("hello".shout())          // HELLO!!!

fun List<Int>.averageSafe(): Double =
    if (isEmpty()) 0.0 else sum().toDouble() / size

// 扩展属性
val String.firstChar: Char?
    get() = if (isEmpty()) null else this[0]

本质:扩展函数是静态解析的语法糖,编译为 StringUtil.shout("hello"),没有真正修改类。不能被覆写,也无法访问私有成员。

Android 中常用扩展把“业务无关的工具方法”从 Activity / View 抽出:

fun View.visible() { visibility = View.VISIBLE }
fun View.gone() { visibility = View.GONE }
fun View.dp(value: Int) = (value * resources.displayMetrics.density).toInt()

Lambda 与高阶函数

1. Lambda 表达式

val sum: (Int, Int) -> Int = { a, b -> a + b }
val sum2 = { a: Int, b: Int -> a + b }
println(sum(1, 2))                // 3

类型 (Int, Int) -> Int 表示接收两个 Int 返回 Int 的函数。

2. 高阶函数

把函数当参数传:

fun compute(a: Int, b: Int, op: (Int, Int) -> Int): Int = op(a, b)

compute(2, 3) { x, y -> x * y }       // 6
compute(2, 3) { x, y -> x + y }      // 5

当最后一个参数是函数类型时,lambda 可以写到括号外,称为 trailing lambda,是 Kotlin 风格特色。

3. it 隐式参数

如果 lambda 只有一个参数,可省略参数声明,用 it 指代:

list.map { it * 2 }
list.filter { it > 0 }

4. 函数类型与别名

typealias ClickHandler = (View) -> Unit

fun setListener(handler: ClickHandler) { /* ... */ }

5. 内联 inline

inline 修饰高阶函数时会把函数体直接内联到调用点,避免额外对象分配(lambda 默认会被编译为 Function1 实例):

inline fun measureTime(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    println(System.currentTimeMillis() - start)
}

Kotlin 标准库的 let / run / forEach / map 等都是 inline,所以使用它们几乎零开销。

6. 作用域函数:let / run / apply / also / takeIf

五个标准函数容易混淆,记两个维度:

函数 引用方式 返回值 典型用途
let it lambda 结果 链式 / 空判断
run this lambda 结果 多步计算
apply this 对象本身 初始化配置
also it 对象本身 副作用 / 链式日志
takeIf this this? 条件保留
val user = User("Alice", 18).apply {
    age = 20
}                                       // 返回配置后的 user

val name = User("Bob", 0)
    .also { println("创建: $it") }       // 副作用 + 返回原对象
    .takeIf { it.age > 0 }              // 条件为真返回自身,否则 null
    ?.name

经验:配置用 apply,副作用用 also,链式转换用 let,条件过滤用 takeIf。

与 Java 互操作

1. 顶层函数的 @JvmStatic

顶层函数默认编译为文件类(如 UtilsKt)的静态方法,调用方需写 UtilsKt.isEmail(...)。如想自定义类名或保持静态调用形式:

// 文件顶部
@file:JvmName("StringUtils")

package com.example.util

fun isEmail(s: String): Boolean = s.contains("@")

// 在 companion object 中
class User(val name: String) {
    companion object {
        @JvmStatic
        fun fromJson(json: String): User = User(json)
    }
}

Java 端:User.fromJson(...) 直接调用,省略 Companion。

2. @JvmField 暴露字段

Kotlin 属性默认生成 getter(var 还会生成 setter)。若想直接暴露为 Java 字段(如常量):

class Config {
    @JvmField
    val MAX_RETRY = 3

    companion object {
        const val KEY = "user"   // const 编译为 static final 常量
    }
}

Java 端:Config.KEY / new Config().MAX_RETRY 直接访问字段。

3. @JvmOverloads 生成重载

带默认参数的 Kotlin 函数,Java 调用方看不到默认值版本。加 @JvmOverloads 让编译器自动生成所有重载:

@JvmOverloads
fun greet(name: String, greeting: String = "Hello", punct: Char = '!') { /* ... */ }

Java 端会得到三个重载:greet(name)、greet(name, greeting)、greet(name, greeting, punct)。

4. 集合互操作

Kotlin 的 List 是只读的,MutableList 对应 Java List。Java 端传入的 java.util.List 在 Kotlin 中是平台类型,建议手动声明 MutableList<String>?。Kotlin 集合与 Java 集合在运行时是同一个对象,没有拷贝开销。

5. 平台类型与可空性

Java 返回的 String 在 Kotlin 中是 String!,既可以当 String 也可以当 String?。最佳实践:跨边界调用时显式声明可空类型,避免运行时 NPE:

// Java: public String getName()
val name: String? = javaObj.getName()   // 显式可空,强制处理

完整示例:用 Kotlin 写一个用户仓库

把以上知识点串起来:

data class User(val id: Long, val name: String, val age: Int)

sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Failure(val error: String) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

class UserRepository {
    private val cache = mutableMapOf<Long, User>()

    fun findById(id: Long): Result<User> {
        cache[id]?.let { return Result.Success(it) }
        return run {
            // 模拟网络/数据库
            Thread.sleep(100)
            val user = User(id, "User-$id", (1..80).random())
            cache[id] = user
            Result.Success(user)
        }
    }
}

fun main() {
    val repo = UserRepository()
    when (val result = repo.findById(1L)) {
        is Result.Success -> println("加载成功:${result.data.name}")
        is Result.Failure -> println("失败:${result.error}")
        Result.Loading   -> println("加载中")
    }
}

这段代码体现了 Kotlin 的核心优势:data class 消除样板、sealed class + when 表达有限状态、?.let 处理缓存命中、run 包裹多步逻辑。

常见坑与最佳实践

  1. 滥用 var 与可变集合:默认 val 与只读 List,确实需要可变再换。Kotlin 集合分 List / MutableList,用错容易在并发场景出 bug。
  2. !! 满天飞:每次写 !! 都要质疑“是否真的不会为 null”。常见根因是 Java 平台类型没显式声明可空。
  3. when 不写 else 漏分支:sealed class 增子类时如果用了 else,编译器不会提示漏分支,可能引入 bug。sealed 配合 when 时尽量不用 else。
  4. 用扩展函数“污染”标准类型:给 String 加各种业务扩展会让自动补全爆炸。建议收到 Util 文件 / 单独 namespace 下,或限制在项目包内。
  5. 作用域函数乱用:apply 返回对象本身,链式中误把 let 当 apply 会丢返回值。手写代码先想清楚返回值要什么。
  6. companion object 里的 const:const val 只能修饰基本类型和 String,且必须放在 companion 或顶层;普通 val 在 companion 里是运行时常量,每次访问都重新计算。
  7. Java 互操作默认参数不生效:Java 调用方看不到默认值,必须加 @JvmOverloads 或写显式重载。
  8. data class 当作层级继承用:data class 是 final 不能被继承,且自动生成的 equals 在子类中行为不正确。需要继承层级时改用普通 class + 手写 equals。
  9. 协变逆变不分:List<out T> 是只读协变,MutableList<T> 是不变。一旦把 MutableList<Cat> 当作 MutableList<Animal> 就能塞进狗。
  10. 顶层函数过多:所有函数都堆文件顶层会让 IDE 跳转痛苦。建议按职责分文件,必要时用 @file:JvmName 收一个友好类名。

章节小结

  • val / var + 类型推断 + 字符串模板让 Kotlin 比 Java 更简洁;
  • 函数默认参数 + 命名参数 + vararg 替代 Java 重载链,单表达式函数让简单逻辑一行搞定;
  • 类用主构造 + init + data class / sealed class / object 三件套,几乎不需要手写 equals / toString / 单例样板;
  • 空安全是 Kotlin 杀手级特性,?. ?: let 是日常 80% 的可空处理,!! 只在确信非空时偶尔使用;
  • 扩展函数让你在不继承的前提下给已有类追加 API,是 Compose 与 Jetpack 大量使用的模式;
  • 与 Java 互操作只需关注几个注解:@JvmStatic / @JvmField / @JvmOverloads / @file:JvmName,以及把平台类型显式声明可空。

下一章预告

下一章我们正式跨入 Kotlin 协程与 Flow 的世界:suspend 函数、CoroutineScope、Dispatchers.Main / IO / Default、结构化并发、Job 取消、Flow 与 StateFlow / SharedFlow,以及如何与 ViewModel + LiveData 整合。掌握协程后,你就能在 Compose 中优雅地处理异步与状态。