当前位置: 首页 > news >正文

Kotlin 中的惰性集合

1 通过序列提高效率

首先看以下代码:

val list = listOf(1, 2, 3, 4, 5)
list.filter { it > 2 }.map { it * 2 }

上面的写法很简单,在处理集合时,类似于上面的操作能帮我们解决大部分的问题。但是,当 list 中的元素非常多的时候(比如超过 10 万),上面的操作在处理集合的时候就会显得比较低效。

filter 方法和 map 方法都会返回都会返回一个新的集合,也就是说上面的操作会产生两个临时集合,因为 list 会先调用 filter 方法,然后产生的集合会再次调用 map 方法。如果 list 中的元素非常多,这将是一笔不小的开销。为了解决这个问题,Sequence(序列)就出现了。序列可以避免创建这些临时的中间对象。

以下是序列的使用:

val list = listOf(1, 2, 3, 4, 5)
list.asSequence().filter { it > 2 }.map { it * 2 }.toList()

首先通过 asSequence() 方法将列表转换为一个序列,然后在这个序列上进行相应的操作,最后通过 toList() 方法将序列转换为列表。

将 list 转换为序列,在很大程度上就提高了上面操作集合的效率。这是因此在使用序列的时候,filter 方法和 map 方法的操作都没有创建额外的集合,这样当集合中的元素数量巨大的时候,就减少了大部分开销。

在 Kotlin 中,序列中元素的求值是惰性的,这就意味着在利用序列进行链式求值时,不需要像操作普通集合那样,每进行一次求值操作,就产生一个新的集合保存中间数据。那么惰性又是什么意思呢?

在编程语言理论中,惰性求值(Lazy Evaluation)表示一种在需要时才进行求值的计算方式。在使用惰性求值的时候,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用时才去求值。通过这种方式,不仅能得到性能上的提升,还有一个重要的好处就是它可以构造出一个无限的数据类型。

通过上面的定义我们可以知道惰性求值的两个好处,一个是优化性能,另一个就是能构造出无限的数据类型。

2 序列的操作方式

我们知道序列中元素的求值方式是采用惰性求值的。那么,惰性求值在序列中是如何体现的呢?

以下面的代码为例:

list.asSequence().filter { it > 2 }.map { it * 2 }.toList()

在这个例子中,我们对序列总共执行了两类操作,第一类:

filter { it > 2 }.map { it * 2 }

filter 和 map 的操作返回的都是序列,我们将这类操作称为中间操作。还有一类:

toList()

这一类操作序列转换为 List,我们将这类操作称为末端操作。其实,Kotlin 中序列的操作就分为两类,一类是中间操作,另一类则是末端操作。

2.1 中间操作

在对普通集合进行链式操作的时候,有些操作会产生中间集合,当用这类操作来对序列进行求值的时候,它们就被称为中间操作,比如上面的 filter 和 map。

每一次中间操作返回的都是一个序列,产生的新序列内部知道如何去变换原来序列中的元素。中间操作都是采用惰性求值的, 比如:

list.asSequence().filter {println("filter $it")it > 2
}.map {println("map $it")it * 2
}

执行,没有打印,说明上面的操作中的 println 方法根本就没有执行,这说明 filter 方法和 map 方法的执行被延迟了,这就是惰性求值的体现。

惰性求值仅仅在该值被需要的时候才会真正去求值。那么这个“被需要”的状态该怎么触发呢?这就是另外一个操作了——末端操作。

2.2 末端操作

在对集合进行操作的时候,大部分情况下,我们在意的只是结果,而不是中间过程。

末端操作就是一个返回结果的操作,它的返回值不能是序列,必须是一个明确的结果,比如列表、数字、对象等表意明确的结果。末端操作一般都是放在链式操作的末尾,在执行末端操作的时候,会出发中间操作的延迟计算,也就是将被需要这个状态打开了。

下面给上面的例子加上末端操作:

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

可以看到,所有的中间操作都被执行了。

如果不用序列而是用列表来实现会有什么不同之处:

