Kotlin 惰性集合操作-序列 Sequence

news2024/10/9 8:36:30

集合操作函数 和 序列

在了解 Kotlin 惰性集合之前,先看一下 Koltin 标注库中的一些集合操作函数。

定义一个数据模型 Person 和 Book 类:

data class Person(val name: String, val age: Int) 

data class Book(val title: String, val authors: List<String>)

filter 和 map 操作:

    val people = listOf<Person>(
            Person("xiaowang", 30),
            Person("xiaozhang", 32),
            Person("xiaoli", 28)
        )
        //大于 30 岁的人的名字集合列表
     people.filter { it.age >= 30 }.map(Person::name)

count 操作:

   val people = listOf<Person>(
            Person("xiaowang", 30),
            Person("xiaozhang", 32),
            Person("xiaoli", 28)
        )
        //小于 30 岁人的个数
   people.count { it.age < 30 }

flatmap 操作:

      val books = listOf<Book>(
            Book("Java 语言程序设计", arrayListOf("xiaowang", "xiaozhang")),
            Book("Kotlin 语言程序设计", arrayListOf("xiaoli", "xiaomao")),
        )
        // 所有书的名字集合列表
        books.flatMap { it.authors }.toList()

在上面这些函数,每做一步操作,都会创建中间集合,也就是每一步的中间结果都被临时存储在一个临时集合中

filter 函数源码:

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    //创建一个新的集合列表
    return filterTo(ArrayList<T>(), predicate)
}

public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
    for (element in this) if (predicate(element)) destination.add(element)
    return destination
}

map 函数源码:

public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
    //创建一个新的集合列表
    return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}

public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.mapTo(destination: C, transform: (T) -> R): C {
    for (item in this)
        destination.add(transform(item))
    return destination
}

如果被操作的元素过多,假设 people 或 books 超过 50个、100个,那么 函数链式调用 如:fliter{}.map{} 就会变得低效,且浪费内存。

Kotlin 为解决上面这种问题,提供了惰性集合操作 Sequence 接口。这个接口表示一个可以逐个列举的元素列表。Sequence 只提供了一个 方法, iterator,用来从序列中获取值。

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>
}

public inline fun <T> Sequence(crossinline iterator: () -> Iterator<T>): Sequence<T> = object : Sequence<T> {
    override fun iterator(): Iterator<T> = iterator()
}

/**
 * Creates a sequence that returns all elements from this iterator. The sequence is constrained to be iterated only once.
 *
 * @sample samples.collections.Sequences.Building.sequenceFromIterator
 */
public fun <T> Iterator<T>.asSequence(): Sequence<T> = Sequence { this }.constrainOnce()

序列中的元素求值是惰性的。因此,可以使用序列更高效地对集合元素执行链式操作,而不需要创建额外的集合来保存过程中产生的中间结果。关于这个惰性是怎么来的,后面再详细解释。

可以调用扩展函数 asSequence 把任意集合转换成序列,调用 toList 来做反向的转换。

 val people = listOf<Person>(
            Person("xiaowang", 30),
            Person("xiaozhang", 32),
            Person("xiaoli", 28)
        )
  people.asSequence().filter { it.age >= 30 }.map(Person::name).toList()
 val books = listOf<Book>(
            Book("Java 语言程序设计", arrayListOf("xiaowang", "xiaozhang")),
            Book("Kotlin 语言程序设计", arrayListOf("xiaoli", "xiaomao")),
        )
 books.asSequence().flatMap { it.authors }.toList()

序列中间和末端操作

序列操作分为两类:中间的和末端的。一次中间操作返回的是另一个序列,这个新序列知道如何变换原始序列中的元素。而一次末端返回的是一个结果,这个结果可能是集合、元素、数字,或者其他从初始集合的变换序列中获取的任意对象。

中间操作始终是惰性的。

下面从例子来理解这个惰性

listOf(1, 2, 3, 4).asSequence().map {
            println("map${it}")
            it * it
        }.filter {
            println("filter${it}")
            it % 2 == 0
        }

上面这段代码在控制台不会输出任何内容(因为没有末端操作)。

listOf(1, 2, 3, 4).asSequence().map {
            println("map${it}")
            it * it
        }.filter {
            println("filter${it}")
            it % 2 == 0
        }.toList()
 
 控制台输出:
