Sequence 一定比 List 快?等等,我们先从基础讲起

各位 Kotliner,周一好!今天我们研究一下 Kotlin Iterator 的相关技术。

在 Kotlin 中,我们经常会使用 formapfiltertake 这些 API 处理集合。

这些 API 可以说是日常开发中使用频率最高的工具之一,但坦白讲,很多开发者可能没有真的理解它们背后的机制。

你有没有想过:

  1. for 循环为什么能够遍历 List
  2. Sequence 为什么可以做到惰性执行?
  3. Sequence 真的比普通集合操作更高效吗?什么情况下反而会更慢?
  4. 为什么遍历集合时不能直接删除元素?

要理解这些问题,需要先从 Kotlin 集合遍历的基础------Iterator 开始。

Iterator 的工作机制

迭代器允许我们在不暴露集合内部结构的情况下,一次访问其中的一个元素。

在 Kotlin 中,Iterator 是集合 API 的基础之一,它为各种集合提供了一套统一的顺序访问方式。

继续看它的定义会发现,Iterator 其实非常简单,核心只有两个操作符函数:hasNext()next()

kotlin 复制代码
/**
 * 用于遍历集合,或者其他可以表示为一系列元素的对象。
 * 允许按照顺序访问其中的元素。
 */
public actual interface Iterator<out T> {
    /**
     * 返回迭代过程中的下一个元素。
     *
     * 如果已经没有下一个元素,则抛出 NoSuchElementException。
     */
    public actual operator fun next(): T

    /**
     * 如果迭代过程中还有更多元素,则返回 true。
     */
    public actual operator fun hasNext(): Boolean
}

这套设计体现的正是经典的迭代器模式

hasNext() 用于判断后面是否还有元素,避免访问越界;next() 则返回下一个元素,同时推进迭代器内部的遍历状态。

虽然接口中只有两个函数,但正是这份简单的约定,让 ListSet,甚至自定义的数据结构,都可以使用统一的方式进行顺序遍历。

Iterator 的核心能力

Iterator 接口主要围绕两个相互配合的函数展开:hasNext()next()

  1. hasNext(): Boolean

    hasNext() 用于检查当前迭代器是否还有下一个元素。

    你可以把它理解成在询问:"遍历已经结束了吗?后面还有没有元素?"

    如果仍然可以继续遍历,它会返回 true;当所有元素都已经访问完毕时,它会返回 false

    需要注意的是,调用 hasNext() 不会把调用方可见的遍历位置移动到下一个元素。它只是告诉我们当前是否还能继续调用 next()

  2. next(): T

    next() 会返回迭代过程中的下一个元素,并推进迭代器内部的状态,使下一次调用可以继续访问后面的元素。

    它是我们从迭代器中真正取出数据的主要方式。

为了避免错误,只有在确定仍然存在下一个元素时,才应该调用 next()

如果迭代器已经到达末尾,也就是此时 hasNext() 会返回 false,但我们仍然继续调用 next(),那么它将抛出 NoSuchElementException

因此,hasNext()next() 几乎总是配合循环一起使用,从而形成一种安全、可靠的遍历方式。

使用 Iterator

下面手动使用 Iterator 遍历一个列表:

kotlin 复制代码
val names = listOf("rockbyte", "kotlin", "developer")
val iterator = names.iterator()

while (iterator.hasNext()) {
    println(iterator.next())
}

在这个例子中,iterator() 会为列表创建一个迭代器。

随后,while 循环不断调用 hasNext() 检查是否还有元素,并通过 next() 依次取出它们。

当然,平时写代码时,我们很少会为了遍历集合而手动操作迭代器。更常见的写法是直接使用 for 循环:

kotlin 复制代码
val names = listOf("rockbyte", "kotlin", "developer")

for (name in names) {
    println(name)
}

从 Kotlin 的语言规则来看,这段 for 循环在语义上仍然依赖 iterator()hasNext()next()

可以将它理解为下面这种展开形式:

