同一份任务标签 "Queued"、"Urgent"、"Quiet"、"Later",先转小写、再筛选 q 开头、最后 take(1)。既然第一项 "Queued" 已经符合条件,后面三项还会不会参与计算?直接对 List 链式调用与先转成 Sequence,得到的结果一样,却有不同的调用轨迹。这不是一次计时竞赛,而是要确定操作在什么时候执行。

本篇承接 07 的同步标签函数,继续在同一个 Kotlin/JVM 2.2.21、JDK 21、Gradle Wrapper 8.14 的累计工程里做实验。查询规则仍未接入 CLI,也没有后台任务;所有结论都以本章的四项输入和具体 List/List.asSequence() 构造方式为边界。

直接对 List 调用时,take(1) 已经太晚

急切版本把回调安在转换和筛选位置,用于记录执行顺序:

1
2
3
4
5
6
7
8
fun eagerTaskLabels(labels: List<String>, record: (String) -> Unit): List<String> =
labels.map { label ->
record("map:$label")
label.lowercase()
}.filter { label ->
record("filter:$label")
label.startsWith("q")
}.take(1)

在 TaskLabelPipelineTest 的四项输入上,调用这个函数后轨迹先出现四条 map:,接着是四条 filter:;最后结果才是 listOf("queued")。take(1) 只截取已经完成的过滤结果,不能倒回去省掉先前 map 和 filter 对后三项的处理。官方集合操作说明和 Sequence 的对照例子可以用来解释这个具体调用链;单看返回的列表,却看不出它做了多少中间工作。

输入四项不是性能测试样本,这里也没有测堆分配或运行时间。仅凭“Sequence 少调用六次”不能宣称某种写法更快:迭代器、函数调用、数据大小、终端操作和 JVM 优化都会影响成本。37 才计划用固定工作负载做成本诊断。

Sequence 先组合规则,终端收集才真正调用

懒惰版本只把同一份标签改为 asSequence(),再接同样的规则:

1
2
3
4
5
6
7
8
fun lazyTaskLabels(labels: List<String>, record: (String) -> Unit): Sequence<String> =
labels.asSequence().map { label ->
record("map:$label")
label.lowercase()
}.filter { label ->
record("filter:$label")
label.startsWith("q")
}.take(1)

测试先构造 sequence,立即检查轨迹:空。第一次执行 sequence.toList(),依次只出现 "map:Queued"、"filter:queued",返回 listOf("queued")。第一项已经满足 filter 且 take(1) 已收够,后三项在这一次收集中没有走到转换或筛选。具体的区分是官方 Sequence 中间操作和终端操作:这里 map、filter、take 是中间操作,toList() 才触发求值。

第二次对本章由 List.asSequence() 得到的 sequence 再调用 toList(),轨迹又增加一对 map:Queued、filter:queued。这不是第一次结果被缓存;当前构造方式在每次遍历时重新调用规则。不能把这个测试泛化成“所有 Sequence 都能遍历两次”:序列还可能由一次性迭代来源或其他构造方式得到,应根据来源与 API 约束判断。

时点 对 List 的链条 对本章 List.asSequence() 的链条
构造/调用结束 map 4 次、filter 4 次,已有结果 尚未收集,map/filter 均 0 次
第一次要结果 已有 queued toList() 触发 map 1 次、filter 1 次,得到 queued
再读一次 原 List 值不自动重算 再 toList() 又执行 map 1 次、filter 1 次

表中计数来自 TaskLabelPipelineTest 的断言,不是把任意 Sequence 或任意集合的时间复杂度写成常量。要考虑 sorted、distinct 等带状态的中间操作、不同终端操作或共享可变源时,必须分别重新测。尤其是 06 的别名问题还在:把一个可变 List 转成 Sequence 并没有冻结底层对象。

验证覆盖了什么

在仓库根目录运行 cd examples/kotlin-language && bash verification/08.sh。脚本记录时间、系统、JDK 与 Wrapper 环境,离线 clean test 退出 0;00–08 九组 suite 各 2/2。TaskLabelPipelineTest 一组断言完整急切轨迹,另一组断言延迟、截断和对本章来源的重复收集;旧 submit alpha CLI 退出 0,缺文本 submit 退出 1,只用于回归。原始命令、XML、输出和断言位于 writing-plans/kotlin/verification/08/。没有性能数值、真实网络或并发证据。

下载含Wrapper、完整累计工程和08证据的快照(SHA-256校验)。素材目录给出解包后的 clean test 和 CLI 复跑命令;序列实验只是同步内存操作,没有定义任务执行效率。

练习与答案

练习一:对本篇 List 输入,只在最后加 take(1),filter 调用几次?答:四次。map 先处理四项,filter 再处理四项,take 最后从过滤结果拿第一项;本篇测试保存了这八个调用事件的确切顺序。

练习二:构造 lazyTaskLabels(...) 后立即观察计数是多少?两次 toList() 后呢?答:构造时 0;第一次 map、filter 各 1 次,第二次同样各加 1。这里由 List.asSequence() 构造,不能套用到任意一次性序列。

练习三:减少回调次数是否足以决定生产系统一定改用 Sequence?答:不足。当前证据只有四项输入的调用轨迹;成本还包括中间集合、迭代器、终端消费方式和 JVM 优化。若任务负载很小或需要多次重用结果,先列清语义与输入,再单独测性能。

下一篇开始引入任务类;前八篇的字符串与集合实验只是合成输入及变换,还没有任务实体或持久化。