惰性集合操作:序列
map 和 filter 函数在集合上执行时,会及早创建中间集合。这意味着每一步的中间结果都被存储在一个临时列表中。例如,先 filter 再 map,会先生成一个过滤后的列表,再生成映射后的列表。当元素很多时,这种方式非常低效,占用额外内存且增加 GC 压力。
序列(Sequence)给了你执行这些操作的另一种选择,可以避免创建这些临时中间对象。

filter 和 map 都会返回一个列表,这意味着上图的链式调用会创建两个列表:一个保存 filter 函数的结果,一个保存 map 的结果。而序列则不同。

上图没有创建任何用于存储元素的中间集合。Kotlin 惰性集合操作的入口就是 Sequence 接口,这个接口表示一个可以逐个列举元素的元素序列,它只提供了一个方法 iterator,用来从序列中获取值。这个接口的强大之处在于其操作的实现方式,序列中的元素求值是惰性的。用序列可以更高效地执行链式操作,而不用创建额外的集合来保存过程中产生的中间结果。
可以调用扩展函数 asSequence 把任意集合转换成序列,调用 toList 来做反向的转换。为什么要把序列转换回集合?如果只需要迭代序列中的元素,可以直接使用序列。如果要使用其他 API 方法,比如用下标访问元素,那么就需要把序列转换成列表。
执行序列操作:中间和末端操作
序列操作分为两类:中间的和末端的。一次中间操作返回的是另一个序列,这个新序列知道如何变换原始序列中的元素。而一次末端操作返回的是一个结果,这个结果可能是集合、元素、数字,或者其他从初始集合的变换序列中获取的任意对象。
在上面的图中我们可以看出 map 和 filter 函数是中间操作,而 toList 是末端操作。上方的例子去掉 toList 后,它们不会进行任何操作,因为 map 和 filter 函数的变换被延期了,他们只有在获取结果的时候才会被调用。只有末端操作会触发执行所有的延期计算。
对序列来说,所有操作是按顺序应用在每一个元素上:处理完第一个元素(先映射再过滤),然后完成第二个元素的处理,以此类推。这种方法意味着如果在轮到它们之前就已经取得了结果,那部分元素根本不会发生任何变换。例子:一个 map 加上一个 find,当 find 找到符合条件的元素后,后面的元素无需继续完成操作。而同样的操作应用在集合上时,那么 map 的结果首先被求出来,然后满足判断式的一个元素会被找出来。惰性方法意味着你可以跳过处理部分元素。
在集合上执行操作的顺序也会影响性能,filter 和 map 之间,先应用 filter 有助于减少变换的总次数。
性能考量与适用场景
虽然序列提供了惰性求值的优势,但并非在所有场景下都优于集合。对于小型集合,创建 Iterator 对象的开销可能超过节省的内存收益。此外,序列不支持随机访问(如通过索引获取元素),也不支持某些需要多次遍历的操作。因此,建议在处理大数据流、复杂链式过滤或仅需单次遍历的场景下优先使用序列。
创建序列
- 在集合上调用
asSequence
generateSequence
给定序列中的前一个元素,这个函数会计算出下一个元素
生成并使用自然数序列(计算 100 以内所有自然数之和)
val sum = generateSequence(1) { it + 1 }
.takeWhile { it <= 100 }
.sum()
println(sum)
创建并使用父目录的序列 如果元素的父元素和它的类型相同(比如人类或者 java 文件),你可能会对它所有祖先组成的序列的特质感兴趣。
val file = File("path/to/file.txt")
val ancestors = generateSequence(file) { it.parentFile }
上图可以查询文件是否放在隐藏目录中,通过创建一个其父目录的序列并检查每个目录的属性实现。通过提供第一个元素和获取每个后续元素的方式来实现,把 any 换成 find,还可以得到想要的那个目录(对象)。注意!使用序列允许你找到需要的目录之后立即停止遍历父目录。
使用 Java 函数式接口
Kotlin 的 lambda 可以无缝和 Java API 互操作。以 onClickListener 为例。

在 Java 中使用匿名内部类的写法在 Kotlin 中可以变成上图写法。这种方法可以工作的原因是 OnClickListener 接口只有一个抽象方法。这种接口被称为函数式接口,或者 SAM 接口,SAM 代表单抽象方法。Java API 中随处可见像 Runnable 和 Callable 这样的函数式接口,以及支持它们的方法。Kotlin 允许你在调用接受函数式接口作为参数的方法时使用 lambda。
Kotlin 拥有完全的函数类型,需要接受 lambda 作为参数的 Kotlin 函数应该使用函数类型而不是函数式接口类型,作为这些参数的类型。
把 lambda 当作参数传递给 Java 方法
可以把 lambda 传给任何期望函数式接口的方法 如:在 Java 中,void postponeComputation(int delay, Runnable computation) 这个函数在 Kotlin 中可以调用它并把一个 lambda 作为实参传给它,编译器会自动把它转换成一个 Runnable 的实例。
postponeComputation(1000) { println(42) }
注意!当我们说一个 Runnable 的实例时,指的是一个 实现了 Runnable 接口的匿名类的实例。编译器会帮你创建并使用 lambda 作为单抽象方法,在这里是 run 方法的方法体。
通过显式创建一个实现了 Runnable 的匿名对象也能达到同样的效果。
postponeComputation(1000, object : Runnable {
override fun run() {
println(1)
}
})
但是 当你显式声明对象时,每次调用都会创建一个新的实例。使用 lambda 不一样,如果 lambda 没有访问任何来自定义它的函数的变量,相应的匿名类实例可以在多次调用之间重用。
如果 lambda 从包围它的作用域中捕捉了变量,每次调用就不能重用同一个实例了,这种情况下每次调用时编译器都要创建一个新对象,其中存储着被捕捉的变量的值。lambda 表达式在 Kotlin 1.0 中会被编译成一个匿名类,除非它是一个内联 lambda。如果它捕捉了变量,每个被捕捉的变量会在匿名类中有对应的字段,而且每次对 lambda 的调用都会创建一个这个匿名类的新实例。但是对集合使用扩展方法的方式并不适用,内联函数也是。
SAM 构造方法:显式地把 lambda 转换成函数式接口
SAM 构造方法是编译器生成的函数,让你执行从 lambda 到函数式接口实例的显式转换,可以在编译器不会自动应用转换的上下文中使用它。比如有一个方法返回的是一个函数式接口的实例,不能直接返回一个 lambda,要用 SAM 方法包装起来。
使用 SAM 构造方法来返回值。