2023-01-01 20:23:05.071 17000-17000/com.wangjiang.example D/TestSequence: map1
2023-01-01 20:23:05.071 17000-17000/com.wangjiang.example D/TestSequence: filter1
2023-01-01 20:23:05.071 17000-17000/com.wangjiang.example D/TestSequence: map2
2023-01-01 20:23:05.071 17000-17000/com.wangjiang.example D/TestSequence: filter4
2023-01-01 20:23:05.071 17000-17000/com.wangjiang.example D/TestSequence: map3
2023-01-01 20:23:05.071 17000-17000/com.wangjiang.example D/TestSequence: filter9
2023-01-01 20:23:05.071 17000-17000/com.wangjiang.example D/TestSequence: map4
2023-01-01 20:23:05.071 17000-17000/com.wangjiang.example D/TestSequence: filter16

在末端操作 .toList()的时候,mapfilter 变换才被执行了,而且元素是被逐个执行的。并不是所有元素经在 map 操作执行完成后,再执行 filter 操作。

为什么元素是逐个被执行,首先看下 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
}

最后的 toCollection 方法中的 for (item in this),其实就是调用 Sequence 中的迭代器 Iterator 进行元素迭代。其中这个 this 来自于 filter,也就是使用 filterIterator 进行元素迭代。来看下 filter

public fun <T> Sequence<T>.filter(predicate: (T) -> Boolean): Sequence<T> {
    return FilteringSequence(this, true, predicate)
}

internal class FilteringSequence<T>(
    private val sequence: Sequence<T>,
    private val sendWhen: Boolean = true,
    private val predicate: (T) -> Boolean
) : Sequence<T> {

    override fun iterator(): Iterator<T> = object : Iterator<T> {
        val iterator = sequence.iterator()
        var nextState: Int = -1 // -1 for unknown, 0 for done, 1 for continue
        var nextItem: T? = null

        private fun calcNext() {
            while (iterator.hasNext()) {
                val item = iterator.next()
                if (predicate(item) == sendWhen) {
                    nextItem = item
                    nextState = 1
                    return
                }
            }
            nextState = 0
        }

        override fun next(): T {
            if (nextState == -1)
                calcNext()
            if (nextState == 0)
                throw NoSuchElementException()
            val result = nextItem
            nextItem = null
            nextState = -1
            @Suppress("UNCHECKED_CAST")
            return result as T
        }

        override fun hasNext(): Boolean {
            if (nextState == -1)
                calcNext()
            return nextState == 1
        }
    }
}

filter 中又会使用上一个 Sequencesequence.iterator() 进行元素迭代。再看下 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)
    }
}

也是使用上一个 Sequencesequence.iterator() 进行元素迭代。所以以此类推,最终会使用转换为 asSequence() 的源 iterator()

下面自定义一个 Sequence 来验证上面的猜想:

listOf(1, 2, 3, 4).asSequence().mapToString {
            Log.d("TestSequence","mapToString${it}")
            it.toString()
        }.toList()

    fun <T> Sequence<T>.mapToString(transform: (T) -> String): Sequence<String> {
        return TransformingStringSequence(this, transform)
    }

    class TransformingStringSequence<T>
    constructor(private val sequence: Sequence<T>, private val transformer: (T) -> String) : Sequence<String> {
        override fun iterator(): Iterator<String> = object : Iterator<String> {
            val iterator = sequence.iterator()
            override fun next(): String {
                val next = iterator.next()
                Log.d("TestSequence","next:${next}")
                return transformer(next)
            }

            override fun hasNext(): Boolean {
                return iterator.hasNext()
            }
        }
    }

控制台输出:
2023-01-01 20:43:43.899 21797-21797/com.wangjiang.example D/TestSequence: next:1
2023-01-01 20:43:43.899 21797-21797/com.wangjiang.example D/TestSequence: mapToString1
2023-01-01 20:43:43.899 21797-21797/com.wangjiang.example D/TestSequence: next:2
2023-01-01 20:43:43.899 21797-21797/com.wangjiang.example D/TestSequence: mapToString2
2023-01-01 20:43:43.899 21797-21797/com.wangjiang.example D/TestSequence: next:3
2023-01-01 20:43:43.899 21797-21797/com.wangjiang.example D/TestSequence: mapToString3
2023-01-01 20:43:43.899 21797-21797/com.wangjiang.example D/TestSequence: next:4
2023-01-01 20:43:43.899 21797-21797/com.wangjiang.example D/TestSequence: mapToString4

