Kotlin 命令速查表 - Kotlin 常用语法与协程编程大全
这套速查给写 Kotlin 的开发者——无论是 Android 应用、JVM 后端还是随手写脚本——聚焦每天都碰的几件事:用 kotlinc/Gradle 编译运行、空安全那套写法(安全调用、Elvis、非空断言)、filter/map/groupBy 等集合操作符,以及协程做结构化并发。与泛泛的语法罗列不同,条目按"这句代码解决什么问题"组织,从"避免空指针"到"把 I/O 挪出主线程"都有对应。读完应能写出没有散落 !! 的空安全链、流式处理集合,并让协程按作用域正确取消。
典型使用场景
面向用 Kotlin 写服务端、Android 或跨平台代码、被编译器空安全与集合 API 折磨过的开发者。表内把四块高频能力并排放:构建层用 ./gradlew build / test / dependencies 保证本地与 CI 结果一致,用 kotlinc -include-runtime -d 打出可直接 java -jar 运行的产物;语言层的空安全优先用 ?. 与 ?: 组合,!! 只在类型已保证非空时才用;数据处理层依赖 filter / map / flatMap / groupBy / partition 这套声明式集合链;并发层用 launch 与 async 的返回值差异区分「只要跑」和「要结果」,withContext(Dispatchers.IO) 把阻塞调用挪出主线程。对象建模则由 data class、sealed class、object、companion object 配合作用域函数(apply 初始化、let 转换、also 副作用)完成。
编译与运行 5
kotlinc main.kt -include-runtime -d main.jarjava -jar main.jarkotlinc -script script.ktskotlinc-jvmkotlin main.ktGradle 构建 5
./gradlew build./gradlew run./gradlew test./gradlew clean./gradlew dependencies空安全 5
var name: String? = nullname?.lengthname ?: "default"name!!.lengthval len = name?.length ?: 0集合操作 10
list.filter { it > 5 }list.map { it * 2 }list.flatMap { it.split(",") }list.groupBy { it.category }list.sortedBy { it.age }list.distinct()list.take(5)list.drop(3)list.firstOrNull { it.id == 1 }list.partition { it > 0 }协程 Coroutine 6
runBlocking { ... }launch { ... }async { ... }delay(1000)withContext(Dispatchers.IO) { ... }coroutineScope { ... }类与对象 8
data class User(val name: String, val age: Int)sealed class Resultobject Singleton { val x = 1 }companion object { val MAX = 100 }class User(val name: String)open class Animal; class Dog : Animal()interface Drawable { fun draw() }enum class Color { RED, GREEN, BLUE }作用域函数与委托 8
obj.let { it.foo() }obj.apply { this.x = 1 }obj.also { it.log() }obj.run { compute() }with(obj) { foo() }val lazyVal by lazy { init() }var x by Delegates.observable(0) { _, old, new -> }val v by map参数矩阵
| 参数 | 作用 | 示例 |
|---|---|---|
kotlinc main.kt -include-runtime -d main.jar | 编译为带运行时的可执行 jar | kotlinc main.kt -include-runtime -d main.jar |
java -jar main.jar | 运行编译好的 jar | java -jar main.jar |
kotlinc -script script.kts | 直接运行 Kotlin 脚本 | kotlinc -script demo.kts |
kotlin main.kt | 免编译直接运行源文件 | kotlin main.kt |
kotlinc-jvm | 进入 REPL 试代码 | kotlinc-jvm |
./gradlew build | 完整构建:编译 + 测试 + 打包 | ./gradlew build |
./gradlew run | 运行配置了 application 插件的主类 | ./gradlew run |
./gradlew test | 运行测试套件,失败即构建失败 | ./gradlew test |
./gradlew clean | 清空 build 目录产物 | ./gradlew clean |
./gradlew dependencies | 打印依赖树与解析版本 | ./gradlew dependencies |
String? | 声明可空类型,赋值可为 null | var name: String? = null |
name?.length | 安全调用,左侧为 null 时整体返回 null | val n = name?.length |
name ?: "default" | Elvis 兜底,左侧为 null 时取右侧 | val v = name ?: "default" |
name?.length ?: 0 | 安全调用 + 兜底的组合表达式 | val len = name?.length ?: 0 |
name!! | 非空断言,为 null 时抛 NPE,应尽量避免 | val len = name!!.length |
filter { } | 按条件筛选,返回新列表 | list.filter { it > 5 } |
map { } | 逐元素变换,常用于 DTO 转视图模型 | list.map { it * 2 } |
flatMap { } | 映射到集合后拍平,用于一对多展开 | list.flatMap { it.split(",") } |
groupBy { } | 按键分组为 Map | list.groupBy { it.category } |
sortedBy { } | 按字段升序排序 | list.sortedBy { it.age } |
distinct() / take(n) / drop(n) | 去重 / 取前 n 个 / 丢弃前 n 个 | list.take(5) |
firstOrNull { } | 取首个匹配项,无匹配返回 null | list.firstOrNull { it.id == 1 } |
partition { } | 按条件一次分成满足与不满足两组 | list.partition { it > 0 } |
launch { } | 启动不返回值的协程,返回 Job | launch { save() } |
async { } | 启动返回值的协程,返回 Deferred | val d = async { load() } |
runBlocking { } | 阻塞式运行协程,适合测试与脚本 | runBlocking { runApp() } |
withContext(Dispatchers.IO) { } | 切换到 IO 上下文执行阻塞调用 | withContext(Dispatchers.IO) { readFile() } |
delay(1000) | 非阻塞延迟 | delay(1000) |
coroutineScope { } | 创建结构化并发作用域 | coroutineScope { launch { } } |
data class | 数据类,自动生成 equals/hashCode/toString/copy | data class User(val name: String, val age: Int) |
sealed class | 密封类,配合 when 可穷举分支 | sealed class Result |
object / companion object | 单例对象声明 / 类级静态成员 | companion object { val MAX = 100 } |
apply { } | 接收者为 this,返回对象本身,适合初始化 | obj.apply { this.x = 1 } |
let { } | 接收者为 it,返回 lambda 结果,适合转换 | obj.let { it.foo() } |
also { } | 接收者为 it,返回对象本身,适合副作用 | obj.also { it.log() } |
run { } / with(obj) { } | 接收者为 this,返回 lambda 结果 | obj.run { compute() } |
by lazy { } | 延迟初始化委托,首次访问才计算 | val lazyVal by lazy { init() } |
Delegates.observable | 可观察属性委托,赋值触发回调 | var x by Delegates.observable(0) { _, old, n -> } |
map 委托 | 从 Map 按键取值,键不存在则抛异常 | val v by map |
易错点与避坑指南
现象运行到某处直接抛 NullPointerException,堆栈指向一行看着没问题的代码
原因该行用了 !! 非空断言,实际值在运行时为 null
处置把 !! 换成 ?. 配合 ?: 兜底,让 null 走明确的降级分支而不是崩溃
现象链式集合操作跑到中途,后面拿到的是 null 却没报错
原因中间某步用了 ?. 安全调用,null 被静默传递到下游
处置在链路末端用 ?: 补默认值,或改用 ?: early return 让空数据尽早短路
现象接口响应慢,界面像卡住了一样
原因在主线程上直接执行了阻塞调用,没有切到 IO 上下文
处置用 withContext(Dispatchers.IO) { ... } 包住阻塞的读写操作
现象启动协程后拿不到结果,await 位置恒为默认值
原因用了 launch 而不是 async,launch 返回 Job 没有结果
处置需要返回值时改用 async,并在协程作用域内 await 其 Deferred
现象本地能编译,CI 上却报依赖版本冲突
原因本地用了本机缓存与本仓库之外的依赖,版本解析结果不同
处置统一用 ./gradlew dependencies 打印解析结果核对,提交锁文件并只通过 ./gradlew build 构建
现象给 sealed class 写 when 时被要求补 else 分支
原因when 的分支没有覆盖全部子类,或判断的不是常量
处置检查是否漏掉某个子类声明,或把条件改成对常量值的比较让编译器能穷举
现象改成 flatMap 后内存占用突然升高
原因链式集合操作每一步都会产生中间集合,大数据量时层层累积
处置减少链长、对大集合改用 asSequence() 惰性处理,或在末端才做去重与排序
排障路径
1从零验证当前环境能否编译并运行
./gradlew clean build先清产物再完整构建;若这里失败,再看下面的分项排查,不要跳过 clean。
2确认空安全写法在编译期就能拦住问题
val len = name?.length ?: 0这条不抛异常也不返回 null;替换掉 !! 之后,编译器的可空告警也会随之减少。
3定位依赖版本冲突来自哪个传递依赖
./gradlew dependencies在输出的依赖树里找同一库出现多个版本的位置,按需 exclude 或统一约束版本。
4让阻塞的 IO 调用不占住主线程
withContext(Dispatchers.IO) { readFile(path) }执行后若耗时明显下降,说明之前确实是在主线程上跑阻塞调用。
5快速验证某个协程分支的返回值
runBlocking { val v = async { load() }; println(v.await()) }runBlocking 会在测试或脚本里同步等待,便于观察 await 的实际结果。
6确认数据分组与切分逻辑是否符合预期
list.partition { it > 0 }返回的 Pair 里 first 是满足条件的集合、second 是不满足的,便于定位分类错误。
7把源码打成可分发产物
kotlinc main.kt -include-runtime -d main.jar成功后会生成 main.jar,用 java -jar main.jar 直接验证运行结果。
命令示例
null 安全链:安全调用 + Elvis 兜底一行搞定
val name: String? = loadName()
val len = name?.length ?: 0
println(len)?. 在 name 为 null 时短路返回 null,?: 再把 null 换成 0。绝大多数 !! 场景都能被这个组合替代,从而消除 NPE 而不是把风险推到别处。
输出
0
firstOrNull 代替 first 避免集合为空时抛异常
val user = list.firstOrNull { it.id == 1 }
println(user?.name ?: "anonymous")first() 在无匹配时抛 NoSuchElementException,而这类「查不到」在分页查询里是正常分支。firstOrNull { } 把它变成可空值,配合 ?: 继续链式处理。
输出
anonymous
把 API 数据拍平成 DTO 列表
val rows = response.body
.flatMap { it.items }
.map { Item(id = it.id, title = it.name) }
.groupBy { it.title.first().uppercase() }flatMap 展开一对多结构,map 做 DTO 到视图模型的转换,groupBy 按任意键归并。这一串是列表型页面的标准处理链,且全程返回新列表、不改原集合。
输出
{ A=[Item(id=1, title=Alpha)], B=[Item(id=2, title=Beta)] }withContext 切到 IO 线程执行阻塞调用
suspend fun load(): List<Item> = withContext(Dispatchers.IO) {
repository.blockingQuery()
}协程里直接调阻塞接口会占死所在线程,withContext(Dispatchers.IO) 把这段搬到 IO 线程池再切回原上下文。这是把遗留阻塞代码接进协程体系的标准接缝。
先查清依赖解析版本再升级
./gradlew dependencies --configuration releaseRuntimeClasspath升级前先看解析结果:同一个 + 版本号可能因为传递依赖冲突落到意料之外的版本。确认后再改版本号,能避免「构建突然挂了」且无迹可寻的情况。
常见坑与注意事项
!!非空断言在运行时才失败:类型系统已经放行的路径上,一旦上游真的返回 null 就是线上 NPE,能用?.和?:的位置不要留!!。build/目录里的旧产物会污染增量编译结果,出现「代码明明改了却跑旧逻辑」时先./gradlew clean再判断,不要急着清缓存重装依赖。- Kotlin/JVM 的
==比较的是引用而非内容(与 Java 语义一致),集合去重请用distinct()或数据类的equals,否则会漏判重复项。 lateinit/by lazy推迟的初始化异常会推迟到首次访问才抛出,排查时看到的报错位置与真正出错的位置相差很远,协程里尤其难定位。- 必须用
./gradlew而非全局gradle,否则会用本机 JDK 与 Gradle 版本,表现为别人构建通过、自己构建失败这类难复现问题。 runBlocking会阻塞当前线程直到协程结束,在suspend函数或已有协程作用域里调用会直接死锁,它只应用在测试和main入口。
提示
- Kotlin 的空安全是编译期检查,!! 能不用就不用。
- 协程的 launch 和 async 区别:launch 返回 Job,async 返回 Deferred(可 await)。
- 集合操作链式调用很优雅,但大数据量时注意中间集合的内存开销。
- data class 自动生成 copy/equals/hashCode/toString,适合纯数据模型。
- 作用域函数选择:初始化用 apply,转换用 let/let it,副作用用 also。
- sealed class 搭配 when 使用时编译器会检查分支是否穷举,无需 else。
常见问题
?. 、?: 和 !! 都能"处理 null",实际该怎么选?
优先级明确:?. 是首选,name?.length 在 name 为 null 时整条表达式返回 null,不抛异常;?: 负责给 null 一个默认值,val len = name?.length ?: 0 把两者串起来,是链式调用里最常见的 null-safe 写法;!! 是把 null 当成非空强行解包,一旦实际为 null 就抛 NPE,属于把编译期检查偷换成运行时崩溃。用 !! 的唯一正当理由是你在紧邻的前一行刚做过非空判断,或者该值由不可变构造保证;其余场景都应被 ?. 和 ?: 替代。
协程里 launch 和 async 到底差在哪,什么时候必须用 async?
区别只在返回值:launch 返回 Job,只表示「这个协程启动并结束了」,拿不到结果;async 返回 Deferred,除了等待结束还能拿到结果,用 .await() 取值。规则很直白——协程体里的结果没人需要时用 launch;要拿返回值、并且确实要并发跑多个再一起收集时用 async。调度上两者都默认在当前上下文,需要切到 IO 做阻塞操作时用 withContext(Dispatchers.IO) { ... } 包起来。测试里需要让协程跑完再断言,用 runBlocking { ... } 阻塞等待,别用 delay(1000) 真的睡一秒。
let、apply、also、run 都能把对象"传进 lambda",怎么区分?
分两个维度选:接收者是 it 还是 this,以及返回 lambda 结果还是对象本身。let { it.foo() } 和 with(obj) { foo() } 都返回 lambda 的计算结果,let 用 it 适合链式转换;apply { this.x = 1 } 返回对象本身,接收者是 this,最适合初始化一批属性;also { it.log() } 同样返回原对象但接收者是 it,作用是插入副作用,比如打完日志继续链下去;run { compute() } 是 this 接收者加返回结果,常用于对非空对象连续调用多个成员。记忆口诀:初始化用 apply,转换用 let,副作用用 also。
first() 和 firstOrNull() 只差一个词,列表取值该怎么选?
first() 找不到就抛 NoSuchElementException,firstOrNull { it.id == 1 } 返回 null,所以后者在数据可能不完整的场景更安全,也更适合先判空再处理。配合的还有一批一次性算清的集合操作:groupBy { it.category } 按任意键聚合成 Map,partition { it > 0 } 用一个谓词一次分成两半并返回成对的 Pair(first 是满足的、second 是不满足的),distinct() 按 equals 去重,take(5) 与 drop(3) 配对做分页,sortedBy { it.age } 升序并可 reversed。注意这些都返回新集合、原列表不变,所以链式调用在大数据量上要留意中间集合的内存开销。
sealed class、enum class 和 data class 分别什么时候用?
sealed class Result 定义的是受限继承层次,所有子类必须写在同一个文件里,配合 when 使用时编译器会检查分支是否穷举,因此不需要 else 分支——最适合表示「成功 / 失败 / 加载中」这类互斥状态。enum class Color { RED, GREEN, BLUE } 是固定常量集合,天然自带一组取值。data class User(val name: String, val age: Int) 适合纯数据模型,编译器自动生成 equals、hashCode、toString 和 copy(),改一个字段可以直接 val b = a.copy(age = 30)。若父类要被继承,记得加 open。
由 巧匠 维护
公开更新于 2026年7月21日,内容持续校对官方文档。