kotlin 复制代码
val iterator = names.iterator()

while (iterator.hasNext()) {
    val name = iterator.next()
    println(name)
}

因此,for 循环并没有绕开迭代器,而是帮助我们隐藏了迭代器的操作细节,让代码更加简洁、易读。

这里说的是 Kotlin 语言层面的语义展开。对于数组、区间等常见类型,编译器在实际生成代码时还可能进行专门优化,并不意味着最终字节码一定与上面的 while 代码完全相同。

MutableIterator

普通的 Iterator 只负责遍历和读取元素,但有些时候,我们还需要在遍历过程中修改集合。

例如,在遍历一个列表时,将满足某个条件的元素删除。

为此,Kotlin 提供了 MutableIterator 接口。顾名思义,它继承自基础的 Iterator,因此仍然拥有 hasNext()next(),同时增加了一个重要能力:在遍历过程中安全地删除元素。

下面是 MutableIterator 的接口定义:

kotlin 复制代码
/**
 * 用于遍历可变集合的迭代器。
 * 支持在遍历过程中删除元素。
 *
 * @see MutableCollection.iterator
 */
public actual interface MutableIterator<out T> : Iterator<T> {
    /**
     * 从底层集合中删除最近一次由该迭代器返回的元素。
     *
     * 如果还没有调用过 next,或者最近一次 next 调用之后
     * 已经调用过一次 remove,则抛出 IllegalStateException。
     */
    public actual fun remove(): Unit
}

MutableIterator 的核心能力

MutableIterator 主要有下面两个特点。

  1. 继承自 Iterator

    因为 MutableIterator 继承了 Iterator<T>,所以它可以完成普通迭代器能够完成的全部工作。

    我们仍然可以在 while 循环中调用 hasNext(),并通过 next() 获取元素。

  2. 增加了 remove() 函数

    remove()MutableIterator 最重要的扩展。

    它会删除最近一次由 next() 返回的元素。

这里有一个非常关键的限制:我们不能在任意时机随便调用 remove()

必须先调用 next(),让迭代器确定当前正在处理哪个元素,然后才能调用 remove() 将这个元素从底层集合中删除。

如果使用方式不正确,remove() 会在下面两种情况下抛出 IllegalStateException

  • 还没有调用过 next():在迭代器尚未返回任何元素之前,没有可供删除的目标。
  • 连续调用两次 remove() :删除一个元素之后,必须再次调用 next() 移动到新的元素,才能继续调用 remove()

使用 MutableIterator

在遍历集合的同时删除元素时,使用 MutableIterator.remove() 是一种安全的方式。

如果在普通 for 循环中直接调用集合自身的删除函数,集合的结构会在迭代器并不知情的情况下发生变化。在 JVM 上,许多常见集合实现会检测到这种结构性修改,并抛出 ConcurrentModificationException

假设我们要从一个可变整数列表中删除所有偶数,可以这样写:

kotlin 复制代码
val numbers = mutableListOf(1, 2, 3, 4, 5, 6, 7, 8)
val iterator = numbers.iterator()
// 在 MutableList 上调用 iterator(),返回的是 MutableIterator

while (iterator.hasNext()) {
    val number = iterator.next()

    if (number % 2 == 0) {
        // 删除刚刚由 next() 返回的元素
        iterator.remove()
    }
}

println(numbers) // 输出:[1, 3, 5, 7]

在这个例子中,元素删除操作是通过当前正在执行遍历的 MutableIterator 完成的。

因此,迭代器能够同步维护自己的内部状态,在删除元素后继续访问正确的位置,避免因为集合结构发生变化而破坏遍历过程。

当然,如果需求只是"删除所有满足条件的元素",在实际业务代码中也可以直接使用更简洁的 removeAll

kotlin 复制代码
numbers.removeAll { it % 2 == 0 }

不过,理解 MutableIterator 仍然很重要。它不仅解释了遍历时安全删除元素的机制,也能帮助我们理解为什么直接修改正在遍历的集合可能会出问题。

