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