简述: 很久没有发布原创文章,一如既往,今天开始Kotlin浅谈系列的第十讲,一块儿来探索Kotlin中的序列。序列(Sequences)其实是对应Java8中的Stream的翻版。从以前博客能够了解到Kotlin定义了不少操做集合的API,没错这些函数照样适用于序列(Sequences),并且序列操做在性能方面优于集合操做.并且经过以前函数式API的源码中能够看出它们会建立不少中间集合,每一个操做符都会开辟内存来存储中间数据集,然而这些在序列中就不须要。让咱们一块儿来看看这篇博客内容:java
咱们通常在Kotlin中处理数据集都是集合,以及使用集合中一些函数式操做符API,咱们不多去将数据集转换成序列再去作集合操做。这是由于咱们通常接触的数据量级比较小,使用集合和序列没什么差异,让咱们一块儿来看个例子,你就会明白使用序列的意义了。设计模式
//不使用Sequences序列,使用普通的集合操做
fun computeRunTime(action: (() -> Unit)?) {
val startTime = System.currentTimeMillis()
action?.invoke()
println("the code run time is ${System.currentTimeMillis() - startTime}")
}
fun main(args: Array<String>) = computeRunTime {
(0..10000000)
.map { it + 1 }
.filter { it % 2 == 0 }
.count { it < 10 }
.run {
println("by using list way, result is : $this")
}
}
复制代码
运行结果:性能优化
//转化成Sequences序列,使用序列操做
fun computeRunTime(action: (() -> Unit)?) {
val startTime = System.currentTimeMillis()
action?.invoke()
println("the code run time is ${System.currentTimeMillis() - startTime}")
}
fun main(args: Array<String>) = computeRunTime {
(0..10000000)
.asSequence()
.map { it + 1 }
.filter { it % 2 == 0 }
.count { it < 10 }
.run {
println("by using sequences way, result is : $this")
}
}
复制代码
运行结果:ide
经过以上同一个功能实现,使用普通集合操做和转化成序列后再作操做的运行时间差距不只一点点,也就对应着两种实现方式在数据集量级比较大的状况下,性能差别也是很大的。这样应该知道为何咱们须要使用Sequences序列了吧。函数
序列操做又被称之为惰性集合操做,Sequences序列接口强大在于其操做的实现方式。序列中的元素求值都是惰性的,因此能够更加高效使用序列来对数据集中的元素进行链式操做(映射、过滤、变换等),而不须要像普通集合那样,每进行一次数据操做,都必需要开辟新的内存来存储中间结果,而实际上绝大多数的数据集合操做的需求关注点在于最后的结果而不是中间的过程,工具
序列是在Kotlin中操做数据集的另外一种选择,它和Java8中新增的Stream很像,在Java8中咱们能够把一个数据集合转换成Stream,而后再对Stream进行数据操做(映射、过滤、变换等),序列(Sequences)能够说是用于优化集合在一些特殊场景下的工具。可是它不是用来替代集合,准确来讲它起到是一个互补的做用。源码分析
序列操做分为两大类:post
序列的中间操做始终都是惰性的,一次中间操做返回的都是一个序列(Sequences),产生的新序列内部知道如何变换原始序列中的元素。怎样说明序列的中间操做是惰性的呢?一块儿来看个例子:性能
fun main(args: Array<String>) {
(0..6)
.asSequence()
.map {//map返回是Sequence<T>,故它属于中间操做
println("map: $it")
return@map it + 1
}
.filter {//filter返回是Sequence<T>,故它属于中间操做
println("filter: $it")
return@filter it % 2 == 0
}
}
复制代码
运行结果:优化
以上例子只有中间操做没有末端操做,经过运行结果发现map、filter中并无输出任何提示,这也就意味着map和filter的操做被延迟了,它们只有在获取结果的时候(也便是末端操做被调用的时候)才会输出提示。
序列的末端操做会执行原来中间操做的全部延迟计算,欢聚,一次末端操做返回的是一个结果,返回的结果能够是集合、数字、或者从其余对象集合变换获得任意对象。上述例子加上末端操做:
fun main(args: Array<String>) {
(0..6)
.asSequence()
.map {//map返回是Sequence<T>,故它属于中间操做
println("map: $it")
return@map it + 1
}
.filter {//filter返回是Sequence<T>,故它属于中间操做
println("filter: $it")
return@filter it % 2 == 0
}
.count {//count返回是Int,返回的是一个结果,故它属于末端操做
it < 6
}
.run {
println("result is $this");
}
}
复制代码
运行结果
注意:判别是不是中间操做仍是末端操做很简单,只须要看操做符API函数返回值的类型,若是返回的是一个Sequence<T>那么这就是一个中间操做,若是返回的是一个具体的结果类型,好比Int,Boolean,或者其余任意对象,那么它就是一个末端操做
建立序列(Sequences)的方法主要有:
//定义声明
public fun <T> Iterable<T>.asSequence(): Sequence<T> {
return Sequence { this.iterator() }
}
//调用实现
list.asSequence()
复制代码
//定义声明
@kotlin.internal.LowPriorityInOverloadResolution
public fun <T : Any> generateSequence(seed: T?, nextFunction: (T) -> T?): Sequence<T> =
if (seed == null)
EmptySequence
else
GeneratorSequence({ seed }, nextFunction)
//调用实现,seed是序列的起始值,nextFunction迭代函数操做
val naturalNumbers = generateSequence(0) { it + 1 } //使用迭代器生成一个天然数序列
复制代码
//定义声明
public fun <T> Sequence<T>.constrainOnce(): Sequence<T> {
// as? does not work in js
//return this as? ConstrainedOnceSequence<T> ?: ConstrainedOnceSequence(this)
return if (this is ConstrainedOnceSequence<T>) this else ConstrainedOnceSequence(this)
}
//调用实现
val naturalNumbers = generateSequence(0) { it + 1 }
val naturalNumbersOnce = naturalNumbers.constrainOnce()
复制代码
注意:只能迭代一次,若是超出一次则会抛出IllegalStateException("This sequence can be consumed only once.")异常。
关于序列性能对比,主要在如下几个场景下进行对比,经过性能对比你就清楚在什么场景下该使用普通集合操做仍是序列操做。
使用Sequences序列
fun computeRunTime(action: (() -> Unit)?) {
val startTime = System.currentTimeMillis()
action?.invoke()
println("the code run time is ${System.currentTimeMillis() - startTime} ms")
}
fun main(args: Array<String>) = computeRunTime {
(0..10000000)//10000000数据量级
.asSequence()
.map { it + 1 }
.filter { it % 2 == 0 }
.count { it < 100 }
.run {
println("by using sequence result is $this")
}
}
复制代码
运行结果:
不使用Sequences序列
fun computeRunTime(action: (() -> Unit)?) {
val startTime = System.currentTimeMillis()
action?.invoke()
println("the code run time is ${System.currentTimeMillis() - startTime} ms")
}
fun main(args: Array<String>) = computeRunTime {
(0..10000000)//10000000数据量级
.map { it + 1 }
.filter { it % 2 == 0 }
.count { it < 100 }
.run {
println("by using sequence result is $this")
}
}
复制代码
运行结果:
使用Sequences序列
fun computeRunTime(action: (() -> Unit)?) {
val startTime = System.currentTimeMillis()
action?.invoke()
println("the code run time is ${System.currentTimeMillis() - startTime} ms")
}
fun main(args: Array<String>) = computeRunTime {
(0..1000)//1000数据量级
.asSequence()
.map { it + 1 }
.filter { it % 2 == 0 }
.count { it < 100 }
.run {
println("by using sequence result is $this")
}
}
复制代码
运行结果:
不使用Sequences序列
fun computeRunTime(action: (() -> Unit)?) {
val startTime = System.currentTimeMillis()
action?.invoke()
println("the code run time is ${System.currentTimeMillis() - startTime} ms")
}
fun main(args: Array<String>) = computeRunTime {
(0..1000)//1000数据量级
.map { it + 1 }
.filter { it % 2 == 0 }
.count { it < 100 }
.run {
println("by using list result is $this")
}
}
复制代码
运行结果:
经过以上性能对比发现,在数据量级比较大状况下使用Sequences序列性能会比普通数据集合更优;可是在数据量级比较小状况下使用Sequences序列性能反而会比普通数据集合更差。关于选择序列仍是集合,记得前面翻译了一篇国外的博客,里面有详细的阐述。博客地址
看到上面性能的对比,相信此刻的你火烧眉毛想要知道序列(Sequences)内部性能优化的原理吧,那么咱们一块儿来看下序列内部的原理。来个例子
fun main(args: Array<String>){
(0..10)
.asSequence()
.map { it + 1 }
.filter { it % 2 == 0 }
.count { it < 6 }
.run {
println("by using sequence result is $this")
}
}
复制代码
序列操做: 基本原理是惰性求值,也就是说在进行中间操做的时候,是不会产生中间数据结果的,只有等到进行末端操做的时候才会进行求值。也就是上述例子中0~10中的每一个数据元素都是先执行map操做,接着立刻执行filter操做。而后下一个元素也是先执行map操做,接着立刻执行filter操做。然而普通集合是全部元素都完执行map后的数据存起来,而后从存储数据集中又全部的元素执行filter操做存起来的原理。
集合普通操做: 针对每一次操做都会产生新的中间结果,也就是上述例子中的map操做完后会把原始数据集循环遍历一次获得最新的数据集存放在新的集合中,而后进行filter操做,遍历上一次map新集合中数据元素,最后获得最新的数据集又存在一个新的集合中。
//使用序列
fun main(args: Array<String>){
(0..100)
.asSequence()
.map { it + 1 }
.filter { it % 2 == 0 }
.find { it > 3 }
}
//使用普通集合
fun main(args: Array<String>){
(0..100)
.map { it + 1 }
.filter { it % 2 == 0 }
.find { it > 3 }
}
复制代码
经过以上的原理转化图,会发现使用序列会逐个元素进行操做,在进行末端操做find得到结果以前提前去除一些没必要要的操做,以及find找到一个符合条件元素后,后续众多元素操做均可以省去,从而达到优化的目的。而集合普通操做,不管是哪一个元素都得默认通过全部的操做。其实有些操做在得到结果以前是没有必要执行的以及能够在得到结果以前,就能感知该操做是否符合条件,若是不符合条件提早摒弃,避免没必要要操做带来性能的损失。
//使用序列
fun main(args: Array<String>){
(0..100)
.asSequence()
.map { it + 1 }
.filter { it % 2 == 0 }
.find { it > 3 }
}
//使用普通集合
fun main(args: Array<String>){
(0..100)
.map { it + 1 }
.filter { it % 2 == 0 }
.find { it > 3 }
}
复制代码
经过decompile上述例子的源码会发现,普通集合操做会针对每一个操做都会生成一个while循环,而且每次都会建立新的集合保存中间结果。而使用序列则不会,它们内部会不管进行多少中间操做都是共享同一个迭代器中的数据,想知道共享同一个迭代器中的数据的原理吗?请接着看内部源码实现。
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
byte var1 = 0;
Iterable $receiver$iv = (Iterable)(new IntRange(var1, 100));
//建立新的集合存储map后中间结果
Collection destination$iv$iv = (Collection)(new ArrayList(CollectionsKt.collectionSizeOrDefault($receiver$iv, 10)));
Iterator var4 = $receiver$iv.iterator();
int it;
//对应map操做符生成一个while循环
while(var4.hasNext()) {
it = ((IntIterator)var4).nextInt();
Integer var11 = it + 1;
//将map变换的元素加入到新集合中
destination$iv$iv.add(var11);
}
$receiver$iv = (Iterable)((List)destination$iv$iv);
//建立新的集合存储filter后中间结果
destination$iv$iv = (Collection)(new ArrayList());
var4 = $receiver$iv.iterator();//拿到map后新集合中的迭代器
//对应filter操做符生成一个while循环
while(var4.hasNext()) {
Object element$iv$iv = var4.next();
int it = ((Number)element$iv$iv).intValue();
if (it % 2 == 0) {
//将filter过滤的元素加入到新集合中
destination$iv$iv.add(element$iv$iv);
}
}
$receiver$iv = (Iterable)((List)destination$iv$iv);
Iterator var13 = $receiver$iv.iterator();//拿到filter后新集合中的迭代器
//对应find操做符生成一个while循环,最后末端操做只须要遍历filter后新集合中的迭代器,取出符合条件数据便可。
while(var13.hasNext()) {
Object var14 = var13.next();
it = ((Number)var14).intValue();
if (it > 3) {
break;
}
}
}
复制代码
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
byte var1 = 0;
//利用Sequence扩展函数实现了fitler和map中间操做,最后返回一个Sequence对象。
Sequence var7 = SequencesKt.filter(SequencesKt.map(CollectionsKt.asSequence((Iterable)(new IntRange(var1, 100))), (Function1)null.INSTANCE), (Function1)null.INSTANCE);
//取出通过中间操做产生的序列中的迭代器,能够发现进行map、filter中间操做共享了同一个迭代器中数据,每次操做都会产生新的迭代器对象,可是数据是和原来传入迭代器中数据共享,最后进行末端操做的时候只须要遍历这个迭代器中符合条件元素便可。
Iterator var3 = var7.iterator();
//对应find操做符生成一个while循环,最后末端操做只须要遍历filter后新集合中的迭代器,取出符合条件数据便可。
while(var3.hasNext()) {
Object var4 = var3.next();
int it = ((Number)var4).intValue();
if (it > 3) {
break;
}
}
}
复制代码
SequencesKt.filter(SequencesKt.map(CollectionsKt.asSequence((Iterable)(new IntRange(var1, 100))), (Function1)null.INSTANCE), (Function1)null.INSTANCE);
复制代码
//第一部分
val collectionSequence = CollectionsKt.asSequence((Iterable)(new IntRange(var1, 100)))
//第二部分
val mapSequence = SequencesKt.map(collectionSequence, (Function1)null.INSTANCE)
//第三部分
val filterSequence = SequencesKt.filter(mapSequence, (Function1)null.INSTANCE)
复制代码
第一部分反编译的源码很简单,主要是调用Iterable<T>中扩展函数将原始数据集转换成Sequence<T>对象。
public fun <T> Iterable<T>.asSequence(): Sequence<T> {
return Sequence { this.iterator() }//传入外部Iterable<T>中的迭代器对象
}
复制代码
更深刻一层:
@kotlin.internal.InlineOnly
public inline fun <T> Sequence(crossinline iterator: () -> Iterator<T>): Sequence<T> = object : Sequence<T> {
override fun iterator(): Iterator<T> = iterator()
}
复制代码
经过外部传入的集合中的迭代器方法返回迭代器对象,经过一个对象表达式实例化一个Sequence<T>,Sequence<T>是一个接口,内部有个iterator()抽象函数返回一个迭代器对象,而后把传入迭代器对象做为Sequence<T>内部的迭代器,也就是至关于给迭代器加了Sequence序列的外壳,核心迭代器仍是由外部传入的迭代器对象,有点偷梁换柱的概念。
经过第一部分,成功将普通集合转换成序列Sequence,而后如今进行map操做,实际上调用了Sequence<T>扩展函数map来实现的
val mapSequence = SequencesKt.map(collectionSequence, (Function1)null.INSTANCE)
复制代码
进入map扩展函数:
public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
return TransformingSequence(this, transform)
}
复制代码
会发现内部会返回一个TransformingSequence对象,该对象构造器接收一个Sequence<T>类型对象,和一个transform的lambda表达式,最后返回一个Sequence<R>类型对象。咱们先暂时解析到这,后面会更加介绍。
经过第二部分,进行map操做后,而后返回的仍是Sequence对象,最后再把这个对象进行filter操做,filter也仍是Sequence的扩展函数,最后返回仍是一个Sequence对象。
val filterSequence = SequencesKt.filter(mapSequence, (Function1)null.INSTANCE)
复制代码
进入filter扩展函数:
public fun <T> Sequence<T>.filter(predicate: (T) -> Boolean): Sequence<T> {
return FilteringSequence(this, true, predicate)
}
复制代码
会发现内部会返回一个FilteringSequence对象,该对象构造器接收一个Sequence<T>类型对象,和一个predicate的lambda表达式,最后返回一个Sequence<T>类型对象。咱们先暂时解析到这,后面会更加介绍。
代码结构图: 图中标注的都是一个个对应各个操做符类,它们都实现Sequence<T>接口
首先,Sequence<T>是一个接口,里面只有一个抽象函数,一个返回迭代器对象的函数,能够把它当作一个迭代器对象外壳。
public interface Sequence<out T> {
/** * Returns an [Iterator] that returns the values from the sequence. * * Throws an exception if the sequence is constrained to be iterated once and `iterator` is invoked the second time. */
public operator fun iterator(): Iterator<T>
}
复制代码
Sequence核心类UML类图
这里只画出了某几个经常使用操做符的类图
注意: 经过上面的UML类关系图能够获得,共享同一个迭代器中的数据的原理实际上就是利用Java设计模式中的状态模式(面向对象的多态原理)来实现的,首先经过Iterable<T>的iterator()返回的迭代器对象去实例化Sequence,而后外部调用不一样的操做符,这些操做符对应着相应的扩展函数,扩展函数内部针对每一个不一样操做返回实现Sequence接口的子类对象,而这些子类又根据不一样操做的实现,更改了接口中iterator()抽象函数迭代器的实现,返回一个新的迭代器对象,可是迭代的数据则来源于原始迭代器中。
经过以上对Sequences总体结构深刻分析,那么接着TransformingSequence、FilteringSequence继续解析就很是简单了。咱们就以TransformingSequence为例:
//实现了Sequence<R>接口,重写了iterator()方法,重写迭代器的实现
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()//取得传入Sequence中的迭代器对象
override fun next(): R {
return transformer(iterator.next())//将原来的迭代器中数据元素作了transformer转化传入,共享同一个迭代器中的数据。
}
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)
}
}
复制代码
序列内部的实现原理是采用状态设计模式,根据不一样的操做符的扩展函数,实例化对应的Sequence子类对象,每一个子类对象重写了Sequence接口中的iterator()抽象方法,内部实现根据传入的迭代器对象中的数据元素,加以变换、过滤、合并等操做,返回一个新的迭代器对象。这就能解释为何序列中工做原理是逐个元素执行不一样的操做,而不是像普通集合全部元素先执行A操做,再全部元素执行B操做。这是由于序列内部始终维护着一个迭代器,当一个元素被迭代的时候,就须要依次执行A,B,C各个操做后,若是此时没有末端操做,那么值将会存储在C的迭代器中,依次执行,等待原始集合中共享的数据被迭代完毕,或者不知足某些条件终止迭代,最后取出C迭代器中的数据便可。
欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不按期翻译一篇Kotlin国外技术文章。若是你也喜欢Kotlin,欢迎加入咱们~~~