编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

    • Kotlin精通

      • Kotlin精通模块总览
      • Kotlin基础语法
        • 1.1 Kotlin语言定位
          • 1.1.1 Kotlin是什么
          • 1.1.2 三大编译目标
          • 1.1.3 与Java对比表
          • 1.1.4 综合案例与思考
        • 1.2 变量声明与类型
          • 1.2.1 val与var区别
          • 1.2.2 类型推断规则
          • 1.2.3 基础类型家族
          • 1.2.4 Any与Unit与Nothing
          • 1.2.5 综合案例与思考
        • 1.3 空安全类型系统
          • 1.3.1 T与T可空对比
          • 1.3.2 安全调用三兄弟
          • 1.3.3 智能类型转换
          • 1.3.4 综合案例与思考
        • 1.4 函数与Lambda
          • 1.4.1 函数声明形式
          • 1.4.2 默认与命名参数
          • 1.4.3 单表达式函数
          • 1.4.4 Lambda基础用法
          • 1.4.5 尾随Lambda惯用
          • 1.4.6 综合案例与思考
        • 1.5 流程控制表达式
          • 1.5.1 if是表达式
          • 1.5.2 when模式匹配
          • 1.5.3 区间与for循环
          • 1.5.4 标签与非局部返回
          • 1.5.5 综合案例与思考
        • 1.6 集合与序列
          • 1.6.1 只读与可变分家
          • 1.6.2 集合创建函数
          • 1.6.3 map与filter链
          • 1.6.4 序列惰性求值
          • 1.6.5 综合案例与思考
        • 1.7 字符串与IO
          • 1.7.1 字符串模板语法
          • 1.7.2 三引号原始串
          • 1.7.3 常用字符串API
          • 1.7.4 综合案例与思考
        • 1.8 综合案例练习
          • 1.8.1 学生成绩管理
          • 1.8.2 案例设计思路
        • 1.9 训练题三道
        • 1.10 新手陷阱Top5
      • 类和对象
      • 高阶特性入门
      • 空安全类型原理
      • data类与equals原理
      • 协程调度器原理
      • inline与reified原理
      • 委托与属性原理
    • 库的解读

    • 专栏博客

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

  • IoT智能硬件开发

  • Apps
  • Android提升进阶
  • Kotlin精通
杨充
2026-07-03
目录

Kotlin基础语法

# 第 1 章 Kotlin 基础语法

# 目录介绍

  • 1.1 Kotlin语言定位
    • 1.1.1 Kotlin是什么
    • 1.1.2 三大编译目标
    • 1.1.3 与Java对比表
    • 1.1.4 综合案例与思考
  • 1.2 变量声明与类型
    • 1.2.1 val与var区别
    • 1.2.2 类型推断规则
    • 1.2.3 基础类型家族
    • 1.2.4 Any与Unit与Nothing
    • 1.2.5 综合案例与思考
  • 1.3 空安全类型系统
    • 1.3.1 T与T可空对比
    • 1.3.2 安全调用三兄弟
    • 1.3.3 智能类型转换
    • 1.3.4 综合案例与思考
  • 1.4 函数与Lambda
    • 1.4.1 函数声明形式
    • 1.4.2 默认与命名参数
    • 1.4.3 单表达式函数
    • 1.4.4 Lambda基础用法
    • 1.4.5 尾随Lambda惯用
    • 1.4.6 综合案例与思考
  • 1.5 流程控制表达式
    • 1.5.1 if是表达式
    • 1.5.2 when模式匹配
    • 1.5.3 区间与for循环
    • 1.5.4 标签与非局部返回
    • 1.5.5 综合案例与思考
  • 1.6 集合与序列
    • 1.6.1 只读与可变分家
    • 1.6.2 集合创建函数
    • 1.6.3 map与filter链
    • 1.6.4 序列惰性求值
    • 1.6.5 综合案例与思考
  • 1.7 字符串与IO
    • 1.7.1 字符串模板语法
    • 1.7.2 三引号原始串
    • 1.7.3 常用字符串API
    • 1.7.4 综合案例与思考
  • 1.8 综合案例练习
    • 1.8.1 学生成绩管理
    • 1.8.2 案例设计思路
  • 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 + "!");
    }
}

几个可视化差别:

  1. 没有类包裹——顶层函数直接就是入口
  2. main 参数可省——不需要总是接收 String[]
  3. 字符串模板 $name——不再拼加号
  4. 类型可推断——val name = "Kotlin" 就够,编译器知道它是 String
  5. 分号可省——一行一句

思考:为什么 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>

三种必须显式声明类型的场合:

  1. 无初始化时:val n: Int; n = 42
  2. 推断结果与预期不同:val n: Long = 42(否则默认 Int)
  3. 公共 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<*>
    }
}

三个失效场景:

  1. 成员 var(可能被其他线程改)

    class C {
        var name: String? = null
        fun show() {
            if (name != null) println(name.length)  // ❌
        }
    }
    
  2. open val(子类可能重写为自定义 getter)

    open class Base { open val name: String? = "x" }
    
  3. 跨模块的 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 案例设计思路

按四步法思考:

  1. 建模——先定义 data class Student,把「实体 + 数据」固化到类型系统里
  2. 准备数据——用 listOf 或从文件 / 网络读入,得到 List<Student>
  3. 单次遍历取多个统计量——用 map/count/groupBy 组合
  4. 格式化输出——用字符串模板 + 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 委托,还会顺便「下沉一层」看看这些糖背后的字节码。

上次更新: 2026/07/05, 15:10:54
Kotlin精通模块总览
类和对象

← Kotlin精通模块总览 类和对象→

最近更新
01
11.数据库的原理总结
07-12
02
11.操作系统原理总结
07-10
03
资深程序员软能力
07-10
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式