theme: channing-cyan
lambda
表达式
是什么?
答: 在kotlin
中是一种用{}
限定作用域, 以 ->
区分参数和函数体的表达式, 叫 lambda表达式
其本质是代码块, 你也可以理解成可调用的函数类型对象(但根据反编译发现其实不是, 它的实现方式有很多. 比如: 生成一个函数, 然后传递函数引用等等, 方式还挺多)
// val funcType: (Int, Int) -> Int = {x, y -> x + y}
val funcType: (Int, Int) -> Int = {x: Int, y: Int -> x + y}
val sum01 = funcType.invoke(1, 2)
// 简略成这样:
val sum02 = funcType(1, 2)
// {x: Int, y: Int -> x + y}(1, 2)
{x: Int, y: Int -> x + y}.invoke(1, 2)
其中 {x: Int, y: Int -> x + y}
就是lambda表达式
funcType.invoke(1, 2)
和 {x: Int, y: Int -> x + y}.invoke(1, 2)
表示 lambda函数对象调用 函数
有什么优缺点?
优点:
- 代码比较简洁
- lambda 带来的参数捕获, 很便利, 如果用的好 lambda 用习惯了, 匿名对象的方式反而不想用了(
object : InterfaceName { override xxxxx }
)
缺点:
- 代码可读性较差(用习惯了, 反而比较简单)
- 使用
lambda
有些情况下需要注意this
, 有时候没有, 但有些时候又有(这在后面有解释)
究其原因是大家把 lambda 和 匿名对象 做了比较, 其实他们还是有区别的, 匿名 new 出来的对象, 有一个 this 指向的是该对象自己, 而 lambda 则没有, lambda的this通常都是捕获的外部作用域的 this , 如果 外部作用域没有 this (比如lambda写在顶层函数内部, this是没有的), 则 lambda 就没有 this
怎么用? 有什么应用场景?
最主要的用法用于参数传递, 一般是函数参数为 函数类型, 我们传递个lambda表达式过去
下面是我效仿 maxBy
函数写的
fun <T, R : Comparable<R>> List<T>.maxBy(selector: (T) -> R): R? = listIterator().run {
if (!hasNext()) return null
val maxElement = next()
var maxVal = selector(maxElement)
while (hasNext()) {
val nextElement = next()
val nextVal = selector(nextElement)
if (nextVal > maxVal) {
maxVal = nextVal
}
}
return maxVal
}
而调用方式
val list: List<Int> = listOf(100, 2, 3, 400, 59999, 66, 700)
// println(list.maxBy({ number -> number }))
// println(list.maxBy { number -> number })
println(list.maxBy { it })
首先我们需要注意 lambda
在使用的过程中的几点问题:
- 如果只有一个参数的函数参数类型, 可以直接用
it
代替. 比如: 上面案例中的selector: (T) -> R
参数只有一个T
, 所以T
在使用的时候可以用it
代替, 所以在使用的时候可以直接:
list.maxBy { it }
- 如果一个函数的最后一个参数是一个函数类型的话, 则可以
// 可以从这样
doSomeThing(10, 20, { a, b -> a + b }) // ==>
// 变成下面这样
doSomeThing(10, 20) { a, b -> a + b }
lambda
的函数体可以有多行, 默认最后一行是返回值
val sum = {x: Int, y: Int ->
println("x = ${x}, y = ${y}")
x + y
}
lambda
在kotlin
和java
中的区别
在java
中 lambda
使用外部局部变量需要添加 final 修饰
, 但在 kotlin
中则不需要, 在 kotlin
中这种情况的变量的值可以改变, 在kotlin
中不会仅限于访问 final 变量
, 在kotlin
内部可以修改该变量
val list = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
var odd = 0
var even = 0
list.forEach {
if (0 == it % 2) {
even++
} else {
odd++
}
}
println("单数: $odd, 双数: $even")
注意在
lambda
的代码未必马上执行, 比如:button.onClink { i++ }
该函数只有在触发事件时才会i++
kotlin
支持lambda
内部修改变量的实现方式是:
将 lambda
捕获的变量全部进行包装, 比如 在 java
中可以把对象改成成 AtomicInteger
等类似这种的对象, 然后将值存放在 AtomicInteger
内部, 这样即使 lambda
捕获了 AtomicInteger
对象, 也仅仅捕获的是 引用 该引用也被添加了 final
, 而我们修改的是引用背后的值, 所以可以实现在 lambda
内部修改值外部也能被修改的功能
kotlin
也用的这种方式, 不过它存放的不是 AtomicXXXXX
系列的类, 而是一个叫 Ref
的类
闭包
前面的章节说过, 闭包就是带着外部作用域的函数, 但可以保证外部函数无法使用函数内部的变量, lambda
也是
在我看来, lambda
这种方式巧妙的地方在于, 定义lambda
和调用lambda
两方
在 定义方, 只要是在定义 lambda
之前的变量都可以在 lambda
里面无条件使用(说白了, 就是lambda
捕获了外部类的引用)
由于
lambda
会捕获外部类的引用, 所以需要注意 串行化 的问题
在 调用方, lambda
参数贯穿了调用方的作用域, 只要调用方往里头传递参数, 那么lambda
就可以使用调用放的部分变量, 如果调用方传递的是 this
, 那么lambda
同时掌握了两方的 this
所能访问的所有变量
成员引用
这一章节可以当作 c++ 的
&
引用 来学习
成员引用是一个取值的过程, 类似于定义一个引用, 引用指向了 目标 的地址(静态成员拿到的是偏移地址)
::
引用操作符可以对 成员属性/成员函数/扩展函数/扩展属性/顶层属性/顶层函数/类 等 使用
Person
限定了它在哪个类, ::
表示取引用, age
表示目标
// 这就是拿到 Person 类 name 属性的 偏移地址
val refName: KMutableProperty1<Person, String> = Person::name
然后你会发现 import kotlin.reflect.KMutableProperty1
是 reflect
, 是反射包里面的类型
所以我们操作 refName
更会和反射产生联系
成员引用可以和 lambda
互换使用
这里就大概说了说他的用法, 抽空专门搞个章节学学
集合和lambda
val list = listOf(Person("haha", 22), Person("xixi", 21), Person("dd", 23))
list.maxBy(Person::age)
println(list.filter { it.age > 22 }.map(Person::name))
List
集合的标准函数看起来简单, 使用时不注意可能让程序效率更慢, 就像上面的两个函数,maxBy
底层遍历了一轮, 而 下面的那行代码, 程序执行了 两轮 遍历, 程序员看到的仅仅是一行代码
下面那段代码让程序员手动写效率会更高
println(list.filter { it.age > 22 }.map(Person::name))
var nameList: MutableList<String> = mutableListOf()
list.forEach {
if (it.age > 22) {
nameList.add(it.name)
}
}
println(nameList)
解决方法也很简单, 使用
java
的stream
或者 使用kotlin
的序列
, 后续会介绍
all
any
count
find
对集合应用的判断
-
all
, 都是判断集合中所有的元素是否满足条件, 只要有一个不满足直接返回false
, 否则返回true
-
any
判断集合中是否存在至少一个满足条件的, 如果满足返回true
, 否则返回false
-
count
判断集合内有几个满足条件判断的 -
find
查找集合内第一个满足条件的元素
val list = listOf(1, 2, 3)
val all = list.all { it > 2 }
println(all)
val any = list.any { it > 2 }
println(any)
val count = list.count { it >= 2 }
println(count)
val find = list.find { it == 2 }
println(find)
groupBy
分组
val map = list.groupBy { it.name.first() }
for (entry in map.entries) {
println("key = ${entry.key}, value = ${entry.value}")
}
key = h, value = [Person{name = heihei, age = 34}, Person{name = haha, age = 22}, Person{name = hoho, age = 23}]
key = z, value = [Person{name = zhazha, age = 23}]
key = d, value = [Person{name = dd, age = 12}]
学过 sql
的应该都知道, 这就是那个的分组
flatMap
和 flatten
处理嵌套集合中的元素
flat
: 铺平
private val list = listOf(
Book("k language", listOf("zhazha", "haha", "xixi", "heihei", "hoho")),
Book("v language", listOf("zhazha", "haha", "heihei", "hoho")),
Book("l language", listOf("zhazha", "haha", "xixi", "heihei")),
Book("j language", listOf("zhazha", "haha", "xixi", "hoho"))
)
val map = list.flatMap { it.title.toList() }
map.forEach {
print("$it")
}
flat map
的功能是从一堆杂物(对象)里挑选一块或者很多块砖头(属性或者集合属性), 把他们分堆, 然后铺平, 最后连接在一起, 这就是 flat map
的功能
flatten 函数的功能和上面的差不多, 不过它面对的是 List<List<String>>
这种方式的
惰性集合操作: 序列
list.asSequence().filter { it.age > 60 }.map { it.name }.toList().forEach {println(it)}
它是惰性的, 他避免了 filter
时创建的临时对象, 还有 map
计算时的临时对象, 它借助 Iterator
来实现惰性操作
但在实际操作中, 我没看出来它有多快速
// 加载对象
val list = mutableListOf<Person>()
for (i in 1..10000) {
list.add(Person(UUID.randomUUID().toString(), Random.nextInt() % 150))
}
// lambda正常方式
var start = Instant.now()
list.filter { it.age > 60 }.map { it.name }
var duration = Duration.between(start, Instant.now())
println()
println(duration.toMillis())
// 惰性方式
start = Instant.now()
list.asSequence().filter { it.age > 60 }.map { it.name }
duration = Duration.between(start, Instant.now())
println()
println(duration.toMillis())
// 手动写代码方式
start = Instant.now()
val mutableList = mutableListOf<String>()
list.forEach {
if (it.age > 60) {
mutableList.add(it.name)
}
}
duration = Duration.between(start, Instant.now())
println()
println(duration.toMillis())
20 17
34 22
3 4
不管我试了几次, 都是这样的情况, 也许是对象不够??? 才会导致惰性不行???
我感觉并不是, 使用惰性的方式可能效率没提高多少, 但在节省内存方面应该是显著
但不管怎样, 手动编码方式效率还是最高的
数列的中间和末端操作
中间操作始终是惰性的, 末端操作能够触发所有惰性操作的延迟时间, 惰性操作直接开始执行
数列和集合的区别
- 数列操作是一个元素一个元素的执行的, 一个元素执行一系列函数完毕后留下, 切换另一个元素, 而数列的操作是一个集合一个集合的操作, 一个函数执行完毕留下一个中间集合, 然后传递到下一个函数, 在进行操作
比如上图, 明显两个的源码大意是:
listOf(1, 2, 3, 4).map { it * it }.find { it > 3 }
listOf(1, 2, 3, 4).asSequence().map { it * it }.find { it > 3 }
左边就跟学校一个班级一个班级的学生去打疫苗一样, 这些学生去打第一针(map), 等全班学生都打完第一针后, 再去验验都有谁产生抗体了(find)
右边就跟社会人去打针一样, 预约拿号, 排队打疫苗(map), 打完疫苗后, 不用等别人, 直接去验下是否产生了抗体(find)
一个打完要等别人, 一个打完直接去做下一项
- 集合需要注意调用函数的顺序, 数列不用
listOf(1, 2, 3.0, 4).map{ it * it }.filter{ it is Double }
listOf(1, 2, 3.0, 4).filter { it is Double }.map{ it * it }
这种区别, 不用我多说看图就懂(看不懂的, 回小学学习去)
你过滤的越多, 后续集合越少, 效率越高
lambda的实现方式
fun postponeComputation(id: Int, runnable: () -> Unit) {
println("id = $id")
runnable()
}
fun handleComputation(id: String) {
postponeComputation(1000) { println(id) }
}
lambda
本质上是可以传递给其他函数的一小段代码, 我们可以把它当作一个匿名函数的引用 + 函数(函数参数列表和函数体), 该函数引用可以当作参数传递
按照上面的编码情况, 我们显示查看底层的过程
fun postponeComputation(id: Int, runnable: Function0<Unit>) {
println("id = $id")
runnable()
}
然后下面那个函数的代码会变成这样:
fun handleComputation(id: String) {
postponeComputation(1000, object : Function0<Unit> {
val id = id
fun void invoke(): Unit {
println(this.id)
}
})
}
实际上代码可能不是这样写的, 但主要思想差不多
lambda this
和 匿名对象this
的探讨
fun postponeComputation(id: Int, runnable: () -> Unit) {
println("id = $id")
runnable()
}
fun handleComputation() {
postponeComputation(1000) {
// 这里直接报错
println(this) // error
}
postponeComputation(1999, object : Function0<Unit> {
override fun invoke() {
println(this)
}
})
}
上面这段代码会报错
问题也很明朗, handleComputation
函数是静态的, 所以根本没有 this
, 但是下面的 postponeComputation(1999, object : Function0<Unit>
的 this
能够使用且指向的对象是 匿名对象本身
但是如果代码这样写,
class LambdaRealizationDemo01 {
fun handleComputation() {
postponeComputation(1000) {
// 没有报错
println(this)
}
postponeComputation(1999, object : Function0<Unit> {
override fun invoke() {
println(this)
}
})
}
}
id = 1000
lambda09.LambdaRealizationDemo01@6108b2d7
id = 1999
lambda09.LambdaRealizationDemo01$handleComputation$2@13969fbe
可以很直接的看出来, 俩 this
指向的对象根本不一样, lambda
在一些使用场景特别要注意 this
到底指向的是谁?
下面代码是 java
源码:
public final class LambdaRealizationDemo01 {
public final void handleComputation() {
LambdaRealizationDemo01Kt.postponeComputation(1000, (Function0<Unit>)((Function0)new Function0<Unit>(this){
final /* synthetic */ LambdaRealizationDemo01 this$0;
{
this.this$0 = $receiver;
super(0);
}
public final void invoke() {
LambdaRealizationDemo01 lambdaRealizationDemo01 = this.this$0;
boolean bl = false;
System.out.println(lambdaRealizationDemo01);
}
}));
LambdaRealizationDemo01Kt.postponeComputation(1999, (Function0<Unit>)((Function0)new Function0<Unit>(){
public void invoke() {
boolean bl = false;
System.out.println(this);
}
}));
}
}
仔细看这俩的区别:
LambdaRealizationDemo01Kt.postponeComputation(1000, (Function0<Unit>)((Function0)new Function0<Unit>(this)
LambdaRealizationDemo01Kt.postponeComputation(1999, (Function0<Unit>)((Function0)new Function0<Unit>()
结论:
lambda有个功能叫捕获
, 它会捕获外部作用域的一些变量
, 在上面的例子中, 该lambda
捕获了外部函数作用域中的 this
, 而该函数作用域的 this
就是 LambdaRealizationDemo01
对象
★★★带接收者的 lambda
带接收者的 lambda
函数
fun buildString(builderAction: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.builderAction()
return sb.toString()
}
fun main() {
val s = buildString {
append("Hello ")
append("World!!!")
}
println(s)
}
fun buildString(builderAction: StringBuilder.() -> Unit): String {
return StringBuilder().apply { builderAction() }.toString()
}
带接收者的函数类型
val StringBuilder.appendExcel1: StringBuilder
get() = this.append("01!")
val appendExcel2: StringBuilder.() -> StringBuilder = { this.append("02!") }
fun main() {
val stringBuilder = StringBuilder("Hi")
stringBuilder.appendExcel1
// 这里直接调用了
stringBuilder.appendExcel2()
println(stringBuilder)
}
带接收者的函数类型很好理解, 把它当作类型返回就行 ,
StringBuilder.() -> StringBuilder
, 当作普通 类型 就行, 然后在参数前面加上接收者StringBuilder.
.
- 在定义处不用管其中隐藏的
this
, 按照普通的函数类型使用就好() -> StringBuilder
- 在调用处, 就需要
stringBuilder.appendExcel2()
或者appendExcel2(stringBuilder)
这样传入this
对象本体
带接收者的 lambda
可以用在 dsl
中
不过这些都是后话了
他的使用场景很多, 必须掌握
fun main() {
val yesterday: LocalDateTime = 1.days.ago
println(yesterday)
val tomorrow: LocalDateTime = 1.days.formNow
println(tomorrow)
}
private val Period.formNow: LocalDateTime
get() = LocalDateTime.now() + this
private val Int.days: Period
get() = Period.ofDays(this)
private val Period.ago: LocalDateTime
get() = LocalDateTime.now() - this
这里我再次总结下在 kotlin 里什么是扩展.
扩展是一种提供, 也是一种限定, 它为我们的属性, 函数和函数类型提供了使用 this
指针的可能, 但同时也限定了 this
指针的类型
函数式接口和SAM转换
本章主要探讨
kotlin
如何调用java
的函数式接口SAM
转换kotlin
自己的函数式接口kotlin
函数式接口的特性
我们定义一个 java
的函数式接口
JavaInterface.java
public interface JavaInterface {
void doSomething(String str);
}
接着我们创建属于 kotlin
的函数
KotlinInterfaceDemo.kt
fun delegateWork(j: JavaInterface) {
val str = "helloWorld"
j.doSomething(str)
}
fun main() {
delegateWork(object : JavaInterface{
override fun doSomething(str: String?) {
println("content = $str")
}
})
}
至于很多人说的
java
中函数式接口还可以使用上@FunctionalInterface
注解, 这个注解对于kotlin
来说有跟没有一样,kotlin
有自己的判断方式
SAM转换(Single Abstract Method Conversions)
当 java
的 接口 被 kotlin
认为是函数式接口, kotlin
还提供了一种便捷的方式: SAM构造函数
fun main() {
delegateWork(JavaInterface {
println("content = $it")
})
}
甚至还可以这样:
fun main() {
delegateWork({
println("content = $it")
})
}
JavaInterface
名字没了
最终变成这样:
fun main() {
delegateWork {
println("content = $it")
}
}
小括号没了
使用这种方式有前提:
kotlin
版本在1.4
之后
1.4 才有的函数式接口fun interface ClassName
功能好像, 至于SAM
便捷功能我也没试过, 应该不会还有人没升级到kotlin 1.6
吧?- 接口需要需要被
kotlin
认为是 函数式接口
怎么个认为法呢?
我们给JavaInterface.java
的接口添加了一个新的abstract
函数
public interface JavaInterface {
void doSomething(String str);
void doSomething02(String str);
}
接着 kotlin
的代码就会报错
提示你要变成 object:
的方式
fun main() {
delegateWork(object : JavaInterface {
override fun doSomething(str: String?) {
TODO("Not yet implemented")
}
override fun doSomething02(str: String?) {
TODO("Not yet implemented")
}
})
}
现在我们可以不用关注 JavaInterface.java
代码了
创建一个 新的文件 KotlinInterface.kt
interface KotlinInterface {
fun doSomething(str: String)
}
fun delegateWork(k: KotlinInterface) {
val str = "Hello World"
k.doSomething(str)
}
fun main() {
delegateWork(object : KotlinInterface {
override fun doSomething(str: String) {
println("str = $str")
}
})
}
此时无法使用 SAM 转换功能, 需要再改
fun interface KotlinInterface {
fun doSomething(str: String)
}
给我们的接口添加 fun
此时我们的代码变成这样:
fun main() {
delegateWork(KotlinInterface {
println("str = $it")
})
}
接着还能省略接口名字和小括号:
fun main() {
delegateWork {
println("str = $it")
}
}
接着我们知道, 函数式接口还能优化
怎么说呢?
函数式接口可以变成 函数类型
fun delegateWork(k: (String) -> Unit) {
val str = "Hello World"
k(str)
}
接着我们知道, kotlin
函数类型参数可以使用inline
关键字优化
inline fun delegateWork(k: (String) -> Unit) {
val str = "Hello World"
k(str)
}
这才是我们的最终版本
= “Hello World”
k.doSomething(str)
}fun main() {
delegateWork(object : KotlinInterface {
override fun doSomething(str: String) {
println(“str = $str”)
}
})
}
此时无法使用 SAM 转换功能, 需要再改
```kotlin
fun interface KotlinInterface {
fun doSomething(str: String)
}
给我们的接口添加 fun
此时我们的代码变成这样:
fun main() {
delegateWork(KotlinInterface {
println("str = $it")
})
}
接着还能省略接口名字和小括号:
fun main() {
delegateWork {
println("str = $it")
}
}
接着我们知道, 函数式接口还能优化
怎么说呢?
函数式接口可以变成 函数类型
fun delegateWork(k: (String) -> Unit) {
val str = "Hello World"
k(str)
}
接着我们知道, kotlin
函数类型参数可以使用inline
关键字优化
inline fun delegateWork(k: (String) -> Unit) {
val str = "Hello World"
k(str)
}
这才是我们的最终版本
而上面的 SAM
也好, 函数式接口也好, 都需要 new
一个对象, 而且也不能用 inline
优化