在很多场景中我们会使用到集合,Kotlin 标准库 (Kotlin Standard Library) 中提供了非常多出色的关于集合的实用函数。其中,Kotlin 提供了基于不同执行方式的两种集合类型: 立即执行 (eagerly) 的 Collection 类型,延迟执行 (lazily) 的 Sequence 类型。本篇文章将向您介绍两者的区别,并向您介绍这两种类型分别该在哪种情况下使用,以及它们的性能表现。
Collection 和 Sequence 的对比
立即执行和延迟执行的区别在于每次对集合进行转换时,这个操作会在何时真正执行。
Collection(也称集合) 是在每次操作时立即执行的,执行结果会存储到一个新的集合中。作用于 Collection 的转换操作是内联函数。例如,map 的实现方式,可以看到它是一个创建了新 ArrayList 的内联函数:
代码语言:javascript复制public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}
复制代码
Sequence (也称序列) 是延迟执行的,它有两种类型: 中间操作 (intermediate) 和末端操作(terminal)。中间操作不会立即执行,它们只是被存储起来,仅当末端操作被调用时,才会按照顺序在每个元素上执行中间操作,然后执行末端操作。中间操作 (比如 map、distinct、groupBy 等) 会返回另一个Sequence,而末端操作 (比如 first、toList、count 等) 则不会。
Sequence 是不会保留对集合项目的引用的。它基于原始集合的迭代器 (iterator) 创建,并且保留要执行的所有中间操作的引用。
与在 Collection 中执行转换操作不同,Sequence 执行的中间转换不是内联函数,因为内联函数无法存储,而 Sequence 需要存储它们。我们可以通过下列代码看到像 map 这样的中间操作是如何实现的,可以看到转换函数会存储在一个新的 Sequence 实例中:
代码语言:javascript复制public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R>{
return TransformingSequence(this, transform)
}
复制代码
例如 first 这样的末端操作,会对 Sequence 中的元素进行遍历,直到预置条件匹配为止。
代码语言:javascript复制public inline fun <T> Sequence<T>.first(predicate: (T) -> Boolean): T {
for (element in this) if (predicate(element)) return element
throw NoSuchElementException(“Sequence contains no element matching the predicate.”)
}
复制代码
如果观察 TransformingSequence 这样的类型是如何实现的,我们会发现在迭代器上调用 next 时,转换存储操作也一并被应用。
代码语言:javascript复制internal class TransformingIndexedSequence<T, R>
constructor(private val sequence: Sequence<T>, private val transformer: (Int, T) -> R) : Sequence<R> {
override fun iterator(): Iterator<R> = object : Iterator<R> {
…
override fun next(): R {
return transformer(checkIndexOverflow(index ), iterator.next())
}
…
}
复制代码
无论您使用 Collection 还是 Sequence,Kotlin 标准库都提供了类似于 find、filter、groupBy 等一系列操作,在使用它们之前,您得确保了解这些操作。
Collection 和 Sequence 如何选择
假设我们有一个列表,存储了许多不同形状的对象,我们希望将列表中形状的颜色变成黄色,然后获取列表中的第一个正方形。
运行 Collection 和 Sequence 代码 我们来看一下针对 Collection 和 Sequence 的各个操作是如何执行以及何时执行的。
Collections
- 调用 map 时 —— 一个新的 ArrayList 会被创建。我们遍历了初始 Collection 中所有项目,复制原始的对象,然后更改它的颜色,再将其添加到新的列表中;
- 调用 first 时 —— 遍历每一个项目,直到找到第一个正方形。
Sequences
- asSequence —— 基于原始集合的迭代器创建一个 Sequence;
- 调用 map 时 —— Sequence 会将转换操作的信息存储到一个列表中,该列表只会存储要执行的操作,并不会执行这些操作;
- 调用 first 时 —— 这是一个末端操作,所以会将中间操作作用到集合中的每个元素。我们遍历初始集合,对每个元素执行 map 操作,然后继续执行 first 操作,当遍历到第二个元素时,发现它符合我们的要求,所以就无需在剩余的元素中进行 map 操作了。
使用 Sequence 时不会去创建中间集合,由于项目会被逐个执行,map 操作只会作用到部分输入上。
性能
转换的顺序
无论您使用 Collection 还是 Sequence,转换的顺序都很重要。在上面的例子中,first 不需要先在 map 之后进行操作,因为 first 不需要 map 操作的结果就能够执行。如果我们颠倒业务逻辑的顺序,先把 first 作用到 Collection 上,再对结果执行转换,那么我们只会创建一个新的对象 —— 一个黄色的正方形。当使用 Sequence 时,会避免创建两个新对象,而当使用 Collection 时则会避免创建整个列表。
转换顺序的重要性 — 避免无用操作 因为末端操作可以提前对任务进行处理,而中间操作会延迟进行处理,所以在某些情况下,相比于 Collection,Sequence 可以避免一些无用操作。使用时,请确保检查了转换顺序以及它们的依赖关系。
内联和大数据集所带来的影响
Collection 的操作使用了内联函数,所以处理所用到的字节码以及传递给它的 lambda 字节码都会进行内联操作。而 Sequence 不使用内联函数,因此,它会为每个操作创建新的 Function 对象。
另外,Collection 会为每个转换操作创建一个新的列表,而 Sequence 仅仅是保留对转换函数的引用。
当对数据量小的 Collection 执行 1 到 2 个操作时,上面所说的差异并不会带来什么样的影响,所以这种情况下使用Collection 是没问题的。而当列表数据很大时,中间集合的创建会很消耗资源,这种情况下就应该使用 Sequence。
不幸的是,我不知道有什么样的基准测试能够帮助我们更好地探索出具体不同大小的集合或者操作链才会对 Collection 和 Sequence 产生影响。
综上所述,Collection 会立即执行对数据的操作,而 Sequence 则是延迟执行。根据要处理的数据量大小,选择最合适的一个: 数据量小,则使用 Collection,数据量大,则使用 Sequence,另外,需注意操作顺序带来的影响。