所以这就是 Sequence 为什么在获取结果的时候才会被应用,也就是末端操作被调用的时候,才会依次处理每个元素,这也是 被称为惰性集合操作的原因。

经过一系列的 序列操作,每个元素逐个被处理,那么优先处理 filter 序列,其实可以减少变换的总次数。因为每个序列都是使用上一个序列的 sequence.iterator() 进行元素迭代。

创建序列

在集合操作上,可以使用集合直接调用 asSequence() 转换为序列。那么不是集合,有类似集合一样的变换,该怎么操作呢。

下面以求 1到100 的所有自然数之和为例子:

val naturalNumbers = generateSequence(0) { it + 1 }
val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
val sum = numbersTo100.sum()
println(sum)
控制台输出:
5050

先看下 generateSequence 源码:

public fun <T : Any> generateSequence(seed: T?, nextFunction: (T) -> T?): Sequence<T> =
    if (seed == null)
        EmptySequence
    else
        GeneratorSequence({ seed }, nextFunction)

private class GeneratorSequence<T : Any>(private val getInitialValue: () -> T?, private val getNextValue: (T) -> T?) : Sequence<T> {
    override fun iterator(): Iterator<T> = object : Iterator<T> {
        var nextItem: T? = null
        var nextState: Int = -2 // -2 for initial unknown, -1 for next unknown, 0 for done, 1 for continue

        private fun calcNext() {
            //getInitialValue 获取的到就是 generateSequence 的第一个参数 0
            //getNextValue 获取到的就是 generateSequence 的第二个参数 it+1,这个it 就是 nextItem!!
            nextItem = if (nextState == -2) getInitialValue() else getNextValue(nextItem!!)
            nextState = if (nextItem == null) 0 else 1
        }

        override fun next(): T {
            if (nextState < 0)
                calcNext()

            if (nextState == 0)
                throw NoSuchElementException()
            val result = nextItem as T
            // Do not clean nextItem (to avoid keeping reference on yielded instance) -- need to keep state for getNextValue
            nextState = -1
            return result
        }

        override fun hasNext(): Boolean {
            if (nextState < 0)
                calcNext()
            return nextState == 1
        }
    }
}

上面代码其实就是创建一个 Sequence 接口实现类,并实现它的 iterator 接口方法,返回一个 Iterator 迭代器。

public fun <T> Sequence<T>.takeWhile(predicate: (T) -> Boolean): Sequence<T> {
    return TakeWhileSequence(this, predicate)
}

internal class TakeWhileSequence<T>
constructor(
    private val sequence: Sequence<T>,
    private val predicate: (T) -> Boolean
) : Sequence<T> {
    override fun iterator(): Iterator<T> = object : Iterator<T> {
        val iterator = sequence.iterator()
        var nextState: Int = -1 // -1 for unknown, 0 for done, 1 for continue
        var nextItem: T? = null

        private fun calcNext() {
            if (iterator.hasNext()) {
                //iterator.next() 调用的就是上一个 GeneratorSequence 的 next 方法,而返回值就是它的 it+1
                val item = iterator.next()
                //判断条件,也就是 it <= 100 -> item <= 100
                if (predicate(item)) {
                    nextState = 1
                    nextItem = item
                    return
                }
            }
            nextState = 0
        }

        override fun next(): T {
            if (nextState == -1)
                calcNext() // will change nextState
            if (nextState == 0)
                throw NoSuchElementException()
            @Suppress("UNCHECKED_CAST")
            val result = nextItem as T

            // Clean next to avoid keeping reference on yielded instance
            nextItem = null
            nextState = -1
            return result
        }

        override fun hasNext(): Boolean {
            if (nextState == -1)
                calcNext() // will change nextState
            return nextState == 1
        }
    }
}

TakeWhileSequencenext 方法中,会优先调用内部方法 calcNext,而这个方法内部又是调用 GeneratorSequencenext方法,这样就 拿到了当前值 it+1(上一个是0+1,下一个就是1+1),拿到值后再判断 it <= 100 -> item <= 100

public fun Sequence<Int>.sum(): Int {
    var sum: Int = 0
    for (element in this) {
        sum += element
    }
    return sum
}

sum 方法是序列的末端操作,也就是获取结果。for (element in this) ,调用上一个 Sequence 中的迭代器 Iterator 进行元素迭代,以此类推,直到调用 源 Sequence 中的迭代器 Iterator 进行元素迭代。

总结

