
各位 Kotliner,周一好!今天我们研究一下 Kotlin Iterator 的相关技术。
在 Kotlin 中,我们经常会使用 for、map、filter、take 这些 API 处理集合。
这些 API 可以说是日常开发中使用频率最高的工具之一,但坦白讲,很多开发者可能没有真的理解它们背后的机制。
你有没有想过:
for循环为什么能够遍历List?Sequence为什么可以做到惰性执行?Sequence真的比普通集合操作更高效吗?什么情况下反而会更慢?- 为什么遍历集合时不能直接删除元素?
要理解这些问题,需要先从 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() 则返回下一个元素,同时推进迭代器内部的遍历状态。
虽然接口中只有两个函数,但正是这份简单的约定,让 List、Set,甚至自定义的数据结构,都可以使用统一的方式进行顺序遍历。
Iterator 的核心能力
Iterator 接口主要围绕两个相互配合的函数展开:hasNext() 和 next()。
-
hasNext(): BooleanhasNext()用于检查当前迭代器是否还有下一个元素。你可以把它理解成在询问:"遍历已经结束了吗?后面还有没有元素?"
如果仍然可以继续遍历,它会返回
true;当所有元素都已经访问完毕时,它会返回false。需要注意的是,调用
hasNext()不会把调用方可见的遍历位置移动到下一个元素。它只是告诉我们当前是否还能继续调用next()。 -
next(): Tnext()会返回迭代过程中的下一个元素,并推进迭代器内部的状态,使下一次调用可以继续访问后面的元素。它是我们从迭代器中真正取出数据的主要方式。
为了避免错误,只有在确定仍然存在下一个元素时,才应该调用 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 主要有下面两个特点。
-
继承自
Iterator因为
MutableIterator继承了Iterator<T>,所以它可以完成普通迭代器能够完成的全部工作。我们仍然可以在
while循环中调用hasNext(),并通过next()获取元素。 -
增加了
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 标准库中,Iterable 和 Sequence 都可以用来处理一系列元素。
List、Set 等集合都实现了 Iterable。从 API 表面来看,Iterable 和 Sequence 提供的很多操作都非常相似,例如 map、filter 和 flatMap,甚至连调用方式都几乎一样。
不过,它们底层的执行模型存在明显区别。
对于 Iterable,常见的多步集合转换会采用立即执行的方式:先对整个集合完成当前操作,并产生中间结果,然后再对中间结果执行下一步操作。
而 Sequence 采用的是惰性执行。它会根据需要逐个处理元素,只有下游真正请求结果时,相关计算才会发生。
整个 Sequence 机制建立在一个非常简单的约定之上:iterator() 操作符函数。
kotlin
public interface Sequence<out T> {
/**
* 返回一个可以依次产生该序列元素的 Iterator。
*
* 如果该 Sequence 被限制为只能遍历一次,
* 第二次调用 iterator() 时会抛出异常。
*/
public operator fun iterator(): Iterator<T>
}
与已经将元素保存在内存中的 List、Set 等集合不同,Sequence 更像是对"如何产生一系列元素"以及"这些元素需要经过哪些处理"的描述。
它最核心的承诺只有一个:当外部请求时,它能够提供一个 Iterator。
真正驱动整个序列运行的,就是这个 Iterator。它知道如何产生或读取下一个元素,但通常只有在外部调用 next() 时,才会真正去获取和处理这个元素。
这种由下游不断请求上游提供数据的方式,也可以称为一种 pull-based,也就是拉取式模型。
Sequence 的惰性正是由此而来。
与普通集合会立即执行转换不同,Sequence 中的 map、filter、flatMap 等中间操作不会立即处理所有元素。
它们会推迟计算,直到 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
map、filter 和 take 等转换函数同样可以用于 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]
如果继续查看 Sequence 中 map、filter 等中间操作的实现,就会发现它的设计非常巧妙。
调用这些操作时,它们不会立即遍历原始序列,而是创建并返回一个新的 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 函数,相当于记录了一份"之后应该如何转换数据"的说明。
filter、take 以及其他中间操作也遵循相似的思路。每调用一次中间操作,处理链中就会增加一层新的包装。
不过,不同操作的迭代器实现并不完全一样。
例如,map 的 hasNext() 基本可以直接委托给上游;而 filter 为了判断是否存在"下一个符合条件的元素",它的 hasNext() 可能需要继续向上游读取若干元素,并把找到的结果暂时缓存起来。
虽然具体实现不同,但它们遵循同一套核心思路:不提前完成整个集合的计算,而是在下游请求元素时,按需完成当前元素所需要的工作。
终止操作

