函数类型:
- 自定义类型、基本类型都是类型,函数也能抽象出类型,叫函数类型。既然函数可以抽象成一个类型,那么函数就可以作为变量 赋值/传参 了。
- ->左边的部分用来声明该函数接收什么参数的,多个参数之间用逗号隔开,如果没有参数直接使用()表示就可以了;->右边表示该函数的返回值是什么类型,如果没有返回值直接使用
Unit
即可。匿名函数、具名函数:在定义时是否指定函数名字。
函数字面值:一段代码,本身没有名字,我们可以把它绑定到一个变量上,通过这个变量操作它,Lambda表达式和匿名函数都叫函数字面值。
Lambda:只是对匿名函数进一步简写的语法糖,本质是一个东西。Lambda 表达式始终被大括号包围,-> 右箭头用作分割,读作“goes to”。
高阶函数:参数或者返回值是函数的函数。通常不会单独定义一个Lambda玩(一般写代码都是定义普通函数),而是把Lambda作为另一个函数的参数使用,传参的时候能直接写而不是麻烦地专门写个函数再去传参(注意时间顺序,成员引用是刚好已存在满足要求的函数,而不是这里说的需要了再去写)(就像Java接口回调中你不会专门写个SAM接口实现类再创建实例去传参,而是用创建匿名内部类写法)。
函数式编程:OOP(Object Oriented Programming) 面向对象编程,FP(Functional Programming) 函数式编程。
- Java 中定义工具类方法的时候,一般把这些方法定义成类方法。在 Kotlin 中直接在文件中定义方法就行,不用类包裹了,不过编译器最后还是把它编译成了类方法。
- Java 使用 Lambda 表达式需要函数式接口,它只是一种对匿名内部类的简写作用。而 Kotlin 支持函数式编程,函数可用于 传参/赋值,不需要像 Java 那样自己写或者用系统提供的函数接口。
- Lambda 表达式是对匿名函数的进一步简写形式。
这是一个具名函数(普通函数)。
fun sum(x: Int, y: Int): Int { ... }
只有变量才能传参使用,因此赋值给变量。aa是一个函数变量,它的类型是函数类型 (Int, Int) → Int,-> 左边是参数类型,右边是返回值类型。
//aa的类型是(Int, Int) -> Int
val aa = fun sum(x: Int, y: Int): Int { ... }
函数名字此时没有作用了,去掉就变成了匿名函数。
//aa的类型是(Int, Int) -> Int
val aa = fun (x: Int, y: Int): Int { ... }
去掉fun关键字、去掉返回值类型、去掉参数列表的形参类型(编译器无法推断函数类型因此就要显示声明函数变量的函数类型了)进一步精简成Lambda。函数体中 ->左边是形参(使用的时候有的形参用不到可以使用_下划线替代来减少变量生成节约性能),右边是业务代码,函数体最后一行会被当作返回值。
val aa: (Int, Int) -> Int = { x, y -> ... }
val aa: (Int, Int) -> Int = { _, y -> println(y) } //用不到x屏蔽掉
一些语法简化:
view.setOnClickListener(fun (v: View): Unit { ... }) //匿名函数
view.setOnClickListener({ v: View -> ... }) //Lambda
view.setOnClickListener({ v -> ... }) //Lambda参数类型能被推导可以省略
view.setOnClickListener() { v -> ... } //Lambda是函数最后一个参数可以写到小括号外面
view.setOnClickListener { v -> ... } //Lambda是函数唯一参数可以去掉小括号
view.setOnClickListener { it.setVisiable(GONE) } //Lambda只有一个参数可以省去不写,调用有默认名称 it
传参的时候Lambda(即函数形参 result)并没有立即执行,而是在函数体中显式调用 result 来执行。相当于函数体提供了 result 所需形参(往Lambda中传参),result是在编写这些数据的具体操作逻辑并给出结果(返回值)。
//函数类型变量作为函数形参 result: (Int, Int) -> Int
fun show(num: Int, result: (Int, Int) -> Int): Int {
...
result(num,5) //具体执行的地方,相当于函数中提供了参数(num和5)
}
//相当于具体编写参数的操作逻辑并给出结果(返回值)
val result = {a: Int, b: Int -> a + b}
show(2, result)
针对函数对象的行为(传参、处理、返回)抽象出接口。Kotlin 定义了 kotlin.Function
接口来抽象所有的函数,它没有定义任何方法。kotlin.jvm.functions 包里定义了 Function0 到 Function22 来分别抽象无参到 22 个参数的函数,它们都继承了 kotlin.Function 接口,同时定义了一个 invoke() 函数。invoke() 函数定义了“调用”这个行为,它同时重载了括号操作符“()”,允许我们用括号来传入参数、得到返回值。
//例如Function2接口
//P1、P2是传入的两个参数的类型,R是返回值类型
//因为我们只会向函数中传入参数取出返回值,所以用in out收窄功能
interface Function2 : Function {
operator fun invoke(p1: P1, p2: P2): R
}
//对于我们写的sum函数对象
//会变编译成Function2类型的对象,(Int, Int) -> Int 是具体实现类
//这时我们就可以用invoke()来调用它
val sum: (Int, Int) -> Int = {a, b -> a + b}
println(sum.invoke(1, 2)) //打印:3
println(sum(1, 2)) //打印:3
Java 不支持函数编程(函数只能存在于类中),变通方案是接口回调(函数式接口)。抽象方法一样是定义了参数列表和返回值类型(就像Kotlin中的Lambda,多了无卵用的函数名),只是外层套了一个接口(没有函数类型的对象用来存储传递,实在需要写个类实现后创建对象),都是等实际使用的时候根据情况再编写具体内容,通过匿名内部类对象创建(Java的Lambda只是对匿名内部类简写的语法糖),而这就是SAM转换。
Java中Lambda调用SAM:
interface Cutlery{
String getCutlery(String name,int num);
}
public void eat(int type, Cutlery cutlery){ }
eat(5, (name,num) -> "餐具");
Kotlin调用Java中定义的方法(带有Java的SAM参数)时:同样支持SAM转换。底层是将Java的SAM翻译成函数类型。
//底层是将Java中的eat()翻译成函数类型
public void eat(int type, Cutlery cutlery) {}
↓
fun eat(type: Int, cutlery: (name: String, num: Int) -> String) {}
//调用
eat (5) { name, num -> "餐具" }
Kotlin调用Kotlin中定义的方法(带有Java的SAM参数)时:可以使用SAM构造和匿名内部类的方式调用。由于该方法是在Kotlin中明确定义的,因此它不是一个函数类型无法使用Lambda调用。SAM构造相对于匿名内部类创建的好处是,除了书写简便,在不使用外部变量的情况下SAM会保持一个静态对象的引用而不是每次创建对象,性能会好点。
fun eat(type: Int, cutlery: Cutlery)
//使用SAM构造调用(推荐)
eat(5, Cutlery { name, num -> "餐具" })
//使用匿名内部类调用
eat(5,object : Cutlery {
override fun getCutlery(name: String?, num: Int): String ="餐具"
})
Kotlin调用Kotlin中定义的方法(带有Kotlin的SAM参数)时:只能使用匿名内部类方式调用。既然Kotlin支持函数编程,就不希望你用Java这种SAM编程。
interface Cutlery {
fun getCutlery(name: String, num: Int):String
}
fun eat(type: Int, cutlery: Cutlery){}
//只能使用匿名内部类调用
eat(5, object : Cutlery {
override fun getCutlery(name: String, num: Int): String ="餐具"
})
需要传入函数形参时,现有的具名函数刚好满足要求(函数类型以及代码逻辑),通过成员引用将该函数转换成一个值传递它。
成员变量/函数 扩展属性/函数 |
实例名 :: 属性名/函数名 类名() :: 属性名/函数名 |
单例或伴生对象中的变量/函数 | 类名 :: 属性名/函数名 |
顶层属性/方法 | :: 属性名/函数名 (不属于任何一个类,类型省略) |
构造函数 | :: 类名 |
//这是一个具名函数
fun sum(x: Int, y: Int): Int = x + y
//具名函数加上::双冒号就变成一个函数类型的变量,只有变量才具有 传参/赋值 的功能。
//函数类型的对象才有invoke(),具名函数是没有的。
val aa = ::sum //赋值,aa的类型 KFunction2
aa(2,3) //调用,等效于 aa.invoke(2,3)
method(::sum) //传参
(::sum)(2,3) //调用,等效于 (::sum).invoke(2,3)
A.() -> B
和扩展函数的区别就像 Lambda 和普通函数的区别,可以当作扩展函数的对象化形式。表示可以在 A 类型的实例上调用该方法并返回一个 C 类型的值,在 Lambda 函数体中可以直接使用 A 的 public 成员属性或函数。
主要用于建造者模式,DSL。
- 我们只需要在一个类中使用 operator 来修饰 invoke() 函数,这样的类的对象就可以直接像一个保存 lambda 表达式的变量一样直接调用,而调用后执行的函数就是invoke函数。
- 还有另一种方式来实现可调用的对象,即让类继承自函数类型,然后重写 invoke() 方法.
class A(val str: String) {
operator fun invoke() {
println(str)
}
}
val a = A("Hello")
a()
class B : (String) -> String {
override fun invoke(str: String): String {
return str
}
}
val b = B()
println(b("Hello"))
函数(具名函数、匿名函数、Lambda)可以捕获(引用和修改)自己外部函数中的局部变量,但会使外部变量保存在内存中避免其随着外部环境的销毁,保证了变量的安全性但有性能开销。闭包赋值给变量后,变量销毁内存释放。
Java中函数不是一等公民无法嵌套声明,函数必须存在于类/接口中,使用的是函数式接口解决方案。匿名内部类类似于一个闭包,当捕捉外部的final变量时,它的值和使用这个值的Lambda代码(匿名内部类)一起存储。而对于非final变量来说,因为对外部的局部变量引用和修改无法阻止该变量随着外部环境一起销毁,这时需要把它的值封装在一个特色的容器中,对容器的引用会和Lambda代码一起存储。具体实现是外部定义长度为1的数组用arr[0]操作变量,原理是数组对象被分配到堆内存中会因为内部引用而不被销毁,通过持有该变量的引用来使得两个类可以修改同一个变量。一般都是操作成员变量(类的属性),而不是受到栈帧出栈影响的外部函数中的局部变量。
Kotlin的捕捉只不过把Java中的一些实现细节给优化了,比如捕捉val变量时,它的值会被拷贝下来,当捕捉var变量时,它的值会被作为Ref类的一个实例被保存下来。
//返回一个函数
fun method(): () -> Int {
var count = 0
return { count++ }
}
//用两个变量接收函数并分别多次调用
//每次调用都会count+1
//aa和bb数据是相互独立的
val aa= method()
val bb= method()
println(aa()) //打印:0
println(aa()) //打印:1
println(aa()) //打印:2
println(bb()) //打印:0
println(bb()) //打印:1
println(bb()) //打印:2
//反编译
//被闭包引用的 int 局部变量,会被封装成 IntRef 这个类。
//这个 IntRef 里面保存着 int 变量,原函数和闭包都可以通过 intRef 来读写 int 变量。
//Kotlin 正是通过这种办法使得局部变量可修改。除了 IntRef,还有 LongRef,FloatRef 等
//如果是非基础类型,就统一用 ObjectRef 即可。
method() {
IntRef intRef = new IntRef();
intRef.element = 0;
return (Function0) new 1<>(intRef);
}
mian{
Function0 aa = method();
Function0 bb = method();
System.out.println(((Number) aa.invoke()).intValue());
System.out.println(((Number) aa.invoke()).intValue());
System.out.println(((Number) bb.invoke()).intValue());
ystem.out.println(((Number) bb.invoke()).intValue());
}