SAM 构造方法的名称和底层函数式接口的名称一样,它只接收一个参数——一个被用作函数式接口单抽象方法体的 lambda,并返回实现了这个接口的类的一个实例。除了返回值外,SAM 构造方法还可以用在需要把 lambda 生成的函数式接口实例存储在一个变量中的情况。
使用 SAM 构造方法来重用 listener 实例。

Lambda 和添加/移除监听器。Lambda 内部没有匿名对象那样的 this,没有办法引用到 lambda 转换成的匿名类实例。从编译器角度看,lambda 是一个代码块,不是一个对象,也不能把它当成对象引用,lambda 中的 this 引用指向包围它的类。如果你的事件监听器在处理事件时还需要取消它自己,不能使用 lambda 这样做,这种情况使用实现了接口的匿名对象,在匿名对象内,this 关键字指向该对象实例,可以把它传给移除监听器的 API。
尽管方法调用中的 SAM 转换一般都自动发生,但是当把 lambda 作为参数传给一个重载方法时,也有编译器不能选择正确的重载的情况,此时使用显式 SAM 构造方法是一个好方法。
带接收者的 lambda:with 和 apply
with 函数
很多语言都有对同一个对象执行多次操作而不需要反复把对象的名称写出来的语句,在 Kotlin 中我们使用 with 函数实现。
使用 with 函数构建字母表。

with 实际上是一个接受两个参数的函数,这里的两个参数分别是 StringBuilder 和一个 lambda,这里利用了 lambda 若在参数列表的最后一位可以放在括号外的约定。with 函数把它的第一个参数转换成作为第二个参数传给它的 lambda 的接收者,可以显式地通过 this 引用来访问这个接收者,也可以省略。
带接收者的 lambda 和扩展函数。一个扩展函数某种意义上来说就是带接收者的函数。Lambda 是一种类似普通函数的定义行为的方式,而带接收者的 lambda 是类似扩展函数的定义行为的方式。
方法名称冲突。如果你当作参数传给 with 的对象已经有这样的方法,该方法的名称和你正在使用 with 的类中的方法一样。此时可以给 this 引用加上显式的标签来表明你要调用的是哪个方法。比如函数 alphabet 是类 OuterClass 的一个方法,如果你 想引用的是定义在外部类的 toString 方法而不是 StringBuilder,可以使用 [email protected]()。
with 返回的值是执行 lambda 代码的结果,该结果就是 lambda 中最后一个表达式的值,但有时候我们想返回的是接收者对象,此时依靠 apply 函数。
apply 函数
它和 with 函数几乎一样,唯一的区别是 apply 始终会返回作为实参传递给它的对象,也就是接收者对象。
重写 alphabet 方法。

它被声明成一个扩展函数,它的接收者变成了作为实参的 lambda 的接收者。执行 apply 的结果是一个 StringBuilder,我们可以用 toString 把它转换成 String。
许多情况下 apply 都很有效,其中一种是在 创建一个对象实例并需要用正确的方式初始化它的一些属性的时候。在 Java 中通常通过另一个单独的 Builder 对象来完成,而在 Kotlin 中,可以在任意对象上使用 apply,完全不需要任何来自定义该对象的库的支持。
使用 apply 初始化一个 TextView。
fun createTextView(context: Context): TextView {
return TextView(context).apply {
text = "Hello World"
textSize = 20f
setTextColor(Color.BLACK)
}
}
TextView 省略了变量的命名,在 alphabet 中我们可以看到变量 StringBuilder 是可以省略的,TextView 实例被创建后立即传给了 apply,在传给 apply 的 lambda 中,TextView 实例变成了 lambda 的接收者,然后我们就可以调用它的方法设置它的属性。lambda 之性质或返回已经初始化过的接收者实例,变成了 createTextView 的返回结果。
总结
本文深入探讨了 Kotlin 中的 Lambda 编程特性。通过序列(Sequence)实现了高效的惰性计算,避免了不必要的中间集合创建。在与 Java 交互时,利用 SAM 转换简化了函数式接口的使用,同时理解了 lambda 捕获变量的内存影响。最后,通过 with 和 apply 函数展示了带接收者 lambda 的强大之处,特别是在对象初始化场景下的简洁性。掌握这些特性有助于编写更优雅、高效的 Kotlin 代码。