Kotlin集合之Collection和Sequence

619 阅读4分钟

前言

在我们平时的开发中,会或多或少的使用集合,在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)
}

github.com/JetBrains/k…

Sequence执行的中间转换不是内联函数,因为内联函数无法存储,Sequence需要存储中间结果

例如map的实现方式:

public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R>{      
   return TransformingSequence(this, transform)
}

github.com/JetBrains/k…

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.”)
}

github.com/JetBrains/k…

执行方式的区别-实战验证

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执行过程解析

  1. 调用 map 时 —— 一个新的 ArrayList 会被创建。然后遍历了初始 Collection 中所有项目,复制原始的对象,然后更改它的颜色,再将其添加到新的列表中
  2. 调用 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执行过程解析

  1. asSequence —— 基于原始集合的迭代器创建一个 Sequence;
  2. 调用 map 时 —— Sequence 会将转换操作的信息存储到一个列表中,该列表只会存储要执行的操作,并不会执行这些操作;
  3. 调用 first 时 —— 这是一个终结操作,所以会将中间操作作用到集合中的每个元素。我们遍历初始集合,对每个元素执行 map 操作,然后继续执行 first 操作,当遍历到第二个元素时,发现它符合我们的要求,所以就无需在剩余的元素中进行 map 操作了。

遍历初始集合,对每个元素执行 map 操作,然后继续执行 first 操作,当遍历到第二个元素时,发现它符合我们的要求,所以就无需在剩余的元素中进行 map 操作了。


总结

Collection 的操作使用了内联函数,所以处理所用到的字节码以及传递给它的 lambda 字节码都会进行内联操作。而 Sequence 不使用内联函数,因此,它会为每个操作创建新的 Function 对象。

Collection 会为每个转换操作创建一个新的列表,而 Sequence 仅仅是保留对转换函数的引用。

数据量小的 Collection 执行 1 到 2 个操作时,上面所说的差异并不会带来什么样的影响,所以这种情况下使用 Collection 是没问题的。而当列表数据很大时,中间集合的创建会很消耗资源,这种情况下就应该使用 Sequence。

综上所述

  1. Collection 会立即执行对数据的操作,而 Sequence 则是延迟执行。根据要处理的数据量大小,选择最合适的一个: 数据量小,则使用 Collection,数据量大,则使用 Sequence
  2. 不管是使用 Collection 还是 Sequence 调用的顺序也是很关键的, 如果将 Collection那个操作的first提前到map之前,因为first不需要map的结果就可以就可以执行, 这样 Collection 操作就只会创建一个新的对象, 不必创建整个列表。 Sequence 则少创建两个对象(Shapes集合最后的那两个形状)。在某些情况下,调用顺序很关键。 毕竟减少操作次数也是提升性能的关键。



参考链接

https://mp.weixin.qq.com/s/OaO1Yg6emXanfGSfnvfkIQ