Kotlin基础语法
# 第 1 章 Kotlin 基础语法
# 目录介绍
- 1.1 Kotlin语言定位
- 1.2 变量声明与类型
- 1.3 空安全类型系统
- 1.4 函数与Lambda
- 1.5 流程控制表达式
- 1.6 集合与序列
- 1.7 字符串与IO
- 1.8 综合案例练习
- 1.9 训练题三道
- 1.10 新手陷阱Top5
# 1.1 Kotlin语言定位
# 1.1.1 Kotlin是什么
Kotlin 由 JetBrains 公司在 2011 年立项,2016 年 1.0 发布,2017 年 Google I/O 宣布为 Android 一等公民,2019 年成为 Android 官方首选语言。它是一门面向 JVM 与多平台的静态强类型语言,主打两句 slogan:
- 与 Java 100% 互操作——Kotlin 类可以调用任何 Java 库,Java 类也可以(几乎无痛地)调用 Kotlin 类
- 语法现代化——空安全、扩展函数、协程、data class、DSL 支持一应俱全
与另外三门「主流现代语言」的差别对照:
| 语言 | 立场 | 与 Java 互操作 | 主要战场 | 与 Kotlin 差异点 |
|---|---|---|---|---|
| Scala | 学术派、函数式 | 良好但复杂 | 大数据(Spark) | 语言过于庞大,类型系统难 |
| Groovy | 动态类型胶水 | 优秀 | Gradle 脚本 | 无静态类型保障 |
| Swift | 苹果生态原生 | 无 | iOS / macOS | 完全独立生态 |
| Kotlin | 务实工业级 | 优秀 | Android / Server / KMP | 类型安全 + 语法糖平衡 |
一句话总结:Kotlin 是「给 Java 程序员一次不用改工具链就能升级到现代语言的机会」。
# 1.1.2 三大编译目标
Kotlin 编译器可以把同一份源码编译到三种目标:
┌────────────────────────────┐
│ Kotlin 源码 (.kt) │
└──────────────┬─────────────┘
│
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Kotlin/JVM │ │Kotlin/Native │ │ Kotlin/JS │
│ → .class │ │ → 原生二进制 │ │ → .js │
│ │ │ │ │ │
│ Android │ │ iOS / macOS │ │ Web 前端 │
│ 服务端 │ │ Linux/Win │ │ Node.js │
└──────────────┘ └──────────────┘ └──────────────┘
三种目标共享 Kotlin 标准库的公共 API(kotlin.* 包),但底层实现完全不同:
- Kotlin/JVM 的
String就是java.lang.String - Kotlin/Native 的
String是一个原生 UTF-16 缓冲区 - Kotlin/JS 的
String就是 JavaScript 原生字符串
这为 **KMP(Kotlin Multi Platform)**跨端复用打下基础——上层 UI 各写各的,底层业务逻辑一份 Kotlin 三处编译。
# 1.1.3 与Java对比表
真正让 Java 程序员「一见倾心」的十项差别:
| 特性 | Java | Kotlin |
|---|---|---|
| 空安全 | 运行期 NPE | 编译期禁止(T vs T?) |
| 数据类 | 手写 boilerplate | data class 一行 |
| Lambda | Java 8+ | 一等公民 + 尾随 Lambda |
| 扩展方法 | 无 | fun T.xxx() 到处扩 |
| 协程 | 无(要用 Loom) | 语言级 suspend |
| 类默认可继承 | 是(除非 final) | 否(默认 final) |
| 分号 | 强制 | 可选 |
switch | 语句 | when 是表达式 |
| 属性 | getter/setter 手写 | 自动生成 |
| 主构造器 | 单独写 | 写在类头里 |
思维模型:Kotlin 不发明新概念,而是把 Java 里"程序员每天都在做的样板代码"直接下沉到语言层。
# 1.1.4 综合案例与思考
用一个「Hello, World」感受下:
// hello.kt
fun main() {
val name = "Kotlin"
println("Hello, $name!")
}
对应 Java:
public class Hello {
public static void main(String[] args) {
String name = "Java";
System.out.println("Hello, " + name + "!");
}
}
几个可视化差别:
- 没有类包裹——顶层函数直接就是入口
main参数可省——不需要总是接收String[]- 字符串模板
$name——不再拼加号 - 类型可推断——
val name = "Kotlin"就够,编译器知道它是String - 分号可省——一行一句
思考:为什么 Kotlin 把类默认设成 final?——因为「开放继承」是一份公开契约,你必须主动承诺要维护它(这与 Java 15 后引入 sealed 是同一哲学:让继承关系显式可控)。
# 1.2 变量声明与类型
# 1.2.1 val与var区别
Kotlin 用两个关键字代替 Java 的「default vs final」双规则:
val pi = 3.14159 // 只读,一旦赋值不能重新绑定
var count = 0 // 可变,可以重新赋值
count = 1 // ✅
pi = 3.14 // ❌ Val cannot be reassigned
易错点:val 是「引用不可变」,不是「对象不可变」:
val list = mutableListOf(1, 2, 3)
list.add(4) // ✅ 可以,因为改的是 list 内部
list = mutableListOf() // ❌ 不行,重新绑定引用
想要真正的「对象不可变」,需要用只读接口:
val list: List<Int> = listOf(1, 2, 3) // List 接口没有 add()
风格建议:默认写 val,除非确实需要重新赋值——这是 Kotlin 官方 Style Guide 的第一条铁律。
# 1.2.2 类型推断规则
Kotlin 编译器根据右侧表达式推断左侧类型:
val n = 42 // Int
val f = 3.14 // Double
val s = "hello" // String
val b = true // Boolean
val list = listOf(1, 2) // List<Int>
三种必须显式声明类型的场合:
- 无初始化时:
val n: Int; n = 42 - 推断结果与预期不同:
val n: Long = 42(否则默认 Int) - 公共 API:
fun foo(): List<String>——防止未来实现变化悄悄改类型
Lambda 类型推断:
val add = { a: Int, b: Int -> a + b } // (Int, Int) -> Int
val sq: (Int) -> Int = { it * it } // 有左侧类型时 Lambda 参数可省
# 1.2.3 基础类型家族
| 分类 | 类型 | 位数 | 备注 |
|---|---|---|---|
| 整数 | Byte / Short / Int / Long | 8/16/32/64 | 无隐式扩宽 |
| 浮点 | Float / Double | 32/64 | |
| 字符 | Char | 16 位 UTF-16 | 不是数字 |
| 布尔 | Boolean | JVM 实际用 1 字节 | 只有 true / false |
| 字符串 | String | 引用类型 | 不可变 |
关键差异 1:无自动整型扩宽
val i: Int = 1
val l: Long = i // ❌ Type mismatch
val l2: Long = i.toLong() // ✅ 必须显式转换
这个「不便」是刻意设计——Java 里 int → long → float → double 的隐式扩宽是一大类 bug 的温床。
关键差异 2:Char 不是数字
val c: Char = 'A'
val n: Int = c // ❌ Type mismatch
val n2: Int = c.code // ✅ 65
Java 里 char + 1 是数字运算,Kotlin 里则要显式 .code 转换。
字面量后缀:
| 后缀 | 类型 | 例子 |
|---|---|---|
L | Long | 100L |
F / f | Float | 3.14f |
0x | 十六进制 | 0xFF |
0b | 二进制 | 0b1010 |
_ | 分隔符 | 1_000_000 |
# 1.2.4 Any与Unit与Nothing
Kotlin 类型系统的三个"特殊类型":
| 类型 | 语义 | 对应 Java |
|---|---|---|
Any | 所有非空类型的根 | Object(近似) |
Unit | "没有有用返回值" | void(但 Unit 是真实对象) |
Nothing | 永不返回 | 无对应物 |
Unit 与 Java void 的差别:
fun printHi(): Unit { println("hi") }
// 等价:
fun printHi() { println("hi") } // 省略 : Unit
val u: Unit = printHi() // ✅ Unit 是真实值
Java void 不能作为泛型参数:Callable<void> 报错;Kotlin Callable<Unit> 完全合法——这让泛型函数可以统一处理「有返回值」与「无返回值」两种情况。
Nothing 是所有类型的子类型:
fun fail(msg: String): Nothing = throw IllegalStateException(msg)
val name: String = user.name ?: fail("no name") // fail 返回 Nothing <: String
这就是为什么 throw 表达式可以出现在 ?: 右侧、if 分支里——Nothing 与任何类型都兼容。
在 when 里的妙用(穷尽性):
val result: String = when (x) {
1 -> "one"
2 -> "two"
else -> fail("unknown $x") // 返回 Nothing,编译器认为 when 是穷尽的
}
# 1.2.5 综合案例与思考
温度换算器——展示 val/var + 类型显式转换:
fun main() {
val celsius: Double = 25.0
val fahrenheit = celsius * 9 / 5 + 32 // 推断为 Double
val fahrenheitInt: Int = fahrenheit.toInt() // 显式转换
println("$celsius°C = $fahrenheit°F ≈ $fahrenheitInt°F")
var temperature = celsius
temperature += 10 // var 才能这么写
println("加热 10 度后: $temperature°C")
}
输出:
25.0°C = 77.0°F ≈ 77°F
加热 10 度后: 35.0°C
思考:Any 与 Any? 究竟差在哪?——Any? 是 Any 的父类(可空类型是非空类型的父类),也是整个 Kotlin 类型系统的顶部。Any 只是所有非空引用类型的公共父。
# 1.3 空安全类型系统
# 1.3.1 T与T可空对比
String 与 String? 是两个不同类型。前者的变量永远不为 null,后者可为 null。这是 Kotlin 最招牌的能力。
val a: String = "hello" // 永远非空
val b: String? = null // 可以为空
val c: String = b // ❌ Type mismatch: inferred type is String? but String was expected
编译器帮你做的事:
fun greet(name: String) = "Hello, $name" // 参数非空
fun greetSafe(name: String?) = "Hello, ${name ?: "guest"}" // 参数可空必须处理
greet(null) // ❌ 编译期就拒绝
greetSafe(null) // ✅
代价:一旦某个变量声明为 String?,之后每次访问其属性/方法都要处理空情况。这个「代价」正是空安全的价值——把 NPE 从"运行期黑天鹅"变成"编译期强制流程"。
# 1.3.2 安全调用三兄弟
三个专门为空安全设计的运算符:
| 运算符 | 含义 | 例子 | 结果 |
|---|---|---|---|
?. | 空安全调用 | s?.length | s 为 null 时结果为 null |
?: | Elvis 兜底 | s?.length ?: 0 | s 为 null 时取 0 |
!! | 非空断言 | s!!.length | s 为 null 时抛 NPE |
链式安全调用:
// 类似 Optional chaining
val city: String? = user?.address?.city?.uppercase()
// 任意一环为 null,整条链结果为 null
Elvis 常见用法:
// 1. 兜底默认值
val name = user?.name ?: "guest"
// 2. 提前返回
fun load(id: String?) {
val realId = id ?: return
// 之后 realId 是 String(非空)
}
// 3. 抛异常(借助 Nothing)
val name = user?.name ?: throw IllegalStateException("no name")
!! 的正确使用场景——只在**"逻辑上不可能为 null"的地方作为断言**:
fun processView(v: View?) {
v ?: return // 已判空过滤
// ...一堆复杂逻辑...
val parent = v.parent as? ViewGroup // 尽量用 as?
}
// 而不是这样:
fun bad(v: View?) {
v!!.width // 图省事,一崩就 NPE,Root Cause 难找
}
滥用 !! 是空安全体系的头号 code smell——Lint 工具通常会直接标出来。
# 1.3.3 智能类型转换
编译器做流敏感分析,在检查过后的分支里自动收窄类型:
fun printLength(s: String?) {
if (s != null) {
println(s.length) // 这里 s 已被智能转换为 String
}
}
fun handle(obj: Any) {
if (obj is String) {
println(obj.length) // 智能转换为 String
} else if (obj is List<*>) {
println(obj.size) // 智能转换为 List<*>
}
}
三个失效场景:
成员
var(可能被其他线程改)class C { var name: String? = null fun show() { if (name != null) println(name.length) // ❌ } }open val(子类可能重写为自定义 getter)open class Base { open val name: String? = "x" }跨模块的
val(另一模块的自定义 getter 编译器无法确认)
统一解法——局部拷贝或 ?.let:
class C {
var name: String? = null
fun show() {
val local = name
if (local != null) println(local.length) // ✅
name?.let { println(it.length) } // 或用 let
}
}
# 1.3.4 综合案例与思考
解析一份可能字段缺失的用户配置,全程零 !!:
data class UserConfig(
val nickname: String?,
val age: Int?,
val theme: String?
)
fun describe(config: UserConfig?): String {
if (config == null) return "配置为空"
val nick = config.nickname ?: "匿名用户"
val age = config.age?.let { "$it 岁" } ?: "年龄未填"
val theme = when (config.theme) {
"dark" -> "深色主题"
"light" -> "浅色主题"
null -> "使用系统主题"
else -> "自定义主题:${config.theme}"
}
return "$nick,$age,$theme"
}
fun main() {
val c1 = UserConfig("Alice", 25, "dark")
val c2 = UserConfig(null, null, null)
println(describe(c1))
println(describe(c2))
println(describe(null))
}
输出:
Alice,25 岁,深色主题
匿名用户,年龄未填,使用系统主题
配置为空
思考:为什么 let 常与 ?. 组合?——?.let 相当于「非空时执行块」,读起来比 if (x != null) { ... } 更连贯,还能在 it 里得到智能转换后的非空引用(连成员 var 都能用)。
# 1.4 函数与Lambda
# 1.4.1 函数声明形式
顶层函数(不属于任何类):
// utils.kt
fun add(a: Int, b: Int): Int {
return a + b
}
// 调用
val s = add(1, 2) // 直接调用
成员函数:
class Calculator {
fun add(a: Int, b: Int) = a + b
}
局部函数(函数内定义函数):
fun validate(user: User) {
fun check(field: String, value: String) {
if (value.isEmpty()) throw IllegalArgumentException("$field is empty")
}
check("name", user.name)
check("email", user.email)
}
三者的差异:
| 类型 | 定义位置 | 可访问外部 | JVM 字节码 |
|---|---|---|---|
| 顶层函数 | 文件级 | 无 | 生成 FileNameKt.foo(...) 静态方法 |
| 成员函数 | 类内 | this、类成员 | 实例方法 |
| 局部函数 | 函数内 | 闭包变量 | 生成匿名内部类 |
# 1.4.2 默认与命名参数
默认参数——Kotlin 消灭了 Java 的重载地狱:
fun greet(name: String, greeting: String = "Hello", suffix: String = "!") =
"$greeting, $name$suffix"
greet("Alice") // Hello, Alice!
greet("Bob", "Hi") // Hi, Bob!
greet("Carol", "Hi", "?") // Hi, Carol?
对比 Java 的写法:
String greet(String name) { return greet(name, "Hello", "!"); }
String greet(String name, String greeting) { return greet(name, greeting, "!"); }
String greet(String name, String greeting, String suf) { return greeting + ", " + name + suf; }
命名参数——用参数名调用,可任意顺序:
greet(name = "Alice", suffix = "?", greeting = "Hi")
惯用组合:跳过中间的默认参数:
greet("Alice", suffix = "?") // greeting 用默认,suffix 命名指定
# 1.4.3 单表达式函数
函数体只有一个表达式时,可以用 = 代替 { return ... }:
fun square(x: Int) = x * x // 返回类型推断为 Int
fun max(a: Int, b: Int) = if (a > b) a else b // 返回类型推断为 Int
fun isEven(x: Int): Boolean = x % 2 == 0 // 显式声明返回类型
风格建议:公共 API 的单表达式函数依然显式声明返回类型,避免实现改动悄悄影响接口。
# 1.4.4 Lambda基础用法
Lambda 表达式的完整语法:
val sum: (Int, Int) -> Int = { a, b -> a + b }
val square: (Int) -> Int = { it * it } // 单参数时 it 是默认名
val hi: () -> Unit = { println("hi") }
函数类型 (A, B) -> C 本质上是接口:
// 编译器生成的等价 Java:
Function2<Integer, Integer, Integer> sum = new Function2<>() {
@Override
public Integer invoke(Integer a, Integer b) { return a + b; }
};
Kotlin 标准库定义了 Function0 ~ Function22(0~22 个参数),覆盖绝大多数场景。
Lambda 调用:
sum(1, 2) // 3
sum.invoke(1, 2) // 3,等价写法
# 1.4.5 尾随Lambda惯用
规则:Lambda 是最后一个参数时,可以移出括号写在花括号里;如果是唯一参数,圆括号可以省略。
// 完整写法
list.filter({ it > 0 })
// 尾随 Lambda(移出括号)
list.filter() { it > 0 }
// 唯一参数省括号(惯用)
list.filter { it > 0 }
多参数场景:
// fold 有两个参数:初始值 + Lambda
list.fold(0) { acc, x -> acc + x } // 尾随 Lambda 移出括号
这条规则是 Kotlin DSL 的语法基础——build、launch { }、view { } 全靠它:
// Compose 风格
Column {
Text("Hello")
Button(onClick = { }) {
Text("Click")
}
}
# 1.4.6 综合案例与思考
写一个通用的重试辅助函数:
fun <T> retry(times: Int = 3, delayMs: Long = 100, block: () -> T): T {
var lastException: Exception? = null
repeat(times) { attempt ->
try {
return block()
} catch (e: Exception) {
lastException = e
println("第 ${attempt + 1} 次失败: ${e.message}")
Thread.sleep(delayMs)
}
}
throw lastException ?: IllegalStateException("retry failed")
}
fun main() {
var counter = 0
val result = retry(times = 3) {
counter++
if (counter < 3) throw RuntimeException("attempt $counter failed")
"success on attempt $counter"
}
println(result)
}
输出:
第 1 次失败: attempt 1 failed
第 2 次失败: attempt 2 failed
success on attempt 3
思考:为什么 Kotlin 支持"尾随 Lambda"?——为了让函数调用长得像语言内置结构。repeat(3) { ... } 读起来像 for,launch { ... } 读起来像块语句。这就是 DSL 的表达力来源。
# 1.5 流程控制表达式
# 1.5.1 if是表达式
Kotlin 里 if / when / try 都是表达式(能返回值):
val max = if (a > b) a else b // 类似三元
val n = try { s.toInt() } catch (e: Exception) { 0 }
因此 Kotlin 没有三元运算符 ? :——用 if 就够,还更清晰。
注意:作为表达式使用时,if 必须有 else 分支:
val x = if (a > 0) 1 // ❌ 'if' must have both main and 'else' branches
val x = if (a > 0) 1 else 0 // ✅
# 1.5.2 when模式匹配
when 是 Kotlin 的 switch——但功能强大得多:
val text = when (x) {
0 -> "zero"
1, 2 -> "one or two" // 多值分支
in 3..10 -> "small" // 区间
!in -100..0 -> "positive big" // 范围外
is String -> "a string" // 类型匹配(此时 x 是 Any)
else -> "other"
}
无参数 when(if-else 链的替代):
val level = when {
score >= 90 -> "A"
score >= 80 -> "B"
score >= 60 -> "C"
else -> "F"
}
穷尽性检查——配合 sealed class / enum:
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val msg: String) : Result()
object Loading : Result()
}
fun show(r: Result) = when (r) { // 编译器检查穷尽
is Result.Success -> println(r.data)
is Result.Error -> println(r.msg)
Result.Loading -> println("...")
// 不写 else 也不报错——编译器已确认三个分支覆盖所有子类
}
Kotlin when 比 Java switch 强在:
| 特性 | Java switch | Kotlin when |
|---|---|---|
| 表达式 | Java 14+ | 一开始就是 |
| 区间匹配 | 无 | in 1..10 |
| 类型匹配 | Java 21+ | 一开始就有 |
| 无参形式 | 无 | ✅ |
| 穷尽检查 | Java 21+ | ✅ |
# 1.5.3 区间与for循环
区间(Range)是 Kotlin 的一等公民:
for (i in 1..10) { } // 闭区间 [1, 10]
for (i in 1 until 10) { } // 半开区间 [1, 10)
for (i in 10 downTo 1) { } // 逆序 10, 9, ..., 1
for (i in 10 downTo 1 step 2) { } // 逆序步长 10, 8, ..., 2
for (i in 1..100 step 5) { } // 正序步长
遍历集合:
val list = listOf("a", "b", "c")
for (s in list) println(s) // 元素
for ((i, s) in list.withIndex()) println("$i: $s") // 带索引
for (i in list.indices) println(list[i]) // 索引区间
遍历 Map:
val map = mapOf("a" to 1, "b" to 2)
for ((k, v) in map) println("$k = $v") // 解构
# 1.5.4 标签与非局部返回
Lambda 里 return 的语义有个坑:默认返回外层函数!
fun demo() {
listOf(1, 2, 3).forEach {
if (it == 2) return // ⚠ 从 demo() 返回,不是从 forEach 返回
println(it)
}
println("never reach") // 不会打印
}
标签解决——return@forEach 只返回当前 Lambda:
fun demo() {
listOf(1, 2, 3).forEach {
if (it == 2) return@forEach // 相当于 continue
println(it)
}
println("finished") // ✅ 会打印
}
输出:
1
3
finished
规则:return@LABEL 中的 LABEL 默认是调用点的函数名(forEach、map、filter 等);你也可以自定义标签:
outer@ for (i in 1..5) {
for (j in 1..5) {
if (i * j > 6) break@outer // 跳出外层
}
}
# 1.5.5 综合案例与思考
小型状态机——用户订单状态转换:
enum class OrderStatus { CREATED, PAID, SHIPPED, DELIVERED, CANCELLED }
fun nextStatus(current: OrderStatus, action: String): OrderStatus? = when (current) {
OrderStatus.CREATED -> when (action) {
"pay" -> OrderStatus.PAID
"cancel" -> OrderStatus.CANCELLED
else -> null
}
OrderStatus.PAID -> when (action) {
"ship" -> OrderStatus.SHIPPED
"refund" -> OrderStatus.CANCELLED
else -> null
}
OrderStatus.SHIPPED -> if (action == "confirm") OrderStatus.DELIVERED else null
OrderStatus.DELIVERED, OrderStatus.CANCELLED -> null // 终态
}
fun main() {
var status = OrderStatus.CREATED
val actions = listOf("pay", "ship", "confirm")
for (a in actions) {
val next = nextStatus(status, a) ?: run {
println("$status 不接受动作 $a")
return@main
}
println("$status --[$a]--> $next")
status = next
}
}
输出:
CREATED --[pay]--> PAID
PAID --[ship]--> SHIPPED
SHIPPED --[confirm]--> DELIVERED
思考:Kotlin 有 switch 关键字吗?——没有。when 比 switch 强在:表达式返回值、区间匹配、类型匹配、无参多分支、穷尽性检查——五项 Java 花了 10+ 年才陆续追齐。
# 1.6 集合与序列
# 1.6.1 只读与可变分家
Kotlin 集合的核心设计——只读接口与可变接口分家:
Iterable
├─ Collection (size, contains, iterator)
│ ├─ List (只读)
│ │ └─ MutableList (add, remove, set)
│ └─ Set (只读)
│ └─ MutableSet
└─ Map (只读)
└─ MutableMap
只读只是"接口只读",不代表"数据不可变":
val mutable = mutableListOf(1, 2, 3)
val readonly: List<Int> = mutable // 拿到只读视图
mutable.add(4) // 底层可变
println(readonly) // [1, 2, 3, 4] ← 数据仍变了!
真正的不可变集合:
val truly = listOf(1, 2, 3) // Kotlin:底层 ArrayList,但只读
val trulyImmutable = java.util.Collections.unmodifiableList(mutable)
// 或用 Guava/Immutables 库
Kotlin 1.7+ 引入了 PersistentList(kotlinx.collections.immutable)作为真正不可变集合。
# 1.6.2 集合创建函数
标准库提供一套统一的工厂函数:
| 函数 | 返回类型 | 备注 |
|---|---|---|
listOf(1, 2) | 只读 List | 底层不可变数组 |
mutableListOf(1, 2) | MutableList | 底层 ArrayList |
arrayListOf(1, 2) | ArrayList | 直接暴露 ArrayList |
setOf(1, 2) | 只读 Set | 底层 LinkedHashSet |
mutableSetOf | MutableSet | |
hashSetOf | HashSet | |
mapOf("a" to 1) | 只读 Map | 用 to 创建 Pair |
mutableMapOf | MutableMap | |
hashMapOf | HashMap | |
emptyList() | 空只读 List | 单例,零分配 |
listOfNotNull(a, b, c) | 过滤 null |
惯用组合:
val users = listOf("Alice", "Bob").associateWith { it.length }
// {Alice=5, Bob=3}
val ages = mutableMapOf<String, Int>().apply {
put("Alice", 25)
put("Bob", 30)
}
# 1.6.3 map与filter链
Kotlin 集合的链式操作是它函数式风格的核心:
val evenSquares = (1..10)
.filter { it % 2 == 0 }
.map { it * it }
// [4, 16, 36, 64, 100]
常用操作符:
| 操作符 | 语义 | 例子 |
|---|---|---|
filter | 保留满足条件的元素 | list.filter { it > 0 } |
filterNot | 反向过滤 | list.filterNot { it.isEmpty() } |
map | 一对一映射 | list.map { it.toString() } |
flatMap | 一对多再展平 | list.flatMap { it.words } |
mapNotNull | map + 过滤 null | list.mapNotNull { it.toIntOrNull() } |
reduce | 累积(无初值) | list.reduce { a, b -> a + b } |
fold | 累积(有初值) | list.fold(0) { a, b -> a + b } |
groupBy | 分组 | list.groupBy { it.length } |
associate | 生成 Map | list.associate { it to it.length } |
partition | 二分 | list.partition { it > 0 } |
distinct | 去重 | list.distinct() |
sorted | 自然排序 | list.sorted() |
sortedBy | 按 key 排序 | list.sortedBy { it.length } |
take/drop | 取前 N/去前 N | list.take(3) |
sum/count/max | 归约 | list.sumOf { it.length } |
# 1.6.4 序列惰性求值
问题:(1..1_000_000).filter { it > 500_000 }.map { it * 2 }.first() 会怎么执行?
回答(List 链):先分配一个 50 万元素的 List(filter 结果),再分配一个 50 万元素的 List(map 结果),最后取第一个——两次全量遍历 + 两次大数组分配。
改用 Sequence:
val result = (1..1_000_000).asSequence()
.filter { it > 500_000 }
.map { it * 2 }
.first()
执行方式:从 500_001 开始,filter → map → first 一次拿到 500_001 * 2 = 1_000_002,立即结束。
Sequence 的核心是惰性求值 + 逐元素流水:
List 链: [1..1M] → filter → [500K个] → map → [500K个] → first
全部计算 全部计算
Sequence 链: 1 → filter (跳过)
...
500001 → filter (通过) → map → *2 → first → 返回
选择原则:
| 场景 | 用 List | 用 Sequence |
|---|---|---|
| 小集合(<100) | ✅ | 反而慢 |
| 中集合 + 全量处理 | ✅ | 差不多 |
大集合 + take/first 早退 | ❌ | ✅ 大幅提速 |
| 大集合 + 多步链 | ❌ | ✅ 省中间对象 |
| 无限流 | ❌ | ✅(generateSequence) |
# 1.6.5 综合案例与思考
统计每小时错误数——对比 List 链与 Sequence 链:
data class LogEntry(val hour: Int, val level: String, val msg: String)
fun main() {
// 模拟 100 万条日志
val logs = List(1_000_000) {
LogEntry(hour = it % 24, level = if (it % 100 == 0) "ERROR" else "INFO", msg = "...")
}
// 方式 1:List 链
val t1 = System.currentTimeMillis()
val r1 = logs.filter { it.level == "ERROR" }
.groupBy { it.hour }
.mapValues { it.value.size }
println("List 链: ${System.currentTimeMillis() - t1} ms")
// 方式 2:Sequence 链
val t2 = System.currentTimeMillis()
val r2 = logs.asSequence()
.filter { it.level == "ERROR" }
.groupingBy { it.hour }.eachCount()
println("Sequence 链: ${System.currentTimeMillis() - t2} ms")
println("Errors by hour: ${r1.toSortedMap()}")
check(r1 == r2)
}
典型输出(i7-12700K):
List 链: 128 ms
Sequence 链: 34 ms
Errors by hour: {0=417, 1=417, ...}
思考:Sequence 与 Java Stream 的差别?——语法相似,但 Kotlin Sequence 不支持并行(想并行要用协程或 Java Stream);Sequence 在中间操作上是惰性的、逐元素流水,而 Java Stream 也是同样的模型——两者差别主要在 API 覆盖度与语法糖,性能特征几乎一致。
# 1.7 字符串与IO
# 1.7.1 字符串模板语法
字符串模板是 Kotlin 最常用的语法糖:
val name = "Kotlin"
println("Hello, $name") // 变量插值
println("Length is ${name.length}") // 表达式插值
println("Upper: ${name.uppercase()}") // 复杂表达式必须 ${ }
转义 $:
println("\$name is a variable") // 输出:$name is a variable
字节码层面——Kotlin 编译器把模板展开为 StringBuilder:
new StringBuilder
.append("Hello, ")
.append(name)
.toString()
因此性能与手写 StringBuilder 相同——放心用。
# 1.7.2 三引号原始串
三引号 """ 里的字符串:
- 不需要转义
\ - 保留原始换行
val json = """
{
"name": "Kotlin",
"path": "C:\Users\yc"
}
""".trimIndent()
trimIndent() 会去掉每行共同的开头空格:
val s = """
line 1
line 2
line 3
""".trimIndent()
// 结果:
// "line 1\n line 2\nline 3"
// 注意 "line 2" 前面的两个空格被保留了
trimMargin("|") 用竖线作为对齐标记:
val poem = """
|Roses are red
|Violets are blue
""".trimMargin()
# 1.7.3 常用字符串API
Kotlin 在 String 上加了大量扩展函数,让字符串操作更 Kotlin 化:
| API | 语义 | 例子 |
|---|---|---|
s.isEmpty() / isBlank() | 空 / 空白 | " ".isBlank() → true |
s.isNullOrEmpty() / isNullOrBlank() | 兼容 null | 常用于校验 |
s.split(",") | 拆分 | 返回 List<String> |
s.substring(1..3) | 用 IntRange | 更符合 Kotlin 惯用 |
s.startsWith("http") | 前缀 | |
s.endsWith(".png") | 后缀 | |
s.contains("kotlin", ignoreCase = true) | 忽略大小写 | |
s.replace(old, new) | 替换 | |
s.toIntOrNull() | 解析失败返回 null | 比 toInt() 安全 |
s.padStart(5, '0') | 前补零 | "7".padStart(3, '0') → "007" |
s.repeat(3) | 重复 | "ab".repeat(3) → "ababab" |
s.reversed() | 反转 | |
s.lines() | 按换行拆分 | |
s.uppercase() | 大写(1.5+ 后名) | 老写法 toUpperCase() |
s.trim() | 去首尾空白 |
特别推荐 toIntOrNull 家族:
val n1 = "42".toInt() // 42
val n2 = "abc".toInt() // ❌ NumberFormatException
val n3 = "abc".toIntOrNull() // null,无异常
# 1.7.4 综合案例与思考
CSV 简易解析器:
fun parseCsv(text: String): List<Map<String, String>> {
val lines = text.lines().filter { it.isNotBlank() }
if (lines.size < 2) return emptyList()
val headers = lines.first().split(",").map { it.trim() }
return lines.drop(1).map { line ->
val values = line.split(",").map { it.trim() }
headers.zip(values).toMap()
}
}
fun main() {
val csv = """
name, age, city
Alice, 25, Beijing
Bob, 30, Shanghai
Carol, 28, Guangzhou
""".trimIndent()
val records = parseCsv(csv)
records.forEach { println(it) }
}
输出:
{name=Alice, age=25, city=Beijing}
{name=Bob, age=30, city=Shanghai}
{name=Carol, age=28, city=Guangzhou}
思考:Kotlin 字符串是可变的吗?——不是,String 与 Java 一样是不可变对象。频繁拼接用 StringBuilder(或 buildString { ... } DSL):
val s = buildString {
append("Hello, ")
append("Kotlin")
appendLine("!")
}
# 1.8 综合案例练习
# 1.8.1 学生成绩管理
需求:读取一批学生成绩,输出:平均分、最高分、及格率、按分数段统计人数。
data class Student(val name: String, val score: Int)
fun main() {
val students = listOf(
Student("Alice", 92),
Student("Bob", 78),
Student("Carol", 55),
Student("Dave", 88),
Student("Eva", 67),
Student("Frank", 43),
Student("Grace", 95),
Student("Henry", 71)
)
// 1. 平均分
val avg = students.map { it.score }.average()
// 2. 最高分
val top = students.maxByOrNull { it.score }
// 3. 及格率
val passed = students.count { it.score >= 60 }
val passRate = passed * 100.0 / students.size
// 4. 分数段
val grade = students.groupBy {
when (it.score) {
in 90..100 -> "A"
in 80..89 -> "B"
in 70..79 -> "C"
in 60..69 -> "D"
else -> "F"
}
}.mapValues { it.value.size }
println("平均分: %.1f".format(avg))
println("最高分: ${top?.name} (${top?.score})")
println("及格率: %.1f%%".format(passRate))
println("分数段: ${grade.toSortedMap()}")
}
输出:
平均分: 73.6
最高分: Grace (95)
及格率: 75.0%
分数段: {A=2, B=1, C=2, D=1, F=2}
# 1.8.2 案例设计思路
按四步法思考:
- 建模——先定义
data class Student,把「实体 + 数据」固化到类型系统里 - 准备数据——用
listOf或从文件 / 网络读入,得到List<Student> - 单次遍历取多个统计量——用
map/count/groupBy组合 - 格式化输出——用字符串模板 +
format控制精度
关键思维:不要陷入「一个 for 循环把所有事都做」的过程式陷阱——每一个统计量都是一条独立的链,读起来像声明,改起来只改那一条链。
# 1.9 训练题三道
训练题 1:设计一个函数 safeParse(s: String?): Int?,输入可能为 null 或非数字,全程不允许出现 !!。
参考答案
fun safeParse(s: String?): Int? = s?.trim()?.toIntOrNull()
// 测试
println(safeParse(null)) // null
println(safeParse(" ")) // null
println(safeParse("42")) // 42
println(safeParse("abc")) // null
println(safeParse(" 99 ")) // 99
要点:?. 空传播 + toIntOrNull 解析失败返回 null,全程无异常无 !!。
训练题 2:用 when 表达式实现一个「英文月份缩写转数字」函数,Jan → 1, Feb → 2, ...。
参考答案
fun monthToNumber(m: String): Int? = when (m.trim().lowercase()) {
"jan" -> 1; "feb" -> 2; "mar" -> 3; "apr" -> 4
"may" -> 5; "jun" -> 6; "jul" -> 7; "aug" -> 8
"sep" -> 9; "oct" -> 10; "nov" -> 11; "dec" -> 12
else -> null
}
要点:输入归一化(trim + lowercase)+ 单值分支 + else -> null。想更「Kotlin 化」还可以用 Month.valueOf 枚举做。
训练题 3:给定 List<Pair<String, Int>> 表示"部门-薪资",用集合链算出每个部门的平均薪资,结果按平均薪资降序输出。
参考答案
fun main() {
val data = listOf(
"IT" to 30_000, "IT" to 25_000, "IT" to 35_000,
"HR" to 15_000, "HR" to 18_000,
"Sales" to 22_000, "Sales" to 28_000, "Sales" to 24_000
)
val result = data.groupBy({ it.first }) { it.second }
.mapValues { (_, v) -> v.average() }
.toList()
.sortedByDescending { it.second }
result.forEach { (dept, avg) -> println("$dept: %.1f".format(avg)) }
}
输出:
IT: 30000.0
Sales: 24666.7
HR: 16500.0
要点:groupBy 的双 Lambda 重载(key 提取 + value 变换)+ mapValues + sortedByDescending 的组合技。
# 1.10 新手陷阱Top5
| # | 陷阱 | 场景 | 正确姿势 |
|---|---|---|---|
| 1 | !! 滥用 | 图省事到处 !! | 用 ?. ?: 或 requireNotNull(x) { "msg" } |
| 2 | == 与 === 混淆 | 以为 == 是引用相等 | 记住"两个等号看内容(equals),三个等号看地址(===)" |
| 3 | Int? 装箱代价 | 循环里大量可空基本类型 | 尽量用 Int,边界层再转 Int?;实在需要用 value class |
| 4 | Lambda 捕获 this 泄漏 | Fragment/Activity 里 Lambda 引用外部 this | Compose 用 remember;View 用 viewLifecycleOwner;弱引用兜底 |
| 5 | lateinit 与 by lazy 混用 | 用错场景 | lateinit——var 非空引用,我来赋值;by lazy——val,第一次访问才算 |
扩展陷阱(Top 5 之外但常见):
- 成员
var无法智能转换——用局部拷贝 - 协变泛型误用——
List<Any>不能赋值给List<String>(反过来也不行,除非声明List<out Any>) with(x) { }里this与it混淆——记住with/run/apply是this,let/also是it- 默认参数在 Java 侧不可见——想让 Java 调用要加
@JvmOverloads
下一章预告:入门篇 2 02.类和对象.md 会带你走进 Kotlin 的类系统——从最简单的 class 到 sealed class / data class / object / by 委托,还会顺便「下沉一层」看看这些糖背后的字节码。