前言
在我们平时的开发中,会或多或少的使用集合,在Kotlin的标准库中, 提供了基于不同执行方式的两种集合类型
- Collection类型 立即执行(eagerly)
- Sequence类型 延迟执行 (lazily) 的 Sequence 类型
两者的区别
执行方式的区别-理论分析
Collection操作会立即执行,执行结果会存储在一个新的集合中并返回
Sequence操作会分为两步: 中间操作(intermediate)和终结操作(terminal),中间操作不会立即执行, 它们只是被储存起来,仅当终结操作被调用时, 才会按照序列顺序在每个元素上进行中间操作。然后执行终结操作,例如: 中间操作(比如 map, distinct, groupBy等) 会返回另一个Sequence,而终结操作(比如 first, toList, count)则不会。
Collection的转换操作是内联函数,例如, map 的实现方式,可以看到它是一个创建了新 ArrayList 的内联函数:
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}
Sequence执行的中间转换不是内联函数,因为内联函数无法存储,Sequence需要存储中间结果
例如map的实现方式:
public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R>{
return TransformingSequence(this, transform)
}
TransformingSequence代码实现,在next中进行转换并存储
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())
}
…
}
例如 first 这样的终结操作,会对 Sequence 中的元素进行遍历,直到预置条件匹配为止
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.”)
}
执行方式的区别-实战验证
Collection的操作
data class Shape(val edges: Int, val color: Int, val angle: Int = 0)
// 定义4个形状
val circle = Shape(0, 1)
val square = Shape(4, 2)
val rhombus = Shape(4, 3, 45)
val triangle = Shape(3, 4)
val shapes = listOf(circle, square, rhombus, triangle)
fun main() {
val yellowShapes = shapes.map {
println("操作1: $it")
it.copy(color = 3)
}.first {
println("操作2: $it")
it.edges == 4
}操作结果
Collection执行过程解析
- 调用 map 时 —— 一个新的 ArrayList 会被创建。然后遍历了初始 Collection 中所有项目,复制原始的对象,然后更改它的颜色,再将其添加到新的列表中
- 调用 first 时 —— 遍历每一个项目,直到找到第一个正方形。
先将所有元素都copy操作后,然后根据这个copy后返回的新列表进行遍历查找,找到第一个正方形
data class Shape(val edges: Int, val color: Int, val angle: Int = 0)
// 定义4个形状
val circle = Shape(0, 1)
val square = Shape(4, 2)
val rhombus = Shape(4, 3, 45)
val triangle = Shape(3, 4)
val shapes = listOf(circle, square, rhombus, triangle)
fun main() {
val yellowShapesSequence = shapes.asSequence().map {
println("中间操作: $it")
it.copy(color = 3)
}.first {
println("终结操作: $it")
it.edges == 4
}
println(yellowShapesSequence)
操作日志
Sequence执行过程解析
- asSequence —— 基于原始集合的迭代器创建一个 Sequence;
- 调用 map 时 —— Sequence 会将转换操作的信息存储到一个列表中,该列表只会存储要执行的操作,并不会执行这些操作;
- 调用 first 时 —— 这是一个终结操作,所以会将中间操作作用到集合中的每个元素。我们遍历初始集合,对每个元素执行 map 操作,然后继续执行 first 操作,当遍历到第二个元素时,发现它符合我们的要求,所以就无需在剩余的元素中进行 map 操作了。
遍历初始集合,对每个元素执行 map 操作,然后继续执行 first 操作,当遍历到第二个元素时,发现它符合我们的要求,所以就无需在剩余的元素中进行 map 操作了。
总结
Collection 的操作使用了内联函数,所以处理所用到的字节码以及传递给它的 lambda 字节码都会进行内联操作。而 Sequence 不使用内联函数,因此,它会为每个操作创建新的 Function 对象。
Collection 会为每个转换操作创建一个新的列表,而 Sequence 仅仅是保留对转换函数的引用。
数据量小的 Collection 执行 1 到 2 个操作时,上面所说的差异并不会带来什么样的影响,所以这种情况下使用 Collection 是没问题的。而当列表数据很大时,中间集合的创建会很消耗资源,这种情况下就应该使用 Sequence。
综上所述
- Collection 会立即执行对数据的操作,而 Sequence 则是延迟执行。根据要处理的数据量大小,选择最合适的一个: 数据量小,则使用 Collection,数据量大,则使用 Sequence
- 不管是使用 Collection 还是 Sequence 调用的顺序也是很关键的, 如果将 Collection那个操作的first提前到map之前,因为first不需要map的结果就可以就可以执行, 这样 Collection 操作就只会创建一个新的对象, 不必创建整个列表。 Sequence 则少创建两个对象(Shapes集合最后的那两个形状)。在某些情况下,调用顺序很关键。 毕竟减少操作次数也是提升性能的关键。
参考链接
https://mp.weixin.qq.com/s/OaO1Yg6emXanfGSfnvfkIQ