theme: channing-cyan

lambda表达式

是什么?

答: 在kotlin中是一种用{}限定作用域, 以 -> 区分参数和函数体的表达式, 叫 lambda表达式

其本质是代码块, 你也可以理解成可调用的函数类型对象(但根据反编译发现其实不是, 它的实现方式有很多. 比如: 生成一个函数, 然后传递函数引用等等, 方式还挺多)

image.png

// 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函数对象调用 函数

有什么优缺点?

优点:

  1. 代码比较简洁
  2. lambda 带来的参数捕获, 很便利, 如果用的好 lambda 用习惯了, 匿名对象的方式反而不想用了(object : InterfaceName { override xxxxx })

缺点:

  1. 代码可读性较差(用习惯了, 反而比较简单)

  2. 使用 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 在使用的过程中的几点问题:

  1. 如果只有一个参数的函数参数类型, 可以直接用 it 代替. 比如: 上面案例中的 selector: (T) -> R 参数只有一个 T, 所以T在使用的时候可以用 it 代替, 所以在使用的时候可以直接:

    list.maxBy { it }
    
  2. 如果一个函数的最后一个参数是一个函数类型的话, 则可以

    // 可以从这样
    doSomeThing(10, 20, { a, b -> a + b }) // ==>
    // 变成下面这样
    doSomeThing(10, 20) { a, b -> a + b }
    
  3. 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")

image.png

注意在 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 所能访问的所有变量

image.png

成员引用

这一章节可以当作 c++ 的 & 引用 来学习

成员引用是一个取值的过程, 类似于定义一个引用, 引用指向了 目标 的地址(静态成员拿到的是偏移地址)

:: 引用操作符可以对 成员属性/成员函数/扩展函数/扩展属性/顶层属性/顶层函数/类 等 使用

image.png

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

不管我试了几次, 都是这样的情况, 也许是对象不够??? 才会导致惰性不行???

我感觉并不是, 使用惰性的方式可能效率没提高多少, 但在节省内存方面应该是显著

但不管怎样, 手动编码方式效率还是最高的

数列的中间和末端操作

中间操作始终是惰性的, 末端操作能够触发所有惰性操作的延迟时间, 惰性操作直接开始执行

数列和集合的区别

  1. 数列操作是一个元素一个元素的执行的, 一个元素执行一系列函数完毕后留下, 切换另一个元素, 而数列的操作是一个集合一个集合的操作, 一个函数执行完毕留下一个中间集合, 然后传递到下一个函数, 在进行操作

image.png

比如上图, 明显两个的源码大意是:

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)

一个打完要等别人, 一个打完直接去做下一项

  1. 集合需要注意调用函数的顺序, 数列不用
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 }

image.png

这种区别, 不用我多说看图就懂(看不懂的, 回小学学习去) 1.gif

你过滤的越多, 后续集合越少, 效率越高

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

image.png

然后下面那个函数的代码会变成这样:

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

上面这段代码会报错

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O7xjTQuU-1656299616129)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/914605fcb4b04d7aacf622d049714567~tplv-k3u1fbpfcp-watermark.image?)]

问题也很明朗, 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..

  1. 在定义处不用管其中隐藏的 this, 按照普通的函数类型使用就好() -> StringBuilder
  2. 在调用处, 就需要stringBuilder.appendExcel2()或者appendExcel2(stringBuilder)这样传入 this 对象本体

带接收者的 lambda 可以用在 dsl 中

image.png

image.png

不过这些都是后话了

他的使用场景很多, 必须掌握

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 指针的类型

image.png

image.png

函数式接口和SAM转换

本章主要探讨

  1. kotlin 如何调用 java 的函数式接口

  2. SAM转换

  3. kotlin 自己的函数式接口

  4. 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")
   }
}

小括号没了

使用这种方式有前提:

  1. kotlin 版本在 1.4 之后

    1.4 才有的函数式接口fun interface ClassName功能好像, 至于SAM便捷功能我也没试过, 应该不会还有人没升级到 kotlin 1.6 吧?

  2. 接口需要需要被 kotlin 认为是 函数式接口

    怎么个认为法呢?

    我们给 JavaInterface.java 的接口添加了一个新的 abstract 函数

    public interface JavaInterface {
       void doSomething(String str);
       void doSomething02(String str);
    }
    

    接着 kotlin 的代码就会报错

    image.png

    提示你要变成 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优化

更多推荐