小结

Iterator 为不同类型的集合提供了一种简单而统一的顺序遍历方式。

普通的 Iterator 负责读取元素;MutableIterator 则在此基础上增加了修改能力,允许我们在遍历过程中安全地删除当前元素。

无论是显式调用 iterator(),还是使用更符合 Kotlin 习惯的 for 循环,它们背后都离不开迭代器这套简单的访问协议。

Sequence 是什么

在 Kotlin 标准库中,IterableSequence 都可以用来处理一系列元素。

ListSet 等集合都实现了 Iterable。从 API 表面来看,IterableSequence 提供的很多操作都非常相似,例如 mapfilterflatMap,甚至连调用方式都几乎一样。

不过,它们底层的执行模型存在明显区别。

对于 Iterable,常见的多步集合转换会采用立即执行的方式:先对整个集合完成当前操作,并产生中间结果,然后再对中间结果执行下一步操作。

Sequence 采用的是惰性执行。它会根据需要逐个处理元素,只有下游真正请求结果时,相关计算才会发生。

整个 Sequence 机制建立在一个非常简单的约定之上:iterator() 操作符函数。

kotlin 复制代码
public interface Sequence<out T> {
    /**
     * 返回一个可以依次产生该序列元素的 Iterator。
     *
     * 如果该 Sequence 被限制为只能遍历一次,
     * 第二次调用 iterator() 时会抛出异常。
     */
    public operator fun iterator(): Iterator<T>
}

与已经将元素保存在内存中的 ListSet 等集合不同,Sequence 更像是对"如何产生一系列元素"以及"这些元素需要经过哪些处理"的描述。

它最核心的承诺只有一个:当外部请求时,它能够提供一个 Iterator

真正驱动整个序列运行的,就是这个 Iterator。它知道如何产生或读取下一个元素,但通常只有在外部调用 next() 时,才会真正去获取和处理这个元素。

这种由下游不断请求上游提供数据的方式,也可以称为一种 pull-based,也就是拉取式模型

Sequence 的惰性正是由此而来。

与普通集合会立即执行转换不同,Sequence 中的 mapfilterflatMap 等中间操作不会立即处理所有元素。

它们会推迟计算,直到 toList()count()first() 等终止操作开始消费序列。

创建 Sequence

我们可以通过 sequenceOf()generateSequence() 创建一个 Sequence,也可以调用集合的 asSequence() 将其转换为序列。

kotlin 复制代码
val numbers = sequenceOf(1, 2, 3, 4, 5)
println(numbers.toList()) // 输出:[1, 2, 3, 4, 5]

val generated = generateSequence(1) { it + 1 }
println(generated.take(5).toList()) // 输出:[1, 2, 3, 4, 5]

第二个例子中的 generateSequence(1) { it + 1 } 会产生一个没有自然终点的无限序列。

正因为 Sequence 是按需生成元素的,所以我们可以先使用 take(5) 限制数量,再通过 toList() 取出前五个结果。

如果直接对这个无限序列调用 toList()count(),计算将无法自然结束。

转换 Sequence

mapfiltertake 等转换函数同样可以用于 Sequence

这些操作是惰性的。也就是说,调用它们时只是在描述处理步骤,只有终止操作真正开始消费结果时,元素才会被处理。

kotlin 复制代码
val sequence = sequenceOf(1, 2, 3, 4, 5)

val transformed = sequence
    .map { it * 2 }
    .filter { it > 5 }

println(transformed.toList()) // 输出:[6, 8, 10]

如果继续查看 Sequencemapfilter 等中间操作的实现,就会发现它的设计非常巧妙。

调用这些操作时,它们不会立即遍历原始序列,而是创建并返回一个新的 Sequence 对象。

新的 Sequence 会保存原来的上游序列,以及当前需要执行的转换操作。多个操作连续调用后,就会形成一层包裹一层的结构,每一层都保存着整个计算过程中的一个步骤。

