kotlin提供了两种使用集合的方式,Collections和Sequences(集合与序列),下面讲下两者区别及如何使用。
集合 vs 序列
即时求值和延迟求值的区别在于每次transform执行的时候。
集合是每次transform执行时立即计算的,并且计算结果会存储在一个新的集合里面。集合的每个transform是内联方法,例如集合的map方法:
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}
可以看到有inline标记,且会创建一个新的ArrayList。
而序列是延迟计算的,分为两种操作:中间转换及最终操作。中间转换不会立即执行,而是会存储操作本身,只有当最终操作执行时,才会按序列在每个item上执行中间操作,然后调用最终操作。中间操作(如map,distinct,groupBy等等)返回另一个序列而终端操作(如first,toList,count等等)没有。
序列不持有对集合项的引用,它们是基于原始集合的迭代器创建的,并保留对所有需要执行的中间操作的引用。
与集合上的转换不同,序列的中间转换不是内联函数 - 不能存储内联函数,序列需要存储这些转换操作。例如序列上的map中间操作,我们可以看到转换函数保存在一个新的实例中Sequence:
public fun <T,R> Sequence <T> .map(transform:(T) - > R):Sequence <R> {
return TransformingSequence(this,transform)
}
终端操作,例如first,遍历序列的元素,直到匹配预置条件。
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(在map上面使用),我们将看到当在序列迭代器上调用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())
}
…
}
无论是使用集合或者序列,kotlin标准库都提供了非常多的操作,在使用前,请先了解他们 https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/#functions。
集合与序列的使用
假设我们有一个不同形状的对象列表,我们想让他们变成黄色,然后取第一个方形。
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 = listOf(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)
}
我们看看每个操作如何以及何时应用于集合以及何时应用于序列集合。
集合
-
map被调用 - 创建一个新的ArrayList,遍历初始集合的所有项目,通过复制原始对象并更改颜色来转换它,然后将其添加到新列表中。
-
first 被调用 - 遍历每个项目,直到找到第一个方块
序列
-
asSequence - 根据原始集合的迭代器创建序列
-
map被调用 - 转换被添加到序列需要执行的操作列表中但不执行操作
-
first被调用 - 这是一个终端操作,因此,在集合的每个元素上触发所有中间操作。遍历初始集合进行迭代,应用map,然后first。由于第二个元素满足first条件,因此我们不再将该映射应用于集合的其余部分。
使用序列时,不会创建中间集合,并且由于逐项计算项目,因此仅对部分输入执行map。
性能
转换顺序
无论是使用集合还是序列,转换的顺序都很重要。在上面的例子中,first不应该在map后面执行,因为它不是map转换的结果。如果我们颠倒业务逻辑的顺序并首先在集合上调用first然后转换结果,那么我们只创建一个新对象 - 黄色方块。如果使用序列 - 我们避免创建2个新对象,如果使用集合,我们避免创建整个新列表。
由于最终操作可以提前完成处理,并且中间操是延迟执行,因此在某些情况下,与集合相比序列可以避免进行不必要的工作。
内联和大数据集的结果
集合操作使用内联函数,因此operation的字节码以及传递给它的lambda的字节码将被内联。序列不使用内联函数,因此,Function为每个操作创建新对象。 另一方面,集合为每个转换创建一个新列表,而序列只保留对转换函数的引用。
所以当使用1-2个操作符处理小型集合时,这些差异没有太大影响,因此使用集合没问题。但是,在处理大型列表时,中间集合的创建代价可能会变得昂贵; 在这种情况下,使用序列。
不幸的是,我不知道任何基准测试研究可以帮助我们更好地理解集合与序列的性能如何受到不同大小的集合或操作链的影响。
如有翻译或者理解错误请多指教!