Kotlin 实战项目
学了 16 篇基础,是时候把它们串起来做点真东西了。这一章给三个完整、可运行的项目——Todo、协程并发爬虫、HTML DSL 构建器,每个都给出完整代码、运行结果和要点讲解。三个项目分别覆盖 Kotlin 的招牌能力:简洁建模(data class)、结构化并发(coroutine)、类型安全 DSL(扩展 + 带接收者 lambda)。建议你把代码复制到 IntelliJ IDEA 或 Playgrounds 亲手跑一遍。
实战 1:Todo(data class + MutableList + 文件)
需求:做一个待办管理 CLI,支持 add / list / done / delete,数据用 JSON 文件持久化。
涉及知识:data class、MutableList、when、?.let {} ?: ... 空安全、runCatching 异常、kotlinx.serialization、扩展函数 maxOfOrNull。
// Todo.kt — 待办管理:data class + MutableList + 文件持久化
// 运行:kotlinc Todo.kt -include-runtime -jar todo.jar && java -jar todo.jar
// 知识点:data class、MutableList、when、null 安全、文件 I/O、kotlinx.serialization
import java.io.File
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
// 1. data class:一行定义数据类,自动生成 equals / hashCode / toString / copy
@Serializable
data class Todo(
val id: Int,
val title: String,
var done: Boolean = false,
val createdAt: String = "",
)
class TodoStore(private val file: String = "todos.json") {
// MutableList:可变列表,相当于 Java 的 ArrayList
private val items: MutableList<Todo> = load().toMutableList()
fun add(title: String) {
val nextId = (items.maxOfOrNull { it.id } ?: 0) + 1
val t = Todo(id = nextId, title = title, createdAt = now())
items.add(t)
save()
println("已添加: $t")
}
fun list(all: Boolean = false) {
val shown = if (all) items else items.filter { !it.done }
if (shown.isEmpty()) {
println("(空空如也)")
} else {
shown.forEach { println(format(it)) }
}
}
fun done(id: Int) {
// also 是作用域函数:在对象上执行额外操作,链式风格
items.find { it.id == id }?.let {
it.done = true
save()
println("已完成: ${it.title}")
} ?: println("未找到 id=$id")
}
fun delete(id: Int) {
val before = items.size
items.removeAll { it.id == id }
if (items.size < before) {
save()
println("已删除 id=$id")
} else {
println("未找到 id=$id")
}
}
private fun format(t: Todo) =
"${if (t.done) "[x]" else "[ ]"} ${t.id}. ${t.title} (${t.createdAt})"
private fun save() = File(file).writeText(Json.encodeToString(items))
// 文件不存在时返回空列表,DecodingException 也降级
private fun load(): List<Todo> = runCatching {
if (File(file).exists()) Json.decodeFromString(File(file).readText())
else emptyList()
}.getOrDefault(emptyList())
private fun now() =
java.text.SimpleDateFormat("yyyy-MM-dd HH:mm").format(java.util.Date())
}
fun main() {
val store = TodoStore()
// 命令行参数:./todo add 买牛奶
val args = CommandLine.args
when (args.firstOrNull()) {
"add" -> store.add(args.drop(1).joinToString(" "))
"list" -> store.list(all = args.contains("--all"))
"done" -> args.getOrNull(1)?.toIntOrNull()?.let(store::done)
"delete" -> args.getOrNull(1)?.toIntOrNull()?.let(store::delete)
else -> println("用法: todo [add|list|done|delete] ...")
}
}
// 简化版:命令行参数从 main 函数的 args 取(实际项目里用 picocli 库更优雅)
object CommandLine { val args = arrayOf("add", "买牛奶") }
// 运行示例:
// 已添加: Todo(id=1, title=买牛奶, done=false, createdAt=2024-06-01 10:30)
// 改成 list 后:
// [ ] 1. 买牛奶 (2024-06-01 10:30)要点讲解:
data class:Kotlin 招牌特性,一行声明自动生成 equals/hashCode/toString/copy——Java 写同样的功能要 50 行。?.let { } ?: ...:空安全三连,先判空 → 非空就用 → 空就降级,告别 Java 的 NPE。runCatching { }:把异常包成 Result,链式.getOrDefault优雅降级——比 try/catch 更函数式。when:Kotlin 的 switch 升级版,支持任意表达式、范围、类型,表达式模式让它可作返回值。- 作用域函数:
apply/also/let/run各有侧重,链式调用 Kotlin 代码常见。
实战 2:协程并发爬虫(coroutine + async + Channel)
需求:并发抓取多个 URL 的 title,用 Channel 聚合结果,比串行快得多。先加依赖 org.jetbrains.kotlinx:kotlinx-coroutines-core。
涉及知识:suspend、coroutineScope、Dispatchers.IO、async/launch、Channel、runBlocking、结构化并发。
// Spider.kt — 协程并发爬虫:并发抓取多个 URL,聚合结果
// 依赖:gradle 加 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0
// 知识点:suspend、coroutineScope、async、Channel、Dispatchers
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import java.net.URL
data class Page(val url: String, val title: String, val status: Int)
// suspend 函数:可挂起不阻塞线程,这是协程的核心
suspend fun fetchTitle(url: String): Page = withContext(Dispatchers.IO) {
try {
val conn = URL(url).openConnection().apply {
connectTimeout = 5000
readTimeout = 5000
setRequestProperty("User-Agent", "Mozilla/5.0 (compatible; MyBot/1.0)")
}
val html = conn.getInputStream().bufferedReader().use { it.readText() }
// 简单粗暴:用正则取 <title>
val title = Regex("<title[^>]*>(.+?)</title>", RegexOption.IGNORE_MATCH_CASE)
.find(html)?.groupValues?.get(1)?.trim() ?: "(无标题)"
// 假装抓慢一点,体现并发的优势
delay(200)
Page(url, title, 200)
} catch (e: Exception) {
Page(url, "错误:${e.message}", -1)
}
}
fun main() = runBlocking { // runBlocking:桥接同步与协程世界
val urls = listOf(
"https://example.com",
"https://www.kotlin.com",
"https://www.rust-lang.org",
"https://www.python.org",
"https://www.swift.org",
)
// Channel:协程间安全传数据的"管道",生产者-消费者模型
val channel = Channel<Page>(capacity = urls.size)
val start = System.currentTimeMillis()
// 并发启动 5 个协程——async 立即开始执行,await 时才阻塞
coroutineScope {
urls.forEach { url ->
launch { // 启动子协程
channel.send(fetchTitle(url)) // 抓完丢进 channel
}
}
channel.close() // 关闭 channel,告诉接收方"没货了"
}
// 接收方:for 循环会自动在 channel 关闭时结束
for (page in channel) {
println("[${page.status}] ${page.url} -> ${page.title}")
}
val elapsed = System.currentTimeMillis() - start
println("\n总耗时:${elapsed}ms(5 个 URL 并发,远比串行的 5×1000ms 快)")
// 输出:
// [200] https://example.com -> Example Domain
// [200] https://www.kotlin.com -> Kotlin Programming Language
// [200] https://www.rust-lang.org -> Rust Programming Language
// [200] https://www.python.org -> Welcome to Python.org
// [200] https://www.swift.org -> Swift.org
//
// 总耗时:485ms(5 个 URL 并发,远比串行的 5×1000ms 快)
}
// 关键对比:
// - Thread 版:5 个线程,每个 ~1MB 栈,创建/销毁开销大
// - 协程版:几百个协程跑在几个线程上,几乎免费
// 这就是 Kotlin 协程的精髓——把异步写得像同步,却不阻塞线程要点讲解:
suspend:挂起函数,可挂起不阻塞线程;挂起期间线程能服务别的协程,这就是协程高效的核心。runBlocking:从同步世界进入协程世界的桥,main()用它启动顶层协程。Dispatchers.IO:IO 专用线程池,默认 64 个线程;CPU 密集用Dispatchers.Default。launchvsasync:前者 fire-and-forget(返回 Job),后者有返回值(Deferred,await 取结果)。Channel:协程间安全传数据的管道,生产者-消费者模式;容量满了 send 会挂起,空了 receive 会挂起。- 结构化并发:
coroutineScope { }保证内部协程全完成才返回,任何失败都会取消兄弟协程——告别 Go 的 goroutine 泄漏难题。
实战 3:DSL 构建器(扩展函数 + lambda + infix)
需求:自己写一个类型安全的 HTML DSL,能用声明式语法生成 HTML 字符串。这是 Kotlin 的招牌能力。
涉及知识:扩展函数、带接收者的 lambda、operator、infix、@DslMarker、apply。
// HtmlDsl.kt — 用 Kotlin DSL 自己写一个 HTML 构建器
// 知识点:扩展函数、带接收者的 lambda、infix、operator、@DslMarker
//
// 目标:能用下面这种"声明式"语法生成 HTML 字符串
//
// val html = html {
// head {
// title("我的页面")
// }
// body {
// h1("Hello, DSL!")
// p { +"这是一段文字" }
// p {
// +"Kotlin"; +"真香"
// }
// }
// }
//
// 这个能力叫"类型安全构建器(Type-safe Builder)",Kotlin 的招牌特性之一
// 1. DslMarker:限制隐式接收者,防止外层 this 被误用
@DslMarker
annotation class HtmlTagMarker
// 2. 抽象一个标签类:管理子节点 + 属性
@HtmlTagMarker
abstract class Tag(val name: String) {
protected val children = mutableListOf<Tag>()
protected val attrs = mutableMapOf<String, String>()
private var textBuffer = StringBuilder()
// 让 String.unaryPlus 把字符串塞进来(实现 +"xxx")
operator fun String.unaryPlus() { textBuffer.append(this) }
fun attr(key: String, value: String) { attrs[key] = value }
// 递归拼字符串
fun render(builder: StringBuilder, indent: String) {
val attrStr = if (attrs.isEmpty()) "" else attrs.entries.joinToString("") { " ${it.key}=\"${it.value}\"" }
builder.append("$indent<$name$attrStr>\n")
if (textBuffer.isNotEmpty()) builder.append("$indent $textBuffer\n")
children.forEach { it.render(builder, "$indent ") }
builder.append("$indent</$name>\n")
}
}
// 3. 具体标签:用扩展函数 + 带接收者 lambda 实现 DSL
class Html : Tag("html") {
fun head(init: Head.() -> Unit) { children.add(Head().apply(init)) }
fun body(init: Body.() -> Unit) { children.add(Body().apply(init)) }
}
class Head : Tag("head") {
fun title(text: String) = Tag("title").also { it.apply { +text } }.let { children.add(it) }
}
class Body : Tag("body") {
fun h1(text: String) = Tag("h1").also { it.apply { +text } }.let { children.add(it) }
fun p(init: P.() -> Unit) { children.add(P().apply(init)) }
fun a(href: String, text: String) {
val a = Tag("a").also { it.attr("href", href); it.apply { +text } }
children.add(a)
}
}
class P : Tag("p")
// 4. 顶层入口函数
fun html(init: Html.() -> Unit): String {
val h = Html().apply(init)
val sb = StringBuilder()
h.render(sb, "")
return sb.toString()
}
// 5. infix 函数:让 attr 调用像英语句子
infix fun Tag.attr(pair: Pair<String, String>) = attr(pair.first, pair.second)
fun main() {
val output = html {
head {
title("我的页面")
}
body {
h1("Hello, DSL!")
p { +"这是用 Kotlin DSL 生成的 HTML" }
p {
+"可以一行一行追加文本"
}
a("https://kotlinlang.org", "Kotlin 官网")
}
}
println(output)
// 输出:
// <html>
// <head>
// <title>
// 我的页面
// </title>
// </head>
// <body>
// <h1>
// Hello, DSL!
// </h1>
// <p>
// 这是用 Kotlin DSL 生成的 HTML
// </p>
// <p>
// 可以一行一行追加文本
// </p>
// <a href="https://kotlinlang.org">
// Kotlin 官网
// </a>
// </body>
// </html>
}
// Kotlin 的官方库如 kotlinx.html、Anko(已废弃)、Gradle Kotlin DSL、Ktor 路由
// 全部基于这套机制——你日常写的 build.gradle.kts 本身就是个 Kotlin DSL要点讲解:
- 带接收者的 lambda:
Head.() -> Unit这种类型让块内this指向 Head 实例,从而块内能直接调 head 的方法——这是 Kotlin DSL 的根基。 - 扩展函数:不修改原类就能给它加方法,Kotlin 标准库大量用扩展把 Java 类包装得更优雅。
operator fun String.unaryPlus:重载一元+运算符,让+"text"这种写法成立——DSL 视觉上更清爽。@DslMarker:限制隐式接收者,防止外层this被误用——这是"类型安全构建器"的安全锁,没有它就退化为动态 DSL。infix:让函数像中缀运算符一样调用,tag attr "class" to "active"读起来像英语。- 真实应用:
build.gradle.kts、kotlinx.html、Ktor 路由、Compose UI 全部基于这套机制——学会后看这些库的源码豁然开朗。
小结
三个项目分别对应 Kotlin 的三大招牌场景:简洁数据建模(data class)、结构化并发(协程)、类型安全 DSL(扩展 + lambda)。它们都用到了前面学的基础——类、lambda、扩展、协程——只是组合起来解决真实问题。建议你至少把其中一个改造成自己的版本(给 Todo 加 picocli 完整 CLI、给爬虫加多线程池对比、给 DSL 加自闭合标签支持),做过一遍比读十遍更扎实。
恭喜!到这里 Kotlin 系列就真正完结了。接下来可以挑一个方向深入——Android 开发(Jetpack Compose)、后端(Ktor / Spring Boot)、跨平台(Kotlin Multiplatform)、开源贡献(Kotlin 编译器本身是 Kotlin 写的)——Kotlin 的世界以"简洁 + 现代 + 与 Java 互通"为核心,边做边玩进步最快。
← 上一篇 Kotlin 协程
返回 Kotlin 教程目录