这种结构有些类似装饰器模式:后一个 Sequence 包装前一个 Sequence,最终组成一条完整的处理链。

下面来看一个概念化的 Sequence.map() 实现:

kotlin 复制代码
// 这是经过简化、但在概念上与标准库实现一致的版本。
// 标准库中的实际实现使用了 TransformingSequence。
public fun <T, R> Sequence<T>.map(
    transform: (T) -> R
): Sequence<R> {
    // 1. 这里不会遍历原始 Sequence。
    // 2. 它只是返回一个新的 Sequence,
    //    并保存原始 Sequence 和转换函数。
    return object : Sequence<R> {

        override fun iterator(): Iterator<R> {
            // 3. 当外部请求新 Sequence 的迭代器时,
            //    再创建一个负责转换的 Iterator。
            return object : Iterator<R> {

                // 4. 获取上游原始 Sequence 的 Iterator。
                val originalIterator = this@map.iterator()

                override fun hasNext(): Boolean {
                    // 5. hasNext() 可以直接委托给上游 Iterator。
                    return originalIterator.hasNext()
                }

                override fun next(): R {
                    // 6. 只有外部调用 next() 时,
                    //    才从上游取出一个元素,并只转换这一个元素。
                    val originalElement = originalIterator.next()
                    return transform(originalElement)
                }
            }
        }
    }
}

从这段代码可以看到,调用 map() 时几乎没有进行实际的数据处理。

它只是创建了一个新的 Sequence,保存上游序列和 transform 函数,相当于记录了一份"之后应该如何转换数据"的说明。

filtertake 以及其他中间操作也遵循相似的思路。每调用一次中间操作,处理链中就会增加一层新的包装。

不过,不同操作的迭代器实现并不完全一样。

例如,maphasNext() 基本可以直接委托给上游;而 filter 为了判断是否存在"下一个符合条件的元素",它的 hasNext() 可能需要继续向上游读取若干元素,并把找到的结果暂时缓存起来。

虽然具体实现不同,但它们遵循同一套核心思路:不提前完成整个集合的计算,而是在下游请求元素时,按需完成当前元素所需要的工作。

终止操作

在终止操作被调用之前,整个 Sequence 处理链都处于尚未真正执行的状态。

终止操作会消费序列中的元素,并产生一个最终结果,例如:

  • toList()
  • forEach()
  • count()
  • first()

终止操作会调用处理链最后一个 Sequenceiterator()

最后一个 Sequence 又会请求自己所包装的上游 Sequence 的迭代器,如此一层层向前,直到最初的数据源。

迭代器链建立完成后,终止操作开始调用 hasNext()next(),逐个把元素从上游拉取下来,并让它依次经过整条处理链。

来看下面这个例子:

kotlin 复制代码
val result = (1..10).asSequence()
    .filter {
        println("Filtering $it")
        it % 2 == 0
    }
    .map {
        println("Mapping $it")
        it * 2
    }
    .take(2) // 另一个中间操作
    .toList() // 终止操作,真正启动整条处理链

println("Result: $result")

这段代码真正的执行起点是 toList()

它的执行过程大致如下。

  1. toList() 请求 take(2) 对应的迭代器,并准备一个列表用于收集最终结果。
  2. take(2) 的迭代器向 map 迭代器请求第一个元素。
  3. map 迭代器继续向 filter 迭代器请求元素。
  4. filter 迭代器开始从原始数据源 (1..10) 中拉取数据。
    • 取出 1,输出 Filtering 1。它不满足偶数条件,因此继续查找。
    • 取出 2,输出 Filtering 2。它满足条件,于是 filter 返回 2
  5. map 收到 2,输出 Mapping 2,执行转换 2 * 2 = 4,然后返回 4
  6. take(2)4 交给下游的 toList()toList() 把它加入结果列表,此时 take 还允许再返回一个元素。
  7. toList() 再次请求下一个元素,take(2) 继续向 map 请求第二个结果。
  8. 上面的过程再次发生:
    • filter 取出 3,输出 Filtering 3,条件不成立。
    • filter 取出 4,输出 Filtering 4,条件成立,于是返回 4
  9. map 收到 4,输出 Mapping 4,将它转换为 8 并返回。
  10. take(2)8 交给 toList()toList() 把它加入结果列表,此时 take(2) 已经返回了两个元素。
  11. 当下游再次检查是否还有元素时,take(2) 返回 falsetoList() 随即结束,原始数据源中只有 14 被实际处理。

