前言
上篇文章我们说了lambda
在集合中的使用,我们不仅学习了集合的函数式API,也了解到了在链式调用集合函数式API时,带来的额外内存开销,那么这篇文章我们就来看下如何减少这种内存消耗。
序列
为什么说序列在链式调用函数式API时,要比集合内存消耗更小?我们拿list
来举例
companion object {
private val list = listOf(Person("张三", 20), Person("李四", 30), Person("王五", 32))
@JvmStatic
fun main(args: Array<String>) {
val map = list.filter { it.age >= 30 }.map { it.name }
}
}
class Person(val name: String, val age: Int)
//源码
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
return filterTo(ArrayList<T>(), predicate) //此处传递了一个新的集合,用来存储满足条件的元素
}
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform) //此处传递了一个新的集合,用来存储变换的元素
}
上面的代码,list
在filter()
的时候先遍历了一遍,把>=30
的人员筛选出来,放在了一个新的集合中,然后用新的集合作为数据源,接着走map()
操作,得到年龄>=30人员的姓名集合。我们原本目的,只是要获取年龄>=30的人员姓名集合,但却生成了2个集合,中间生成的集合对于我们来说,显然是没有任何意义的,反而增加了我们的内存消耗。这种及早操作
如果数据量比较小的话,倒也无妨,但如果数据量成千上万的话,就会很低效了。
序列
之所以不会生成中间集合,是因为序列
是惰性操作
中间操作始终是惰性的,末端操作触发执行所有的延期操作。
companion object {
private val list = listOf(Person("张三", 20), Person("李四", 30), Person("王五", 32))
@JvmStatic
fun main(args: Array<String>) {
list.asSequence().filter {
print("filter: $it")
it.age >= 30
}.map {
print("map: $it")
it.name
}
}
}
class Person(val name: String, val age: Int)
上面代码,我们只有中间操作,没有末端操作,所以执行代码后,控制台并没有任何内容输出,这意味着我们的中间操作fliter()
map()
都被延期了。我们随便点击一个中间操作map()
,看下源码,为什么被延期了。
public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
return TransformingSequence(this, transform)
}
internal class TransformingSequence<T, R>
constructor(private val sequence: Sequence<T>, private val transformer: (T) -> R) : Sequence<R> {
override fun iterator(): Iterator<R> = object : Iterator<R> {
val iterator = sequence.iterator()
override fun next(): R {
return transformer(iterator.next())
}
override fun hasNext(): Boolean {
return iterator.hasNext()
}
}
internal fun <E> flatten(iterator: (R) -> Iterator<E>): Sequence<E> {
return FlatteningSequence<T, R, E>(sequence, transformer, iterator)
}
}
通过源码我们发现,序列
在调用map()
的时候,只是返回了一个序列,其余什么操作都没有,所以它是惰性的。相关的变化操作放在了迭代器的next()
方法中,看到这,我们可能有点疑惑,中间操作只返回了一个序列,元素的操作都放在了迭代器中,那序列
是在何时何地调用了这个迭代方法呢?前面我们说到中间操作都是惰性的被延期的,末端操作执行所有的延期计算,所以你们可能已经猜到了,迭代方法是在末端操作中执行的,我们随便写个toList()
的末端操作,然后结合源码看一下。
list.asSequence().filter {
print("filter: $it")
it.age >= 30
}.map {
print("map: $it")
it.name
}.toList()
//以下是源码
public fun <T> Sequence<T>.toList(): List<T> {
return this.toMutableList().optimizeReadOnlyList()
}
public fun <T> Sequence<T>.toMutableList(): MutableList<T> {
return toCollection(ArrayList<T>())
}
public fun <T, C : MutableCollection<in T>> Sequence<T>.toCollection(destination: C): C {
for (item in this) {
destination.add(item)
}
return destination
}
通过源码,我们看到toList()
内部调用了toMutableList()
方法,toMutableList()
又调用了toCollection()
,而在toCollection()
中,遍历了序列把元素存储在了一个新的集合中,在遍历时,使用了in运算符
,会调用迭代器的next()
方法,进行元素操作,到这,我们发现元素的操作才开始执行,也就捋明白了为什么中间操作是惰性的,为什么所有的操作都是在末端操作中开始执行的了。
通过上述源码,我们不难发现,序列
对元素的操作是在末端操作的循环中进行的,而且是所有操作按顺序应用在每一个元素上,拿我们demo来说,就是先对第一个元素进行了filter()
操作,接着进行map()
操作,完成了一套完整操作后,再开始第二个元素的处理,以此类推。但集合
是先对所有元素进行了filter()
操作后,再进行map()
操作。
这意味着如果我们用序列
对数据进行处理时,部分元素不会发生任何变换,因为在遍历到他们之前,我们就已经取到了结果,
private val list = listOf(1, 2, 3)
@JvmStatic
fun main(args: Array<String>) {
list.asSequence().map {
println("map :$it")
it * 2
}.find { it > 2 }
}
输出结果:
map :1
map :2
private val list = listOf(1, 2, 3)
@JvmStatic
fun main(args: Array<String>) {
list.map {
println("map :$it")
it * 2
}.find { it > 2 }
}
输出结果:
map :1
map :2
map :3
我们可以看到使用序列
要比使用集合
操作次数要少,当数据量很大的时候,这个优势也就很明显了。
总结
序列
是惰性操作,允许你合并一个集合上的多次操作,且不需要创建新的集合来保存中间结果。
在数据量大,且有连续操作时,建议使用序列
而非集合
.
网友评论