## 惰性集合操作:[序列](http://www.kotlincn.net/docs/reference/sequences.html#%E5%BA%8F%E5%88%97)
### 通过序列提高效率
在前面一节中,你看到了许多链式集合函数调用的例子,比如map 和filter 。这些函数会及早地创建中间集合,也就是说每一步的中间结果都被存储在一个临时列表。序列给了你执行这些操作的另一种选择,可以避免创建这些临时中间对象。
```
people.map(Person:: name).filter{ it.startsWith("A")}
```
Kotlin 标准库参考文档有说明, filter和map 都会返回一个列表。这意味着上面例子中的**链式调用会创建两个列表: 一个保存filter 函数的结果,另一个保存map 函数的结果。如果源列表只有两个元素,这不是什么问题,但是如果有一百万个元素,(链式)调用就会变得十分低效**。
为了提高效率, 可以把操作变成使用序列,而不是直接使用集合:
![](https://img.kancloud.cn/37/b2/37b2fdb0f882b9a55befa523ac642e54_1006x235.png)
应用这次操作后的结果和前面的例子一模一样: 一个以字母A 开头的人名列表。但是第二个例子**没有创建任何用于存储元素的中间集合,所以元素数量巨大的情况下性能将显著提升**。
Kotlin惰性集合操作的入口就是Sequence接口。这个接口表示的就是一个可以逐个列举元素的元素序列。Sequence只提供了一个方法,iterator ,用来从序列中获取值。
Sequence 接口的强大之处在于其操作的实现方式。**序列中的元素求值是惰性的。因此,可以使用序列更高效地对集合元素执行链式操作,而不需要创建额外的集合来保存过程中产生的中间结果**。
**可以调用扩展函数asSequence 把任意集合转换成序列,调用toList 来做反向的转换**。
**为什么需要把序列转换回集合?用序列代替集合不是更方便吗**?特别是它们还有这么多优点。答案是:**有时候如果你只需要迭代序列中的元素,可以直接使用序列。如果你要用其他的API方法,比如用下标访问元素,那么你需要把序列转换成列表**。
>[info]注意:通常, 需要对一个大型集合执行链式操作时要使用序列。以后讨论Kotlin 常规集合的及早操作高效的原因,尽管它会创建中间集合。但是如果集合拥有数量巨大的元素元,素为中间结果进行重新分配开销巨大,所以惰性求值是更好的选择
因为序列的操作是惰性的,为了执行它们,你需要直接送代序列元素,或者把序列转换成一个集合。
在Kotlin中,序列中元素的求值是惰性的,这就意味着在利用序列进行链式求值的时候,不需要像操作普通集合那样,每进行一次求值操作,就产生一个新的集合保存中间数据。那么惰性又是什么意思呢?先来看看它的定义:
**在编程语言理论中,惰性求值(Lazy Evaluation)表示一种在需要时才进行求值的计算方式。在使用惰性求值的时候,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用时才去求值。通过这种方式,不仅能得到性能上的提升,还有一个最重要的好处就是它可以构造出一个无限的数据类型**。
通过上面的定义我们可以简单归纳出**惰性求值的两个好处,一个是优化性能,另一个就是能够构造出无限的数据类型**。这里只需要先知道这个概念,在后面我们会详细介绍。
### 执行序列操作:中间和末端操作
序列操作分为两类:中间的和末端的。一次中间操作返回的是另一个序列,这个新序列知道如何变换原始序列中的元素。而一次末端操作返回的是一个结果,这个结果可能是集合、元素、数字,或者其他从初始集合的变换序列中获取的任意对象。
![](https://img.kancloud.cn/6d/bd/6dbd9058b75dfca2347ee72dcf04d005_586x282.png)
中间操作始终都是惰性的。先看看下面这个缺少了末端操作的例子:
```
fun main(args: Array<String>) {
listOf(1, 2, 3, 4).asSequence()
.map { print("map($it) "); it * it }
.filter { print("filter($it) "); it % 2 == 0 }
}
```
执行这段代码并不会在控制台上输出任何内容。这意味着map 和filter 变换被延期了,它们只有在获取结果的时候才会被应用( 即末端操作被调用的时候),即惰性求值仅仅在该值被需要的时候才会真正去求值。那么这个“被需要”的状态该怎么去触发呢?这就需要另外一个操作了——末端操作。:
#### 末端操作
在对集合进行操作的时候,大部分情况下,我们在意的只是结果,而不是中间过程。末端操作就是一个返回结果的操作,它的返回值不能是序列,必须是一个明确的结果,比如列表、数字、对象等表意明确的结果。末端操作一般都放在链式操作的末尾,在执行末端操作的时候,会去触发中间操作的延迟计算,也就是将“被需要”这个状态打开了。我们给前面的那个例子加上末端操作:
~~~
fun main(args: Array<String>) {
val list = listOf(1, 2, 3, 4, 5)
list.asSequence().filter {
println("filter($it)")
it > 2
}.map {
println("map($it)")
it * 2
}.toList()
}
~~~
结果
```
filter(1)
filter(2)
filter(3)
map(3)
filter(4)
map(4)
filter(5)
map(5)
```
可以看到,所有的中间操作都被执行了。仔细看看上面的结果,我们可以发现一些有趣的地方。作为对比,我们先来看看上面的操作如果不用序列而用列表来实现会有什么不同之处:
~~~
fun main(args: Array<String>) {
val list = listOf(1, 2, 3, 4, 5)
list.filter {
println("filter($it)")
it > 2
}.map {
println("map($it)")
it * 2
}
}
~~~
输出结果
```
filter(1)
filter(2)
filter(3)
filter(4)
filter(5)
map(3)
map(4)
map(5)
```
通过对比上面的结果,我们可以发现,普通集合在进行链式操作的时候会先在list上调用filter,然后产生一个结果列表,接下来map就在这个结果列表上进行操作。而**序列则不一样,序列在执行链式操作的时候,会将所有的操作都应用在一个元素上,也就是说,第1个元素执行完所有的操作之后,第2个元素再去执行所有的操作,以此类推**。反映到我们这个例子上面,就是第1个元素执行了filter之后再去执行map,然后第2个元素也是这样。
通过上面序列的返回结果我们还能发现,由于列表中的元素1、2没有满足filter操作中大于2的条件,所以接下来的map操作就不会去执行了。所以**当我们使用序列的时候,如果filter和map的位置是可以相互调换的话,应该优先使用filter,这样会减少一部分开销**。
下面我们看另一个示例
```
fun main(args: Array<String>) {
listOf(1, 2, 3, 4).asSequence()
.map { print("map($it) "); it * it }
.filter { print("filter($it) "); it % 2 == 0 }
.toList()
}
```
输出结果
```
map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16)
```
末端操作触发执行了所有的延期计算。
这个例子中另外一件值得注意的重要事情是计算执行的顺序。一个笨办法是先在每个元素上调用map 函数,然后在结果序列的每个元素上再调用filter 函数。map 和filter 对集合就是这样做的,而序列不一样。对序列来说,所有操作是按
顺序应用在每一个元素上:处理完第一个元素(先映射再过滤),然后完成第二个元素的处理,以此类推。
这种方法意味着部分元素根本不会发生任何变换,如果在轮到它们之前就己经取得了结果。我们来看一个map 和find 的例子。首先把一个数字映射成它的平方,然后找到第一个比数字3 大的条目:
```
println(listOf(1, 2, 3, 4).asSequence().map { it * it }.filter {it >3})//4
```
如果同样的操作被应用在集合而不是序列上时,那么map 的结果首先被求出来,即变换初始集合中的所有元素。第二步,中间集合中满足判断式的一个元素会被找出来。而对于序列来说,惰性方法意味着你可以跳过处理部分元素。下图阐明了这段代码两种求值方式之间的区别, 一种是及早求值(使用集合), 一种是惰性求值(使用序列)。
![](https://img.kancloud.cn/73/56/73562365b278a20e6b1ea46cbc78217a_942x447.png)
第一种情况,当你使用集合的时候,列表被变换成了另一个列表,所以map 变换应用到每一个元素上,包括了数字3 和4 。然后,第一个满足判断式的元素被找到了:数字2 的平方。
第二种情况, find 调用一开始就逐个地处理元素。从原始序列中取一个数字,用map 变换它,然后再检查它是否满足传给find 的判断式。当进行到数字2 时,发现它的平方己经比数字3 大,就把它作为find 操作结果返回了。不再需要继续检查数字3 和4 ,因为这之前你己经找到了结果。
在集合上执行操作的顺序也会影响性能。假设你有一个人的集合,想要打印集合中那些长度小于某个限制的人名。你需要做两件事: 把每个人映射成他们的名字,然后过滤掉其中那些不够短的名字。这种情况可以用任何顺序应用map 和filter操作。两种顺序得到的结果一样,但它们应该执行的变换总次数是不一样的,如图所示。
![](https://img.kancloud.cn/72/11/72112fb6b5432704901131e929b1e362_1148x181.png)
![](https://img.kancloud.cn/79/b3/79b3eb702a6bda9d1ce7495e4735b58e_1155x540.png)
如果map 在前,每个元素都被变换。而如果filter在前,不合适的元素会被尽早地过滤掉且不会发生变换。
### 序列可以是无限的
在介绍惰性求值的时候,我们提到过一点,就是**惰性求值最大的好处是可以构造出一个无限的数据类型**。那么我们能否**使用序列来构造出一个无限的数据类型**呢?答案是肯定的。我们先思考一下,常见的无限的数据类型是什么?我们很容易就能想到数列,比如自然数数列就是一个无限的数列。
那接下来,该怎样去实现一个自然数数列呢?采用一般的列表肯定是不行的,因为构建一个列表必须列举出列表中元素,而我们是没有办法将自然数全部列举出来的。
我们知道,自然数是有一定规律的,就是后一个数永远是前一个数加1的结果,我们**只需要实现一个列表,让这个列表描述这种规律,那么也就相当于实现了一个无限的自然数数列**。好在Kotlin也给我们提供了这样一个方法,去**创建无限的数列**:
```
val naturalNumList = generateSequence(0) { it + 1}
```
通过上面这一行代码,我们就非常简单地实现了自然数数列。上面我们**调用了一个方法generateSequence来创建序列**。我们知道**序列是惰性求值的,所以上面创建的序列是不会把所有的自然数都列举出来的,只有在我们调用一个末端操作的时候,才去列举我们所需要的列表**。比如我们要从这个自然数列表中取出前10个自然数:
```
>>> naturalNumList.takeWhile {it <= 9}.toList()
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
```
>[info]注意:关于无限数列这一点,我们不能将一个无限的数据结构通过穷举的方式呈现出来,而只是实现了一种表示无限的状态,让我们在使用时感觉它就是无限的。
### 序列与Java 8 Stream对比
如果你熟悉Java 8的话,当你看到序列的时候,你一定不会陌生。因为序列看上去就和Java 8中的流(Stream)比较类似。这里我们来列举一些Java 8 Stream中比较常见的特性,并与Kotlin中的序列进行比较
#### 1. Java也能使用函数式风格API
在前面我们介绍了Kotlin中的许多函数式风格API,这些API相比于Java中传统的集合操作显得优雅多了。但是当Java 8出来之后,在Java中也能像在Kotlin中那样操作集合了,比如前面将性别为男的学生筛选出来就可以这样去做:
```
students.stream().filter (it -> it.sex == "m").collect(toList());
```
在上面的Java代码中,我们通过使用stream就能够使用类似于filter这种简洁的函数式API了。但是相比于Kotlin, Java的这种操作方式还是有些烦琐,因为如果要对集合使用这种API,就必须先将集合转换为stream,操作完成之后,还要将stream转换为List,这种操作有点类似于Kotlin的序列。这是因为Java 8的流和Kotlin中的序列一样,也是惰性求值的,这就意味着Java 8的流也是存在中间操作和末端操作的(事实也确实如此),所以必须通过上面的一系列转换才行。
#### 2. Stream是一次性的
与Kotlin的序列不同,Java 8中的流是一次性的。意思就是说,**如果我们创建了一个Stream,我们只能在这个Stream上遍历一次。这就和迭代器很相似,当你遍历完成之后,这个流就相当于被消费掉了,你必须再创建一个新的Stream才能再遍历一次**。
```
Stream<Student> studentsStream = students.stream();
studentsStream.filter (it -> it.sex == "m").collect(toList());
studentsStream.filter (it -> it.sex == "f").collect(toList()); //你不能再继续在studentsStream上进行这种遍历操作,否则会报错
```
#### 3. Stream能够并行处理数据
Java 8中的流非常强大,其中有一个非常重要的特性就是Java 8 Stream能够在多核架构上并行地进行流的处理。比如将前面的例子转换为并行处理的方式如下:
```
students.paralleStream().filter (it -> it.sex == "m").collect(toList());
```
只需要将stream换成paralleStream即可。当然使用流并行处理数据还有许多需要注意的地方,这里只是简单地介绍一下。并行处理数据这一特性是Kotlin的序列目前还没有实现的地方,如果我们需要用到处理多线程的集合还需要依赖Java。
>[info]流VS序列
如果你很熟悉Java 8 中的流这个概念,你会发现序列就是它的翻版。Kotlin提供了这个概念自己的版本,原因是Java 8 的流并不支持那些基于Java 老版本的平台,例如Android。如果你的目标版本是Java 8 ,流提供了一个Kotlin 集合和序列目前还没有实现的重要特性:在多个CPU 上并行执行流操作(比如map和filter )的能力。可以根据Java 的目标版本和你的特殊要求在流和序列之间做出选择。
### 创建序列
前面的例子都是使用同一个方法创建序列: 在集合上调用asSequence()。另一种可能性是**使用generateSequence函数。给定序列中的前一个元素,这个函数会计算出下一个元素**。下面这个例子就是如何使用generateSequence计算100 以内所有自然数之和。
```
fun main(args: Array<String>) {
val naturalNumbers = generateSequence(0) { it + 1 }
val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
println(numbersTo100.sum())//当获取结果sum时,所有被推迟的操作都被执行
//5050
}
```
>[info]注意,这个例子中的naturalNumbers 和numbersTo100都是有延期操作的序列。这些序列中的实际数字直到你调用末端操作(这里是sum )的时候才会求值。
另一种常见的用例是父序列。如果元素的父元素和它的类型相同(比如人类或者Java 文件),你可能会对它所有祖先组成的序列的特质感兴趣。下面这个例子可以查询文件是否放在隐藏目录中,通过创建一个其父目录的序列并检查每个目录的属性来实现。
```
import java.io.File
fun File.isInsideHiddenDirectory() =
generateSequence(this) { it.parentFile }.any { it.isHidden }
fun main(args: Array<String>) {
val file = File("/Users/svtk/.HiddenDir/a.txt")
println(file.isInsideHiddenDirectory())//true
}
```
又一次,你生成了一个序列,通过提供第一个元素和获取每个后续元素的方式来实现。如果把any换成find,你还可以得到想要的那个目录(对象〉。注意,使用序列允许你找到需要的目录之后立即停止遍历父目录。
- 前言
- Kotlin简介
- IntelliJ IDEA技巧总结
- idea设置类注释和方法注释模板
- 像Android Studion一样创建工程
- Gradle
- Gradle入门
- Gradle进阶
- 使用Gradle创建一个Kotlin工程
- 环境搭建
- Androidstudio平台搭建
- Eclipse的Kotlin环境配置
- 使用IntelliJ IDEA
- Kotlin学习路线
- Kotlin官方中文版文档教程
- 概述
- kotlin用于服务器端开发
- kotlin用于Android开发
- kotlin用于JavaScript开发
- kotlin用于原生开发
- Kotlin 用于数据科学
- 协程
- 多平台
- 新特性
- 1.1的新特性
- 1.2的新特性
- 1.3的新特性
- 开始
- 基本语法
- 习惯用法
- 编码规范
- 基础
- 基本类型
- 包与导入
- 控制流
- 返回与跳转
- 类与对象
- 类与继承
- 属性与字段
- 接口
- 可见性修饰符
- 扩展
- 数据类
- 密封类
- 泛型
- 嵌套类
- 枚举类
- 对象
- 类型别名
- 内嵌类
- 委托
- 委托属性
- 函数与Lambda表达式
- 函数
- Lambda表达式
- 内联函数
- 集合
- 集合概述
- 构造集合
- 迭代器
- 区间与数列
- 序列
- 操作概述
- 转换
- 过滤
- 加减操作符
- 分组
- 取集合的一部分
- 取单个元素
- 排序
- 聚合操作
- 集合写操作
- List相关操作
- Set相关操作
- Map相关操作
- 多平台程序设计
- 平台相关声明
- 以Gradle创建
- 更多语言结构
- 解构声明
- 类型检测与转换
- This表达式
- 相等性
- 操作符重载
- 空安全
- 异常
- 注解
- 反射
- 作用域函数
- 类型安全的构造器
- Opt-in Requirements
- 核心库
- 标准库
- kotlin.test
- 参考
- 关键字与操作符
- 语法
- 编码风格约定
- Java互操作
- Kotlin中调用Java
- Java中调用Kotlin
- JavaScript
- 动态类型
- kotlin中调用JavaScript
- JavaScript中调用kotlin
- JavaScript模块
- JavaScript反射
- JavaScript DCE
- 原生
- 并发
- 不可变性
- kotlin库
- 平台库
- 与C语言互操作
- 与Object-C及Swift互操作
- CocoaPods集成
- Gradle插件
- 调试
- FAQ
- 协程
- 协程指南
- 基础
- 取消与超时
- 组合挂起函数
- 协程上下文与调度器
- 异步流
- 通道
- 异常处理与监督
- 共享的可变状态与并发
- Select表达式(实验性)
- 工具
- 编写kotlin代码文档
- 使用Kapt
- 使用Gradle
- 使用Maven
- 使用Ant
- Kotlin与OSGI
- 编译器插件
- 编码规范
- 演进
- kotlin语言演进
- 不同组件的稳定性
- kotlin1.3的兼容性指南
- 常见问题
- FAQ
- 与Java比较
- 与Scala比较(官方已删除)
- Google开发者官网简介
- Kotlin and Android
- Get Started with Kotlin on Android
- Kotlin on Android FAQ
- Android KTX
- Resources to Learn Kotlin
- Kotlin样品
- Kotlin零基础到进阶
- 第一阶段兴趣入门
- kotlin简介和学习方法
- 数据类型和类型系统
- 入门
- 分类
- val和var
- 二进制基础
- 基础
- 基本语法
- 包
- 示例
- 编码规范
- 代码注释
- 异常
- 根类型“Any”
- Any? 可空类型
- 可空性的实现原理
- kotlin.Unit类型
- kotlin.Nothing类型
- 基本数据类型
- 数值类型
- 布尔类型
- 字符型
- 位运算符
- 变量和常量
- 语法和运算符
- 关键字
- 硬关键字
- 软关键字
- 修饰符关键字
- 特殊标识符
- 操作符和特殊符号
- 算术运算符
- 赋值运算符
- 比较运算符
- 逻辑运算符
- this关键字
- super关键字
- 操作符重载
- 一元操作符
- 二元操作符
- 字符串
- 字符串介绍和属性
- 字符串常见方法操作
- 字符串模板
- 数组
- 数组介绍创建及遍历
- 数组常见方法和属性
- 数组变化以及下标越界问题
- 原生数组类型
- 区间
- 正向区间
- 逆向区间
- 步长
- 类型检测与类型转换
- is、!is、as、as-运算符
- 空安全
- 可空类型变量
- 安全调用符
- 非空断言
- Elvis操作符
- 可空性深入
- 可空性和Java
- 函数
- 函数式编程概述
- OOP和FOP
- 函数式编程基本特性
- 组合与范畴
- 在Kotlin中使用函数式编程
- 函数入门
- 函数作用域
- 函数加强
- 命名参数
- 默认参数
- 可变参数
- 表达式函数体
- 顶层、嵌套、中缀函数
- 尾递归函数优化
- 函数重载
- 控制流
- if表达式
- when表达式
- for循环
- while循环
- 循环中的 Break 与 continue
- return返回
- 标签处返回
- 集合
- list集合
- list集合介绍和操作
- list常见方法和属性
- list集合变化和下标越界
- set集合
- set集合介绍和常见操作
- set集合常见方法和属性
- set集合变换和下标越界
- map集合
- map集合介绍和常见操作
- map集合常见方法和属性
- map集合变换
- 集合的函数式API
- map函数
- filter函数
- “ all ”“ any ”“ count ”和“ find ”:对集合应用判断式
- 别样的求和方式:sumBy、sum、fold、reduce
- 根据人的性别进行分组:groupBy
- 扁平化——处理嵌套集合:flatMap、flatten
- 惰性集合操作:序列
- 区间、数组、集合之间转换
- 面向对象
- 面向对象-封装
- 类的创建及属性方法访问
- 类属性和字段
- 构造器
- 嵌套类(内部类)
- 枚举类
- 枚举类遍历&枚举常量常用属性
- 数据类
- 密封类
- 印章类(密封类)
- 面向对象-继承
- 类的继承
- 面向对象-多态
- 抽象类
- 接口
- 接口和抽象类的区别
- 面向对象-深入
- 扩展
- 扩展:为别的类添加方法、属性
- Android中的扩展应用
- 优化Snackbar
- 用扩展函数封装Utils
- 解决烦人的findViewById
- 扩展不是万能的
- 调度方式对扩展函数的影响
- 被滥用的扩展函数
- 委托
- 委托类
- 委托属性
- Kotlin5大内置委托
- Kotlin-Object关键字
- 单例模式
- 匿名类对象
- 伴生对象
- 作用域函数
- let函数
- run函数
- with函数
- apply函数
- also函数
- 标准库函数
- takeIf 与 takeUnless
- 第二阶段重点深入
- Lambda编程
- Lambda成员引用高阶函数
- 高阶函数
- 内联函数
- 泛型
- 泛型的分类
- 泛型约束
- 子类和子类型
- 协变与逆变
- 泛型擦除与实化类型
- 泛型类型参数
- 泛型的背后:类型擦除
- Java为什么无法声明一个泛型数组
- 向后兼容的罪
- 类型擦除的矛盾
- 使用内联函数获取泛型
- 打破泛型不变
- 一个支持协变的List
- 一个支持逆变的Comparator
- 协变和逆变
- 第三阶段难点突破
- 注解和反射
- 声明并应用注解
- DSL
- 协程
- 协程简介
- 协程的基本操作
- 协程取消
- 管道
- 慕课霍丙乾协程笔记
- Kotlin与Java互操作
- 在Kotlin中调用Java
- 在Java中调用Kotlin
- Kotlin与Java中的操作对比
- 第四阶段专题练习
- 朱凯Kotlin知识点总结
- Kotlin 基础
- Kotlin 的变量、函数和类型
- Kotlin 里那些「不是那么写的」
- Kotlin 里那些「更方便的」
- Kotlin 进阶
- Kotlin 的泛型
- Kotlin 的高阶函数、匿名函数和 Lambda 表达式
- Kotlin协程
- 初识
- 进阶
- 深入
- Kotlin 扩展
- 会写「18.dp」只是个入门——Kotlin 的扩展函数和扩展属性(Extension Functions / Properties)
- Kotlin实战-开发Android