高阶函数到底高阶在哪里?

1 阅读5分钟

1.png

前几天同事问我:为什么会有高阶函数这个东西?难道还有低级函数吗?

我笑而不语,回道:等我!

何为高阶

高阶函数指:如果函数本身可以作为另一个函数的参数,或者可以作为一个函数的返回值,或二者兼而有之,那么这个函数,我们就称之为高阶函数。

你有没有发现?这和变量已经一样了!

它让 API 更灵活、更利于表达「按行为配置」的逻辑,是 Kotlin 里函数式风格的支柱;常见用途包括回调、数据变换,以及对集合做 mapfilterreduce 等操作。

语法

当函数的参数类型或返回类型被声明为函数类型时,它就具备了高阶函数的形态。函数类型的写法是 (参数类型) -> 返回类型

fun higherOrderFunction(input: Int, operation: (Int) -> Int): Int {
    return operation(input)
}

这里的 operation 就是一个「入参为 Int、出参为 Int」的函数。

当然,参数可以是多个,例如 (Int, String) -> Int

高阶函数适合在调用时传入不同行为,我们看看函数作为参数怎么调用。例如:

fun double(x: Int): Int {
    return x * 2
}

fun main() {
    val result = higherOrderFunction(5, ::double)
    println(result) // Output: 10
}

::double 以函数引用的形式传入,并在内部作用在输入上。

当然,也可以让高阶函数返回一个函数,例如按名字选择运算:

fun operation(type: String): (Int, Int) -> Int {
    return when (type) {
        "add" -> { a, b -> a + b }
        "multiply" -> { a, b -> a * b }
        else -> { _, _ -> 0 }
    }
}

fun main() {
    val addOperation = operation("add")
    println(addOperation(3, 4)) // Output: 7
}

这里的 operation 会根据传入的 type 动态创建并返回对应的函数。

常见用途

  1. 集合上的 lambda:标准库里的 mapfilterreduce 都建立在高阶函数之上。
val numbers = listOf(1, 2, 3, 4)
val doubled = numbers.map { it * 2 }
println(doubled) // Output: [2, 4, 6, 8]
  1. 事件与回调:完成通知、异步收尾等常写成「接收一个函数」的形式。
fun performAction(onComplete: () -> Unit) {
    println("Performing action...")
    onComplete()
}

fun main() {
    performAction { println("Action completed!") }
}

优势

Kotlin 里你可以把函数当作普通值传递、返回和保存,由此得到:

  • 复用性:行为与「主流程」拆开,同一套骨架可接不同函数,实现不同效果。
  • 可读性:尤其在集合场景下,map/filter/reduce 比手写循环更能直接表达「在做什么」。
  • 灵活性:运行时可以替换、组合行为,适合事件、回调、UI 逻辑等。
  • 抽象层次:便于把控制流、惯用法、算法泛化,写出跨类型、跨场景复用的工具。

小结

高阶函数与 lambda 搭配,让 Kotlin 在保持简洁的同时,能写出清晰、偏函数式且可维护的代码。

这里需要注意的是,lambda 只是向高阶函数传递行为的一种常见方式,正如上文提到的 ::double 这种函数引用写法,同样也能作为参数传递给高阶函数。

进阶:JVM 的落地

嘻嘻,又到了我们最喜欢的 Java 字节码环节了。

对于高阶函数的字节码,Kotlin 会做固定几类变换,使函数式特性在 JVM 上可用且尽量高效。

1. 函数式接口

高阶函数在字节码里体现为 kotlin.jvm.functions.* 下的接口,例如:

  • Function0<R>:无参;
  • Function1<P, R>:单参;
  • Function2<P1, P2, R>:双参;依此类推。

例如:

fun higherOrderExample(operation: (Int) -> Int): Int {
    return operation(10)
}

大致会对应到接受 Function1<Integer, Integer> 的方法,并通过 invoke 调用:

int higherOrderExample(Function1<Integer, Integer> operation) {
    return operation.invoke(10);
}

2. Lambda 变成匿名类

传入的 lambda 通常会变成实现对应函数式接口的匿名类,概念上类似:

val result = higherOrderExample { it * 2 }
Function1<Integer, Integer> lambda = new Function1<Integer, Integer>() {
    @Override
    public Integer invoke(Integer it) {
        return it * 2;
    }
};
int result = higherOrderExample(lambda);

3. invokedynamic

编译器可利用 Java 7 引入的 invokedynamic 指令,让 JVM 在运行时(通常借助 LambdaMetafactory)动态生成 lambda 的实现类,从而取代在编译期生成大量匿名内部类的做法。

这种机制带来了两点明显的好处:

  • 减小产物体积:编译后生成的 .class 文件数量显著减少(特别是在大型项目中)。
  • 优化运行时性能:将 lambda 实例化的策略交由 JVM 底层决策,JVM 可以做更深度的优化(例如复用无捕获状态的 lambda 实例)。

注:自 Kotlin 1.5 起,针对 JVM 目标平台,编译器已经默认使用 invokedynamic 来编译普通的 lambda 表达式。

4. inline

若高阶函数标成 inline,lambda 体可能被直接抄进调用点,不再为本次调用单独建函数对象。例如:

inline fun inlineHigherOrderExample(operation: (Int) -> Int): Int {
    return operation(10)
}

val result = inlineHigherOrderExample { it * 2 }

在效果上可理解为直接得到类似 10 * 2 的字节码,从而减少分配与间接调用。

int result = 10 * 2;

归纳

  1. 高阶函数 → 形参/返回值用 FunctionN 等接口表达。
  2. Lambda → 匿名类实现接口,或走 invokedynamic 等动态生成。
  3. inline → 把 lambda 体内联到调用处,降低对象分配与方法调用开销。

弄清这些有助于在 JVM 目标下做性能取舍(例如何时值得用 inline)。

一点想法

高阶函数为 Kotlin 带来了极其强大的表达力和简洁性,但由于底层的 FunctionN 和对象分配,如果不加控制也容易引入隐形的性能开销。 在实际开发中:

  • 遇到被频繁调用的高阶函数(例如集合遍历、协程挂起等核心库),果断配合 inline 关键字使用以消除闭包和对象创建成本。
  • 而在普通回调(如点击事件响应等低频操作)中,直接使用普通的高阶函数即可,Kotlin 1.5+ 默认开启的 invokedynamic 会帮忙把运行效率和包体积都控制得很好。