使用集合完成工作是十分常见的任务,kotlin标准库也提供了很多易用的工具集合。它提供了两种工作集合:饥饿型集合和懒惰型序列。继续阅读查看两者差别,应该如何使用、什么时候使用以及两者隐藏性能开销。
Collections vs sequences
饥饿型集合和懒惰型序列主要差别在于何时执行转换。
Collections是饥饿的——每个操作都是在调用时执行,操作结果保存在一个新集合中。集合转换为内联函数。比如,查看map是如何实现地,我们可以看到它是一个内联函数,内部创建了一个新ArrayList:
public inline fun Iterable.map(transform: (T) -> R): List {
return mapTo(ArrayList(collectionSizeOrDefault(10)), transform)
}
序列是懒惰型。它们有两种类型操作:中间操作和末端操作。中间操作不会被立即执行;它们只会被暂存。只有当末端操作被调用时,中间操作才会对连续对每一个元素触发,最终应用末端操作。中间操作会(比如 map、distinct、groupBy 等等)返回另一个序列,然而末端操作(比如 first、toList、count 等等)并不会。
序列并不会保持持有对集合中items的引用。它们会基于原始集合迭代器被创建,持有对所有需要执行的中间操作的引用。
和集合转换不同,序列的中间操作并不是内联函数——内联函数不能被保存而序列需要保存。查看中间操作是如何实现的,比如map,我们可以看见转换函数会被保存在序列实例中。
public fun Sequence.map(transform: (T) -> R): Sequence{
return TransformingSequence(this, transform)
}
末端操作,比如first,会迭代遍历序列元素,直到某个元素满足了判断表达式。
public inline fun Sequence.first(predicate: (T) -> Boolean): T {
for (element in this) if (predicate(element)) return element
throw NoSuchElementException(“Sequence contains no element matching the predicate.”)
}
如果我们探究序列比如TransformingSequence(在上面map中使用到了)如何实现,我们会发现在序列迭代器调用next时就会应用所存储的转换。
internal class TransformingIndexedSequence
constructor(private val sequence: Sequence, private val transformer: (Int, T) -> R) : Sequence {
override fun iterator(): Iterator = object : Iterator {
…
override fun next(): R {
return transformer(checkIndexOverflow(index++), iterator.next())
}
…
}
无论你使用集合还是序列,kotlin标准库提供了两者的广泛操作,比如find,filter,groupBy和其他。
Collections and sequences
我们有一个列表,里面有不同的形状。我们需要一个黄色的然后第一个是方形的形状。
data class Shape(val edges:Int, val angle : Int=0, val color:Int)
val circle = Shape(edges=0,color=1)
val square = Shape(edges=4,color=2)
val rhombus = Shape(edges=4,angle=45,color=3)
val triangle = Shape(edges=3,color=4)
val shapes = list(circle,square,rhombus,triangle)
fun main() {
println(shapes.map {it.color}.toList())
val yellowSquareSequence = shapes.asSequence().map {
it.copy(color = 3)
}.first {
it.edges == 4
}
println(yellowSquareSequence)
val yellowSquareCollection = shapes.map {
it.copy(color = 3)
}.first {
it.edges == 4
}
println(yellowSquareCollection)
}
让我们看看每个操作是如何、何时应用到每一个集合或序列的。
Collections
- map被调用——一个新ArrayList被创建。我们迭代初始集合的每一个item,通过拷贝原始对象并且改变颜色来实现转换,之后将其添加到新集合。
- first被调用——我们迭代遍历每一个item直到找到第一个方形。
Sequences
- asSequence —— 基于原始集合迭代器创建序列
- 调用map —— 由sequence将转换被添加到需要执行的操作列表中,但是操作不会执行。
- 调用first —— 这是一个末端操作,所以会对于集合中每个元素触发所有中间操作。我们迭代遍历初始集合应用map并且获取其中的第一项。因此集合的第一个元素以及满足了条件,那么就不再需要对集合的剩下元素执行map操作。
当我们使用没有中间操作集合的序列,之后items会一个个进行评估,map操作也只会对一部分输入执行。
Performance
Order of transformations
无论你使用collections或者sequences,总是需要注意转换顺序。在上述例子中,first并不需要在map之后执行,因为它不是map转换的结果。如果我们反转业务逻辑顺序,首先对collection调用first,如何转换结果,那么我们就只创建一个新对象 —— 黄色方块。当我们使用sequence —— 我们就避免创建两个新对象,当我们使用集合,我们就避免创建整个新list。
因为末端操作可以提早结束处理,中间操作是懒惰的,在一些场景中,相比collection,sequence则可以帮助你避免不需要的工作。你要确保总是会检查转换顺序,还有在两者直接的依赖。
Inlining and large data sets consequences
集合操作使用内联函数,传递进lambda的字节码操作会被内联。sequence并不使用内联函数,因此,每次操作中都会创建新函数对象。
另一方面,集合对每一个转换都创建新list,然而,sequence仅会保持对转换函数的引用。
当在数据量小的集合中,有1-2个操作符,就不会有比较大的性能影响。但是如果在数据量较大的集合使用中间操作符,性能开销就会比较大;在这种场景下,则使用序列。
不幸运地是,我没有找到任何banchmarking材料帮助我们更好的理解collection和sequence的性能差异。
Collections是饥饿型的,而sequences是懒惰型的。这取决与你的数据量,选择最合适使用方案:集合 —— 小数据量 或者 序列 —— 大数据量 , 尤其主义转换顺序。
原文地址:Collections and sequences in Kotlin
MarcinMoskala的 kotlin 性能对比 Effictive Kotlin 性能对比