list.filter {println("filter $it")it > 2
}.map {println("map $it")it * 2
}.toList()//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,这样会减少一部分开销。

3 序列可以是无限的

惰性求值最大的好处就是可以构造出一个无限的数据类型。

那么我们是否可以使用序列来构造一个无限的数据类型呢?答案是肯定的。常见的无限数据类型是什么呢?数列,比如自然数数列就是一个无限的数列。

那么如何去实现一个自然数列呢?采用一般的列表肯定是不行的,因为构建一个列表必须列举出列表中元素,而我们是么有办法将自然数全部列举出来。

自然数是有一定规律的,就是后一个数永远是前一个数加 1 的结果,我们只需要实现一个列表,让这个列表描述这种规律,那么也就是相当于实现了一个无限的自然数数列。Kotlin 为我们提供了这样一个方法,去创建无限的数列:

val naturalNumList = generateSequence(0) { it + 1 }

通过上面着一行代码,通过调用 generateSequence 就非常简单地实现了自然数数列。

我们知道序列是惰性求值的,所以上面创建的序列是不会把所有的自然数都列举出来的,只有在我们调用一个末端操作的时候,才去列举我们所需要的列表。

比如我们要从这个自然数列表中取出前 10 个自然数:

val list = naturalNumList.takeWhile { it <= 9 }.toList()
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

关于无限序列这一点,我们不能将一个无限的数据结构通过穷举的方式呈现出来,而只是实现了一种表示无限的状态,让我们在使用的时候感觉它是无限的。

4 序列与 Java 8 Stream 对比

序列看上去就和 Java 8 中的流(Stream)比较类似。下面就列举一些 Java 8 Stream 中比较常见的特性,并与 Kotlin 中的序列进行比较。

4.1 Java 也能使用函数风格 API

在 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 的流也是存在中间操作和末端操作的,所以必须通过上面的一系列转换才行。

4.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());
4.3 Stream 能够并行处理数据

Java 8 中的流非常强大,其中有一个非常重要的特性就是 Java 8 Stream 能够在多核架构上并行的进行流处理。比如将前面的例子转换为并行处理的方式如下:

students.paralleStream().filter(it -> it.sex == "m").collect(toList());
http://www.lryc.cn/news/323454.html

相关文章:

  • 2024年React初学者入门路线指南
  • 【Java基础】了解Java安全体系JCA,使用BouncyCastle的ED25519算法生成密钥对、数据签名
  • SQL Server创建存储过程
  • GraphPad Prism 10:一站式数据分析解决方案
  • 前端基础篇-深入了解 Ajax 、Axios
  • 是德科技keysight N1912A双通道功率计
  • 怿星科技Neptune CHT-S测试系统,让智能座舱测试更加高效便捷
  • Vscode初建Vue时几个需要注意的问题
  • 最长不下降子序列
  • QT gridlayout 循环设置组件,表格也通用 已解决
  • 后端前行Vue之路(一):初识Vue
  • C#、.NET版本、Visual Studio版本对应关系及Visual Studio老版本离线包下载地址
  • yarn安装包时报错error Error: certificate has expired
  • 手机可以格式化存储卡吗?格式化以后出现什么情况
  • 亚马逊AWS展示高效纠错的全新量子比特!
  • FEX-Emu在Debian/Ubuntu系统使用
  • docker 不同架构镜像融合问题解决
  • windows_anaconda 安装pytorch
  • IP SSL证书注册流程
  • shentou思路流程
  • 航空实时监控
  • 第十四届蓝桥杯JavaB组省赛真题 - 幸运数字
  • 【练习】双指针算法思想
  • Leetcode 20. 有效的括号
  • jupyter | mac jupyter快捷键
  • 么样才能用最便捷的方式为Mac提速呢?
  • 专业前沿问题问答合集10-2——比特币的加密原理
  • C++中的流
  • 解决vue3中使用v-html,click不生效的问题
  • macOS下Java应用的打包和安装程序制作