Kotlin学习笔记(二)
匿名函数我们在定义函数时,不给他名字,这个函数就是匿名函数了。匿名函数通常用法是作为参数传递给其他函数或者被其他函数返回。Kotlin因有了匿名函数,我们开发者就可以轻松实现对kotlin标准库内置函数的定制。案例:fun main() {val a = "luffy".count()var b = "luffy love honey".count({ letter -> letter.eq
目录
函数类型
在 Kotlin 里,除了 Int、String 这类类型,还有函数类型,写法形如:
-
无参:
() -> R -
单参:
(T) -> R -
多参:
(T1, T2) -> R
它的含义是:这是一个可调用的值——像普通变量一样赋值、传给别的函数、作为返回值返回。这就是常说的一等公民(first-class)能力:函数类型的值可以在运行时像数据一样传来传去。
Lambda表达式 VS 匿名函数
Kotlin 有两种常见的“函数字面量”(也就是当场写出来的函数值,作为值传递):lambda 表达式和匿名函数。在 Kotlin 中,函数可以像普通数据一样被赋值给变量、作为参数传入、或作为返回值返回——这也是常说的“函数是一等公民”。因此你会在标准库里经常看到接收函数参数的 API(例如接收 predicate 这类判断函数)。下面用 CharSequence.count 作为示例说明:
fun main() {
val a = "luffy".count()
// lambda:放在花括号里传入
val b = "luffy love honey".count({ letter ->
letter == 'y'
})
// lambda:拖尾 lambda + 隐式 it
val c = "luffy love honey".count {
it == 'y'
}
// 匿名函数:Kotlin 里用 fun(参数列表...) : 返回值类型(可以无返回值){ 代码逻辑... } 表示
// 注意:这里的花括号里面写的是匿名函数代码快,不是 lambda
val d = "luffy love honey".count(fun(letter: Char): Boolean {
return letter == 'y'
})
println("a == $a")
println("b == $b")
println("c == $c")
println("d == $d")
}
函数类型的值(包括 lambda、匿名函数、以及 ::引用)都有类型,类型由参数类型与返回类型决定;可以像普通变量一样赋值、传递。参数个数也可以是 0 个或多个,并支持不同参数类型。
无参lambda
// 无参函数类型 () -> String:变量存的是「无参且返回 String」的函数值。
// 下面两段右侧都是 lambda(花括号);Kotlin 里「匿名函数」通常指 fun(): String { ... } 这种写法,见 midAutumnAnon。
// 第一段:先声明变量 midAutumnLine,类型是 () -> String(无参、返回 String 的函数类型),后面再把右侧的 lambda { ... } 赋给它(在 fun main 这种局部作用域里,
// val 允许「先声明、后赋值一次」,只要使用前已赋值即可)。
val midAutumnLine: () -> String
midAutumnLine = {
val holiday = "Mid-Autumn Festival"
"Happy $holiday"
}
// 第二段:声明和赋值在同一句里完成,类型和 lambda 初值写在一起。
val midAutumnLineDirect: () -> String = {
val holiday = "Mid-Autumn Festival"
"Happy $holiday"
}
println(midAutumnLine())
println(midAutumnLineDirect())
val midAutumnAnon: () -> String = fun(): String {
val holiday = "Mid-Autumn Festival"
return "Happy $holiday"
}
println(midAutumnAnon())
有小伙伴可能注意到了,上面案例中,lambda 返回值类型是String,但是我并没有用return返回字符串,其实原因是lambda会隐式返回最后一个语句的执行结果,而不需要我们用return关键字显式返回数据。
拓展:带标签的lambda
// greet@ / return@greet:标签与带标签 return,只从本 lambda 返回;裸 return 在 lambda 中非法
val holidayGreeting1: (String) -> String = greet@{
val holiday = "Mid-Autumn Festival"
return@greet "$it, Happy $holiday"
}
带参数的lambda
当我们显式写出函数类型 (String) -> String 时,类型信息在类型签名里给出,lambda 里只需要写参数名(以及 ->),例如 { name -> ... }。
如下代码案例中:noNameval2 的类型 (String) -> String 说明了“入参类型是什么、返回类型是什么”;lambda 本体 { name -> ... } 只负责给这个入参起名并在函数体里使用它。类型在类型签名里,名字在 lambda 参数列表里。
//带一个参数的 lambda
val noNameval2: (String) -> String = { name ->
val holiday = "Mid-Autumn Festival"
"$name,Happy $holiday"
}
当 lambda 只有一个参数且我们没写参数列表时,编译器会帮我们生成一个隐式参数名 it,在 lambda 体内可直接用 it 引用该参数:
// 编译器默认会帮我们生成 it作为参数
val noNameval3: (String) -> String = {
val holiday = "Mid-Autumn Festival"
"$it,Happy $holiday"
}
类型推断
定义一个变量时,如果把 lambda 作为变量赋值给它,不需要显式指明变量类型,kotlin支持类型推断。
val noNameval4 = {
val holiday = "Mid-Autumn Festival"
"Happy $holiday"
}
类型推断也支持带参数的匿名函数:
- 如果没有目标类型(例如
val x = { name, year -> ... }(这样写会编译报错),左边没写函数类型),那编译器确实推不出参数类型,这时你就需要在 lambda 形参处写类型。 - 如果有清晰的目标函数类型(变量类型/参数类型/重载上下文),通常可以省略参数类型,甚至使用
it。
一句话描述:对于带参数的 lambda,编译器通常可以从目标函数类型中推断出参数类型;只在缺少目标类型或推断存在歧义时,才需要在 lambda 的参数列表中显式标注类型。
// 写法一:在 lambda 的参数列表上显式标注参数类型
// 编译器可以从这里知道:这是一个接收 (String, Int) 并返回 String 的函数值
val noNameVal1 = { name: String, year: Int ->
val holiday = "Mid-Autumn Festival"
"$name,Happy $holiday,$year"
}
// 写法二:在变量一侧声明「函数类型」(String, Int) -> String
// lambda 右侧就可以只写参数名,参数类型由左侧的函数类型推断得出
val noNameVal2: (String, Int) -> String = { name, year ->
val holiday = "Mid-Autumn Festival"
"$name,Happy $holiday,$year"
}
println("noNameVal1 is ${noNameVal1("Luffy",2026)}")
println("noNameVal2 is ${noNameVal2("Honey",2026)}")
之所以叫 lambda,是因为匿名函数的这个概念源自数学上的 λ 演算(lambda calculus)。λ 演算是数学家 Alonzo Church 在 20 世纪 30 年代提出的一套形式化计算模型,用希腊字母 λ 来表示“函数抽象”。后来很多编程语言在定义“可以当值传递的函数表达式”时,沿用了这一记号和名字,于是把这种用
λ/lambda记法表达函数抽象、并常作为值传递的函数表达式,习惯上称为 lambda(在 Kotlin 里通常特指{ ... }这种 lambda 表达式)。
定义一个参数类型为函数类型的函数
即函数的参数是另外一个函数。如下案例中:showOnBoard 的第二个参数 discountWords 的类型是函数类型 (String, Int) -> String,也就是“接收商品名和小时数,返回一段文案”的函数。我们在 main 里用一个 lambda 表达式构造出这样的函数值,然后把它当作参数传给 showOnBoard。
fun main() {
val discountWords = { goodsName: String, hours: Int ->
val currentYear = 2022
//给变量加上大括号{}之后,字符串就没有空格了
"${currentYear}年,${goodsName}双11大促销倒计时还有:$hours 小时"
}
showOnBoard("牙膏", discountWords)
}
// 具名函数
private fun showOnBoard(goodsName: String, discountWords: (String, Int) -> String) {
//返回一个1到24的随机数
val hour = (1..24).shuffled().last()
println(discountWords(goodsName, hour))
}
简略写法
如果一个函数调用中,最后一个实参是 lambda 表达式,并且它对应的形参类型是函数类型,那么这个 lambda 可以写到括号外;如果它又是唯一的实参,那么整对小括号都可以省略。
fun main() {
//如果一个函数最后的实参是lambda,或者是唯一的参数传入的是lambda,那么lambda外面的圆括号可以省略掉
val a = "luffy".count { it == 'f' }
println("a = $a")
showOnBoard("牙膏") { goodsName: String, hours: Int ->
val currentYear = 2022
"${currentYear}年,${goodsName}双11大促销倒计时还有:$hours 小时"
}
}
//具名函数
private inline fun showOnBoard(goodsName: String, discountWords: (String, Int) -> String) {
//返回1到24的随机数
val hour = (1..24).shuffled().last()
println(discountWords(goodsName, hour))
}
注意:形参类型是函数类型 (String, Int) -> String,实参可以是 lambda、匿名函数、函数引用等任何能生成这种函数值的写法,而不局限于 lambda。
import kotlin.random.Random
private const val CURRENT_YEAR = 2026
fun main() {
// 写法一:lambda 表达式(函数值)
// 这里用一个变量接住 lambda,类型明确为 (String, Int) -> String
val discountWords: (String, Int) -> String = { goodsName, hours ->
makeWords(goodsName, hours)
}
// lambda 表达式:把函数值作为参数传入
showOnBoard("牙膏", discountWords)
// 写法一(变体):尾随 lambda(trailing lambda)
// 当函数的最后一个实参是 lambda 时,可以把它挪到括号外;若它又是唯一实参,甚至可以省略括号
showOnBoard("牙膏") { goodsName, hours ->
makeWords(goodsName, hours)
}
// 写法二:匿名函数(带 fun,但没有函数名)
showOnBoard("牙膏", fun(goodsName: String, hour: Int): String {
return makeWords(goodsName, hour)
})
// 写法三:函数引用(把已存在的具名函数当作值传入)
showOnBoard("牙膏", ::makeWords)
}
// 高阶函数:第二个参数是“函数类型” (String, Int) -> String
private fun showOnBoard(goodsName: String, discountWords: (String, Int) -> String) {
// 返回一个 1..24 的随机数
val hours = Random.nextInt(from = 1, until = 25)
println(discountWords(goodsName, hours))
}
private fun makeWords(goodsName: String, hours: Int): String {
return "$CURRENT_YEAR 年,$goodsName 双11大促销倒计时还有:$hours 小时"
}
private fun makeWords2(goodsName: String, hours: Int): String =
"$CURRENT_YEAR 年,$goodsName 双11大促销倒计时还有:$hours 小时"
上面的案例中,关于函数引用解析如下:
- 调用链:
main()→ 调用showOnBoard("牙膏", ::makeWords)→ 里面调用discountWords(goodsName, hour)→ 实际就是makeWords("牙膏", hour)。 - 参数流向:
"牙膏":main实参 →showOnBoard.goodsName→ 传给makeWords.goodsName。- 随机数
hour:在showOnBoard内部生成 → 作为第二个实参传给discountWords(goodsName, hour)→ 也就是makeWords(goodsName, hour)的hours。
函数内联(inline)
在 Kotlin 中,每次使用 Lambda 表达式通常会在底层生成一个匿名对象实例。如果频繁调用,JVM 为这些对象分配内存会带来可观的性能开销。
为了解决这个问题,Kotlin 引入了 inline(内联)关键字。
核心原理:
`inline` 是一个纯粹的编译期特性,完全由 Kotlin 编译器(kotlinc)在生成 `.class` 字节码时完成。
当编译器遇到 `inline` 函数时,它不会生成常规的函数调用指令,也不会创建 Lambda 对象实例;而是直接把函数体本身以及传入的 Lambda 代码,像“复制粘贴”一样,直接平铺展开到调用点。往往显著减少了对象创建和函数压栈的开销。注意事项:
包含 Lambda 的递归函数无法被内联。因为内联的本质是代码的“展开替换”,对递归的inline函数,编译器通常无法安全地做无界展开,因此会限制/报错(具体形态以编译器为准)。
我们show kotlin bytecode,再把它反编译一下,就能看到lambda确实是以函数引用的形式存在:在 JDK 17 下,Kotlin lambda 常走 invokedynamic + LambdaMetafactory 路径:lambda 逻辑落在合成静态方法中,并在调用点被链接为 Function2 传入高阶函数。它不是 inline 展开,只是实现形态不再总是“显式匿名类”。


开启 inline 后,原本“把 lambda 当参数传递并再调用”的流程,被编译器在编译期直接展开到调用点;从字节码看,main 里只剩普通变量操作、拼接和打印,不再出现 Function2/invoke 调用链:

有小伙伴有疑问了,既然lambda会以函数对象语义参与调用,怎么会有内存开销呢?
我们可以把 lambda 想象成“一个小工具人”,调用普通高阶函数时,你不是把代码直接塞进去跑,而是先把这个“小工具人”打包好,再交给函数去调用。这个“打包+传递”过程就可能花钱(内存开销):
- 要有个“壳子”装这个工具人(对象/函数值载体)
- 如果它要用外面的变量,还得把这些变量一起背进去(闭包)
- 调用时还要多绕一层(
invoke),有时还会把int装成Integer
所以不是 lambda 慢,而是“多了一层打包和中转”,就可能有额外开销。
而 inline 干的事就像:
不派工具人了,直接把要干的活写到现场。这样就少了很多“打包、搬运、中转”的成本。
函数引用
函数引用(::) 就是“把现成函数当参数传出去”的简写,很多时候能代替你手写一段 lambda。但前提是“接口要对上号”(参数个数、类型、返回值都得匹配)。另外还要分清是“先绑好对象再传”(绑定引用)还是“只传方法本身”(非绑定引用),有重名重载时还得告诉编译器你到底指哪一个。
fun main() {
showOnBoader("牙膏", ::getDiscountWords)
}
private fun showOnBoader(goodsName: String, discoutWords: (String, Int) -> String) {
val hours = (1..24).shuffled().last()
println(discoutWords(goodsName, hours))
}
private fun getDiscountWords(goodsName: String, hours: Int): String {
val currentYear = 2020
return "${currentYear}年双十一${goodsName}大促销倒计时:$hours 小时"
}
绑定引用和非绑定引用又是什么东西?
-
绑定引用:
obj::method相当于“这个对象我已经给你选好了”,之后调用时只用传剩下的参数。 -
非绑定引用:
ClassName::method相当于“方法给你,但对象你自己带上”,调用时第一个参数通常就是接收者对象。
class User(val name: String) {
fun greet(prefix: String) = "$prefix $name"
}
val u = User("Tom")
val bound: (String) -> String = u::greet
// 已经绑定了 u,所以只要传 prefix
println(bound("Hi,")) // Hi, Tom
val unbound: (User, String) -> String = User::greet
// 没绑定对象,所以要先传 User,再传 prefix
println(unbound(u, "Hello,")) // Hello, Tom
一句话记忆:
对象::方法= 人已到场(绑定)类名::方法= 方法在,人没到(非绑定)
函数类型作为返回类型
把函数类型作为一个函数的返回类型:
private const val CURRENT_YEAR = 2026
fun main() {
val getDiscountWords = configDiscountWords()
println(getDiscountWords("生活用品"))
// configDiscountWords() 的返回值是函数类型 (String) -> String;
// configDiscountWords()("生活用品") 表示立即调用这个返回的函数,并传参"生活用品",返回结果是 String。
val getDiscountWords1 = configDiscountWords()("生活用品")
println(getDiscountWords1)
}
private fun configDiscountWords(): (String) -> String {
return {
val hours = (1..24).shuffled().last()
"${CURRENT_YEAR}年,${it}双11大促销倒计时还有:${hours}小时"
}
}
如上代码示例中:configDiscountWords() 的返回值本质上是“一个可以再被调用的函数”。对应关系是:
- 输入参数:
String(在 lambda 里用it表示) - 返回结果:
String(最后一行字符串模板)
闭包
在 Kotlin 中,lambda/匿名函数可以捕获外层变量;当这个函数连同被捕获的变量环境一起被保存并在后续调用时继续使用时,这就是闭包。闭包的重点不是“匿名”,而是“函数携带了上下文状态”。可以这么理解,lambda 像一个“可执行小程序”,闭包像它出门时背的“背包”;背包里装的是它捕获的外部变量。以后到哪里执行,都能从背包里拿这些变量。

fun configDiscountWords(): (String) -> String {
var currentYear = 2020
val hours = (1..24).shuffled().last()
return {
//在kotlin中,匿名函数能够修改并引用定义在自身作用域之外的变量,即匿名函数可以引用定义自身的函数里面的变量
currentYear += 10
"${currentYear}年双十一${it}大促销倒计时:$hours 小时"
}
}
今天内容虽然不多,但对于初学Kotlin的同学还是有难度的,想一起学习交流的,私信我进粉丝群,也欢迎大家评论区留言讨论~
更多推荐


所有评论(0)