Kotlin 标准库提供的集合操作函数:filter,map, flatmap 等,在操作的时候会创建存储中间结果的临时列表,当集合元素较多时,这种链式操作就会变得低效。为了解决这种问题,Kotlin 提供了惰性集合操作 Sequence 接口,只有在 末端操作被调用的时候,也就是获取结果的时候,序列中的元素才会被逐个执行,处理完第一个元素后,才会处理第二个元素,这样中间操作是被延期执行的。而且因为是顺序地去执行每一个元素,所以可以先做 filter 变换,再做 map 变换,这样有助于减少变换的总次数。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/135797.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

jmeter 5.5+influxdb 2.0+grafana v9.3.2 - 压测看板setup

Docker set up 安装docker应用 https://docs.docker.com/desktop/install/mac-install/&#xff0c;在官网下载docker安装包&#xff0c;和安装其他的mac应用是一样的操作。 设置国内的镜像仓库&#xff08;拉取镜像会快很多&#xff09; {"registry-mirrors": [&q…

叠氮-聚乙二醇-羧酸;叠氮-单乙二醇-丙酸Azido-PEG1-acid;1393330-34-1小分子PEG衍生物

Azido-PEG1-acid 中文名称&#xff1a;叠氮-聚乙二醇-羧酸&#xff1b;叠氮-单乙二醇-丙酸 英文名称&#xff1a;Azido-PEG1-acid&#xff1b; 分子式&#xff1a;C5H9N3O3 分子量 &#xff1a;159.1 CAS&#xff1a;1393330-34-1 外观&#xff1a;粘稠液体或者固体粉末&#…

SHA和AES加密+GUI Swing写的一个本地运行和保存的密码管理小工具

目录效果项目结构功能1、登录2、加密3、解密4、列表代码1、先准备好两种加密方式的工具类SHAUtilAESUtil2、登录窗口3、主页窗口&#xff08;加密和解密面板&#xff09;4、主页窗口&#xff08;列表面板&#xff09;5、主程序&#xff08;main&#xff09;最后通过SHA和AES加密…

TestStand-序列步骤属性

文章目录GeneralRun OptionLoopingPost ActionSwitchingSynchronizationExpressionPreconditionsRequirementAdditional ResultPropertyCtrl-N创建一个新的Sequence&#xff0c;通过右键创建任意步骤 General Name -步骤的名称。 Type -步骤类型。一般不需要设置。 Adapter-适…

Android Kotlin之协程-异步流Flow的使用

数据流以协程为基础构建&#xff0c;与仅返回单个值的挂起函数相反&#xff0c;数据流可按顺序发出多个值。从概念上来讲&#xff0c;数据流是可通过异步方式进行计算处理的一组数据序列。所发出值的类型必须相同。 数据流包含三个实体&#xff1a; 提供方会生成添加到数据流…

信息安全技术 政务信息共享 数据安全技术要求

声明 本文是学习GB-T 39477-2020 信息安全技术 政务信息共享 数据安全技术要求. 下载地址 http://github5.com/view/790而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 政务信息共享 数据安全 范围 本标准提出了政务信息共享数据安全要求技术框架&…

2023年工作第一天心情感悟

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 今天是2023年1月3日&#xff0c;也是我们上班的第一天。今天这篇随记&#xff0c;也发表下我对2023年的看法&#xff0c;也对过去的2022年做过总结。 &#xff08;2023年元旦&#xff0c;到门头沟…

Spring之ApplicationContext快速入门

目录 一&#xff1a;概述 二&#xff1a;代码演示 三&#xff1a;BeanFactory与ApplicationContext的关系 四&#xff1a;BeanFactory的继承体系 五&#xff1a;ApplicationContext的继承体系 一&#xff1a;概述 ApplicationContext称为Spring容器&#xff0c; 内部封装了…

面试官:能用JavaScript手写一个bind函数吗

经常会看到网上各种手写bind的教程&#xff0c;下面是我在自己实现手写bind的过程中遇到的问题与思考。如果对于如何实现一个手写bind还有疑惑的话&#xff0c;那么可以先看看上面两篇文章。 手写bind vs 原生bind 我们先使用一个典型的手写bind的例子&#xff0c;代码如下&a…

PHP命令执行的函数

在做面试题的时候发现&#xff0c;自己对PHP命令执行的函数的了解并不是很全面&#xff0c;就想这去学习一下。我也在网上找到了许多的资料&#xff0c;在这里我就相当于一个总结吧。 system(); System()函数的主要功能是在系统权限允许的情况是执行系统命令,windows系统和Lin…

