theme: channing-cyan

lambda表达式

是什么?

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

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

kotlin 可变参数 传递给java kotlin传递函数_List

// 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 }
  1. 如果一个函数的最后一个参数是一个函数类型的话, 则可以
// 可以从这样
doSomeThing(10, 20, { a, b -> a + b }) // ==>
// 变成下面这样
doSomeThing(10, 20) { a, b -> a + b }
  1. lambda的函数体可以有多行, 默认最后一行是返回值
val sum = {x: Int, y: Int -> 
    println("x = ${x}, y = ${y}")
    x + y
}

lambdakotlinjava中的区别

javalambda 使用外部局部变量需要添加 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")

kotlin 可变参数 传递给java kotlin传递函数_开发语言_02

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

kotlin 可变参数 传递给java kotlin传递函数_开发语言_03

成员引用

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

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

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

kotlin 可变参数 传递给java kotlin传递函数_android_04

Person 限定了它在哪个类, :: 表示取引用, age 表示目标

// 这就是拿到 Person 类 name 属性的 偏移地址
val refName: KMutableProperty1<Person, String> = Person::name

然后你会发现 import kotlin.reflect.KMutableProperty1reflect , 是反射包里面的类型

所以我们操作 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)

解决方法也很简单, 使用 javastream 或者 使用 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 的应该都知道, 这就是那个的分组

flatMapflatten 处理嵌套集合中的元素

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. 数列操作是一个元素一个元素的执行的, 一个元素执行一系列函数完毕后留下, 切换另一个元素, 而数列的操作是一个集合一个集合的操作, 一个函数执行完毕留下一个中间集合, 然后传递到下一个函数, 在进行操作

kotlin 可变参数 传递给java kotlin传递函数_kotlin 可变参数 传递给java_05

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

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 }

kotlin 可变参数 传递给java kotlin传递函数_kotlin_06

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

kotlin 可变参数 传递给java kotlin传递函数_kotlin_07

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

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

kotlin 可变参数 传递给java kotlin传递函数_android_08

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

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

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

带接收者的 lambda 可以用在 dsl

kotlin 可变参数 传递给java kotlin传递函数_kotlin 可变参数 传递给java_09

kotlin 可变参数 传递给java kotlin传递函数_android_10

不过这些都是后话了

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

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

kotlin 可变参数 传递给java kotlin传递函数_List_11

kotlin 可变参数 传递给java kotlin传递函数_kotlin_12

函数式接口和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 的代码就会报错

kotlin 可变参数 传递给java kotlin传递函数_List_13

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