最终输出如下:

text 复制代码
Filtering 1
Filtering 2
Mapping 2
Filtering 3
Filtering 4
Mapping 4
Result: [4, 8]

这个例子展示了 Sequence 的几个核心特点。

  • 惰性执行

    在终止操作被调用之前,中间操作不会真正开始处理元素。

  • 逐元素处理

    一个元素会先经过 filter,再经过 map,最后到达 take。处理完当前元素后,才会继续拉取后面的元素。

    对于这里使用的 filtermaptake,处理过程中不需要像普通集合的多步转换那样,为每一步创建完整的中间集合,因此通常能够减少额外的内存占用。

  • 短路能力

    take()first() 等操作在获得所需结果后,可以让整条处理链提前结束,不必继续处理数据源中剩余的元素。

还需要注意一点:惰性执行并不意味着所有 Sequence 操作都不需要额外状态。

mapfilter 这类操作可以逐个处理元素;但 distinct 需要记录已经出现过的元素,排序操作通常也需要先收集数据。因此,Sequence 避免的是许多不必要的中间集合,并不是所有场景都只占用常量级内存。

小结

Sequence 的内部机制,是惰性求值在 Kotlin 标准库中的一个典型实现,而它的基础仍然是那个非常简单的 Iterator 接口。

mapfilter 等中间操作不会立即执行。它们会创建一个新的 Sequence,包装前一个序列,并把需要执行的操作保存下来,从而逐步构建出完整的计算流程。

只有当 toList()count()first() 等终止操作被调用时,最终的 Iterator 链才会开始工作。元素会从数据源中被逐个拉取,并依次通过整条处理管道。

这种设计可以避免许多中间集合,并且能够配合 take()first() 等操作提前结束计算,因此特别适合下面这些场景:

  • 数据量比较大;
  • 存在较长的多步转换链;
  • 只需要前几个符合条件的结果;
  • 需要处理按需生成的数据,甚至无限序列。

不过,Sequence 并不等于无条件地"性能更高"。

惰性处理需要额外的 SequenceIterator 包装,也会增加间接函数调用。对于数据量很小、操作非常简单的集合,直接使用普通集合操作可能反而更加直接,甚至更快。

因此,更准确的理解不是"看到集合操作就调用 asSequence()",而是先判断当前任务能否从惰性计算、避免中间结果或提前结束处理中真正受益。

参考资料

相关推荐
AI刀刀19 小时前
deepseek 内容粘贴后符号丢失怎么办?AI 导出鸭实测解决排版乱码问题
android·人工智能·excel·ai导出鸭
东方佑19 小时前
Per-Group 混合精度量化:将 14B 视频生成模型压缩至 11 GB
android
三少爷的鞋20 小时前
Android 面试系列 : 协程为何比线程高效
android
2501_9327502620 小时前
Android 数据持久化解析
android·java
石山代码1 天前
KMP全栈开发:从Android到AI Agent的技术演进与实践
android·人工智能
怣疯knight1 天前
kotlin安卓应用打包编译卡死的可能原因
android·kotlin
plainGeekDev1 天前
NullPointerException → Kotlin 空安全
android·java·kotlin
2501_916008892 天前
苹果上架工具怎么选 不用 Mac 上架 App Store 的几种方案
android·macos·ios·小程序·uni-app·iphone·webview
jijihusong0062 天前
KMP全栈开发教程:从Android到AI Agent
android·人工智能