【服务器数据恢复】EMC存储Zfs文件系统下raid5数据恢复案例

服务器存储数据恢复环境&#xff1a; 某公司一台EMC存储&#xff0c;12块硬盘组成raid5&#xff0c;2块热备盘&#xff1b; Zfs文件系统。 服务器存储故障&#xff1a; 硬盘故障导致存储崩溃。 服务器存储数据恢复过程&#xff1a; 1、对故障存储所有硬盘进行物理故障检测&…

详细软件著作权的申请

一&#xff0c;申请注册账号并进行实名认证 在中国版权保护中心官网注册账号。 我是自己申请的所以选择的个人&#xff0c;这里根据实际情况进行选择后注册。 注册后进行实名认证&#xff08;3-7个工作日人工会进行审核&#xff0c;所以每个著作权人都要提前注册并进行实名认证…

论文投稿指南——中文核心期刊推荐(地球物理学)

【前言】 &#x1f680; 想发论文怎么办&#xff1f;手把手教你论文如何投稿&#xff01;那么&#xff0c;首先要搞懂投稿目标——论文期刊 &#x1f384; 在期刊论文的分布中&#xff0c;存在一种普遍现象&#xff1a;即对于某一特定的学科或专业来说&#xff0c;少数期刊所含…

【电商】电商后台---商品上架前的最后准备

电商后台相关模块进行维护后&#xff0c;离商品上架越来越近。 在供应商、合同、商品、价税等都维护完成后&#xff0c;采购部创建采购单&#xff0c;离商品可以上架销售越来越近了。 本篇再接着梳理一下商品销售前的最后准备工作&#xff08;没考虑促销&#xff09;&#xff…

P1111 修复公路

题目背景 AA地区在地震过后&#xff0c;连接所有村庄的公路都造成了损坏而无法通车。政府派人修复这些公路。 题目描述 给出A地区的村庄数NN&#xff0c;和公路数MM&#xff0c;公路是双向的。并告诉你每条公路的连着哪两个村庄&#xff0c;并告诉你什么时候能修完这条公路。问…

Python-123练习-04简单分支

文章目录1. 判断闰年2. 今年多少天3. 今天是第几天4. 判断奇偶数5. 计算整数 n 的阶乘6. 判断是否直角三角形7. 判断三角形并计算面积8. 出租车计费9. 一元二次方程求根10. 个税计算器11. 分期付款计算器12. 字符大小写转换1. 判断闰年 描述‪‬‪‬‪‬‪‬‪‬‮‬‪‬‫‬‪…

学习HTTP协议,这一篇就够啦

HTTP协议一、什么是HTTP1.1 应用层协议1.2 HTTP1.3 HTTP协议的工作过程二、HTTP协议格式2.1 Fiddler抓包工具2.2 协议格式三、HTTP请求 (Request)3.1 认识 "方法" (method)3.1.1 GET 方法3.1.2 POST 方法3.1.3 GET和POST比较3.1.4 其他方法3.2 认识URL3.2.1 URL基本格…

Elasticsearch:使用 Node.js 将实时数据提取到 Elasticsearch 中(一)

Elasticsearch 是一个强大的 RESTful 搜索和分析引擎&#xff0c;能够处理越来越多的用例。 它将集中存储你的数据&#xff0c;以实现闪电般的快速搜索、微调相关性以及可轻松扩展的强大分析。 关于如何使用 Elastic Stack&#xff08;又名 ELK 堆栈&#xff09;将数据摄取到 E…

【C++编程调试秘籍】| 总结归纳要点(上)

文章目录一、编译器是捕捉缺陷的最好场合1 如何使用编译器捕捉缺陷二、在运行时遇见错误该如何处理1 该输出哪些错误信息2 执行安全检查则会减低程序效率&#xff0c;该如何处理呢3 当运行时遇到错误时&#xff0c;该如何处理四、索引越界1 动态数组2 静态数组3 多维数组5 指针…

【回答问题】ChatGPT上线了!给我推荐20个比较流行的深度学习模型

目录给我推荐20个比较流行的nlp模型给我推荐20个比较流行的计算机视觉模型给我推荐20个比较流行的图像分类模型给我推荐20个比较流行的人脸识别模型给我推荐20个比较流行的实体识别模型给我推荐20个比较流行的语言识别模型给我推荐20个比较流行的激光雷达3D点云模型给我推荐20个…