Kotlin进阶篇:Lambda编程3,lambda在序列中的使

2020-07-23  本文已影响0人  小院里栽棵树

前言

上篇文章我们说了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)  //此处传递了一个新的集合,用来存储变换的元素
}

上面的代码,listfilter()的时候先遍历了一遍,把>=30的人员筛选出来,放在了一个新的集合中,然后用新的集合作为数据源,接着走map()操作,得到年龄>=30人员的姓名集合。我们原本目的,只是要获取年龄>=30的人员姓名集合,但却生成了2个集合,中间生成的集合对于我们来说,显然是没有任何意义的,反而增加了我们的内存消耗。这种及早操作如果数据量比较小的话,倒也无妨,但如果数据量成千上万的话,就会很低效了。

序列之所以不会生成中间集合,是因为序列惰性操作

image
中间操作始终是惰性的,末端操作触发执行所有的延期操作。
  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

我们可以看到使用序列要比使用集合操作次数要少,当数据量很大的时候,这个优势也就很明显了。

总结

序列是惰性操作,允许你合并一个集合上的多次操作,且不需要创建新的集合来保存中间结果。
在数据量大,且有连续操作时,建议使用序列而非集合.

上一篇 下一篇

猜你喜欢

热点阅读