在终止操作被调用之前,整个 Sequence 处理链都处于尚未真正执行的状态。
终止操作会消费序列中的元素,并产生一个最终结果,例如:
toList()forEach()count()first()
终止操作会调用处理链最后一个 Sequence 的 iterator()。
最后一个 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()。
它的执行过程大致如下。
toList()请求take(2)对应的迭代器,并准备一个列表用于收集最终结果。take(2)的迭代器向map迭代器请求第一个元素。map迭代器继续向filter迭代器请求元素。filter迭代器开始从原始数据源(1..10)中拉取数据。- 取出
1,输出Filtering 1。它不满足偶数条件,因此继续查找。 - 取出
2,输出Filtering 2。它满足条件,于是filter返回2。
- 取出
map收到2,输出Mapping 2,执行转换2 * 2 = 4,然后返回4。take(2)将4交给下游的toList()。toList()把它加入结果列表,此时take还允许再返回一个元素。toList()再次请求下一个元素,take(2)继续向map请求第二个结果。- 上面的过程再次发生:
filter取出3,输出Filtering 3,条件不成立。filter取出4,输出Filtering 4,条件成立,于是返回4。
map收到4,输出Mapping 4,将它转换为8并返回。take(2)将8交给toList()。toList()把它加入结果列表,此时take(2)已经返回了两个元素。- 当下游再次检查是否还有元素时,
take(2)返回false。toList()随即结束,原始数据源中只有1到4被实际处理。
最终输出如下:
text
Filtering 1
Filtering 2
Mapping 2
Filtering 3
Filtering 4
Mapping 4
Result: [4, 8]
这个例子展示了 Sequence 的几个核心特点。
-
惰性执行
在终止操作被调用之前,中间操作不会真正开始处理元素。
-
逐元素处理
一个元素会先经过
filter,再经过map,最后到达take。处理完当前元素后,才会继续拉取后面的元素。对于这里使用的
filter、map和take,处理过程中不需要像普通集合的多步转换那样,为每一步创建完整的中间集合,因此通常能够减少额外的内存占用。 -
短路能力
take()、first()等操作在获得所需结果后,可以让整条处理链提前结束,不必继续处理数据源中剩余的元素。
还需要注意一点:惰性执行并不意味着所有 Sequence 操作都不需要额外状态。
map、filter 这类操作可以逐个处理元素;但 distinct 需要记录已经出现过的元素,排序操作通常也需要先收集数据。因此,Sequence 避免的是许多不必要的中间集合,并不是所有场景都只占用常量级内存。
小结
Sequence 的内部机制,是惰性求值在 Kotlin 标准库中的一个典型实现,而它的基础仍然是那个非常简单的 Iterator 接口。
map、filter 等中间操作不会立即执行。它们会创建一个新的 Sequence,包装前一个序列,并把需要执行的操作保存下来,从而逐步构建出完整的计算流程。
只有当 toList()、count()、first() 等终止操作被调用时,最终的 Iterator 链才会开始工作。元素会从数据源中被逐个拉取,并依次通过整条处理管道。
这种设计可以避免许多中间集合,并且能够配合 take()、first() 等操作提前结束计算,因此特别适合下面这些场景:
- 数据量比较大;
- 存在较长的多步转换链;
- 只需要前几个符合条件的结果;
- 需要处理按需生成的数据,甚至无限序列。
不过,Sequence 并不等于无条件地"性能更高"。
惰性处理需要额外的 Sequence 和 Iterator 包装,也会增加间接函数调用。对于数据量很小、操作非常简单的集合,直接使用普通集合操作可能反而更加直接,甚至更快。
因此,更准确的理解不是"看到集合操作就调用 asSequence()",而是先判断当前任务能否从惰性计算、避免中间结果或提前结束处理中真正受益。