Skip to content

第 4 章 函数与 Lambda(高阶函数 · 扩展函数)

学习目标:能写出"参数有默认值 / 命名参数 / 单表达式函数 / vararg / 中缀函数 / 扩展函数 / 高阶函数 / Lambda" 各种现代函数;理解函数式编程的"基本词汇",开始写出"像 Kotlin 的代码"。


4.1 函数定义:Java 写不出的优雅

4.1.1 基本形式

kotlin
fun greet(name: String): String {
    return "Hello, $name!"
}

📌 关键字 fun 比 Java 的 public static String 简洁多了。

4.1.2 单表达式函数(Single-expression Function)

如果函数体只有一个表达式,可以省略花括号、return 和返回类型:

kotlin
fun greet(name: String) = "Hello, $name!"

fun max(a: Int, b: Int) = if (a > b) a else b

fun isAdult(age: Int) = age >= 18

💡 Kotlin 风格:能写成单表达式就写成单表达式,更紧凑也更易读。

4.1.3 默认参数(Default Arguments)

Java 没有,Kotlin 有,这是 Kotlin 最实用的特性之一

kotlin
fun connect(host: String = "localhost", port: Int = 8080, timeout: Int = 5000) {
    println("Connecting to $host:$port (timeout=${timeout}ms)")
}

connect()                                // localhost:8080, 5000ms
connect("example.com")                   // example.com:8080, 5000ms
connect("example.com", 9090)             // example.com:9090, 5000ms

📌 解决了什么问题:Java 里要支持"可选参数"得写一堆重载(5 个参数 = 32 个重载),Kotlin 一行搞定。

4.1.4 命名参数(Named Arguments)

kotlin
connect(timeout = 10000)                              // 只指定 timeout,其它用默认
connect(host = "example.com", timeout = 10000)        // 跳过 port
connect(port = 9090, host = "example.com")            // 顺序无所谓

💡 生活化类比:传统调用是「按位填表」(必须按顺序),命名参数是「贴标签」(哪个先填都行)。

真实场景:替代 Builder 模式

Java 里复杂对象创建常用 Builder 模式:

java
new Request.Builder()
    .url("http://example.com")
    .method("POST")
    .timeout(5000)
    .build();

Kotlin 用默认参数 + 命名参数直接搞定:

kotlin
data class Request(
    val url: String,
    val method: String = "GET",
    val timeout: Int = 5000,
    val headers: Map<String, String> = emptyMap(),
)

Request(url = "http://example.com", method = "POST", timeout = 5000)

4.1.5 可变参数 vararg

kotlin
fun sum(vararg numbers: Int): Int = numbers.sum()

sum(1, 2, 3)             // 6
sum(1, 2, 3, 4, 5, 6)    // 21

// 把数组展开成 vararg
val nums = intArrayOf(1, 2, 3)
sum(*nums)               // 注意 * 号(spread 操作符)

4.1.6 中缀函数 infix

kotlin
infix fun Int.times(s: String) = s.repeat(this)

3 times "Hi "             // "Hi Hi Hi "
3.times("Hi ")            // 等价

// 标准库里大量使用:
val map = mapOf("a" to 1, "b" to 2)   // "a" to 1 就是中缀调用,相当于 "a".to(1)

📌 infix 限制:必须是成员函数或扩展函数;只能有一个参数;参数不能是 vararg。


4.2 扩展函数:Kotlin 的"魔法"

4.2.1 痛点:Java 给别人的类加方法很麻烦

想给 String 加一个 toCamelCase() 方法?Java 里只能写工具类:

java
public class StringUtils {
    public static String toCamelCase(String s) { /* ... */ }
}
StringUtils.toCamelCase("hello_world");

调用起来又长又丑。

4.2.2 Kotlin:一行搞定

kotlin
fun String.toCamelCase(): String =
    this.split("_").joinToString("") {
        it.replaceFirstChar { c -> c.uppercase() }
    }

"hello_kotlin_world".toCamelCase()       // "HelloKotlinWorld"

调用起来跟内置方法一模一样。

4.2.3 扩展函数的本质:编译后是静态方法

kotlin
// 你写的代码
fun String.shout() = this.uppercase() + "!"

// 编译后等价的 Java
public static String shout(String $this$shout) {
    return $this$shout.toUpperCase() + "!";
}

📌 重要事实:扩展函数不是真的把方法加到 String 类里,只是语法糖。String 类本身没变。

4.2.4 扩展函数的"陷阱"

kotlin
open class A
class B : A()

fun A.hello() = "I am A"
fun B.hello() = "I am B"

val a: A = B()
println(a.hello())                       // "I am A"  ← 注意!

📌 结论:扩展函数静态分派(按编译期类型决定调哪个),不像类的方法是动态分派(按运行时类型)。这是面试常考点。

4.2.5 实战:标准库里的扩展函数

Kotlin 标准库本身就大量用扩展函数:

kotlin
// String 上扩展的(Java String 没有的)
"hello".reversed()                       // "olleh"
"hello".toCharArray()                    // ['h','e','l','l','o']
"hello".take(3)                          // "hel"
"hello".chunked(2)                       // ["he", "ll", "o"]

// List 上扩展的
listOf(1,2,3).sum()
listOf(1,2,3).average()
listOf("a","b","c").joinToString(",")    // "a,b,c"

4.3 高阶函数与 Lambda

4.3.1 函数也是值(First-class Function)

Kotlin 里函数可以当变量传、当参数收、当返回值返:

kotlin
val greet: (String) -> String = { name -> "Hello, $name!" }
println(greet("Alice"))                  // Hello, Alice!

// 函数参数
fun apply(x: Int, op: (Int) -> Int): Int = op(x)
apply(5, { it * 2 })                     // 10
apply(5) { it * 2 }                      // 10(lambda 在最后可以拖出括号外)

// 函数返回函数
fun makeAdder(n: Int): (Int) -> Int = { x -> x + n }
val add5 = makeAdder(5)
add5(3)                                  // 8

4.3.2 Lambda 语法

{ 参数列表 -> 函数体 }
kotlin
val sum = { a: Int, b: Int -> a + b }
sum(1, 2)                                // 3

// 类型可以推断
val nums = listOf(1, 2, 3)
nums.map { x -> x * 2 }                  // [2, 4, 6]

// 单参数可省略,用 it 代替
nums.map { it * 2 }                      // 推荐

// 多参数 lambda
mapOf("a" to 1, "b" to 2).forEach { (k, v) ->
    println("$k = $v")
}

📌 it 的来源:当 lambda 只有一个参数时,编译器自动注入一个名为 it 的变量。不用就别命名

4.3.3 函数类型

kotlin
() -> Unit                          // 无参数无返回值
(Int) -> String                     // 收 Int 返 String
(Int, Int) -> Int                   // 二元函数
(String) -> Unit                    // 无返回值
suspend (Int) -> String             // suspend 函数(第 9 章)
String.(Int) -> Char                // String 上的扩展函数类型(receiver type)

4.3.4 高阶函数实战:自己实现 forEach

kotlin
fun <T> List<T>.myForEach(action: (T) -> Unit) {
    for (item in this) action(item)
}

listOf(1, 2, 3).myForEach { println("got: $it") }

4.3.5 inline:高阶函数的"零成本抽象"

每次调用 lambda 都要创建一个 Function 对象,频繁调用会有 GC 压力。Kotlin 用 inline 解决:

kotlin
inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
    for (item in this) action(item)
}

inline 让编译器把高阶函数和它的 lambda 直接嵌入到调用点,不再生成中间对象。

📌 何时用 inline:高阶函数 + lambda 频繁被调用的场景。Kotlin 标准库的 map / filter / forEach 都是 inline 的。


4.4 闭包

Lambda 可以捕获外部变量:

kotlin
fun makeCounter(): () -> Int {
    var count = 0
    return { count++ }
}

val counter = makeCounter()
counter()                                // 0
counter()                                // 1
counter()                                // 2

每次调用 counter() 都返回当前 count 值,然后 count 自增。Lambda "记住" 了 count 这个变量

📌 跟 Java 的区别:Java Lambda 只能捕获 final 或 effectively final 变量;Kotlin 可以修改捕获的 var 变量


4.5 一个综合例子:自定义 retry 函数

实现一个"出错就重试 N 次"的高阶函数:

kotlin
inline fun <T> retry(times: Int = 3, block: (attempt: Int) -> T): T {
    var lastError: Throwable? = null
    repeat(times) { attempt ->
        try {
            return block(attempt + 1)
        } catch (e: Exception) {
            lastError = e
            println("第 ${attempt + 1} 次尝试失败: ${e.message}")
        }
    }
    throw lastError ?: IllegalStateException("retry failed")
}

fun main() {
    val result = retry(times = 5) { attempt ->
        if (attempt < 3) throw RuntimeException("fake error")
        "成功!第 $attempt 次"
    }
    println(result)
}

输出:

第 1 次尝试失败: fake error
第 2 次尝试失败: fake error
成功!第 3 次

4.6 章末小结

              ★ 第 4 章核心知识图谱 ★

        ┌──────────────┼──────────────┐
        │              │              │
     ┌──▼──┐       ┌──▼───┐       ┌──▼───┐
     │ 函数 │       │ 扩展  │       │ Lambda│
     ├─────┤       ├──────┤       ├──────┤
     │默认参│       │给已有 │       │() -> X│
     │命名参│       │类加方 │       │it 参数│
     │vararg│      │静态分 │       │闭包    │
     │infix │      │派     │       │inline │
     └─────┘       └──────┘       └──────┘

🎤 4.7 章末面试题(10 道)

Q1. Kotlin 的"单表达式函数"是什么?

:当函数体只有一个表达式时,可以省略花括号、return 和(通常)返回类型:

kotlin
fun max(a: Int, b: Int) = if (a > b) a else b

等价于:

kotlin
fun max(a: Int, b: Int): Int { return if (a > b) a else b }

适合一行能搞定的工具函数。


Q2. 默认参数能解决什么问题?编译后是怎样的?

:解决"可选参数"问题。Java 没有默认参数,需要写一堆重载:

kotlin
// Kotlin
fun connect(host: String = "localhost", port: Int = 8080) { ... }

// 编译后产生:
// 1. 主方法 connect(host, port)
// 2. 一个合成方法 connect$default(host, port, mask, ignored)
//    根据 mask 决定哪些参数用默认值

注解 @JvmOverloads 可以让 Kotlin 编译器生成所有重载,方便 Java 调用。


Q3. 扩展函数是真的"加方法到类里"了吗?

不是。扩展函数本质是静态方法,只是 IDE 和编译器让你"看起来像方法调用"。

  • 编译后:fun String.shout() = ...public static String shout(String $this$shout)
  • 不会修改 String 类,所以 String 的字段、private 方法都拿不到
  • 静态分派:按编译期类型决定调哪个扩展函数

Q4. 扩展函数能访问私有成员吗?为什么?

不能访问 private / protected 成员,因为它本质是外部静态方法,只能调到 public / internal 的部分。

kotlin
class User(private val secret: String)

fun User.exposeSecret() = this.secret    // ❌ 编译报错:Cannot access 'secret'

如果想访问 private 字段,要么放在 User 类里写,要么把字段改成 internal


Q5. 扩展函数和成员函数同名时调谁?

成员函数优先。这是为了防止"扩展函数被滥用,导致原作者修改类的行为"。

kotlin
class A { fun hello() = "member" }
fun A.hello() = "extension"

A().hello()                              // "member"
// IDE 会有警告:扩展函数被遮蔽了

Q6. Lambda 里的 it 是怎么来的?

:当 lambda 只有一个参数时,Kotlin 编译器自动注入一个名为 it 的变量代表这个参数。多参数时必须显式命名:

kotlin
list.map { it * 2 }                      // 单参,自动用 it
list.mapIndexed { i, v -> "$i: $v" }     // 多参,必须显式

如果觉得 it 不够清楚,也可以显式命名:list.map { x -> x * 2 }


Q7. inline 函数是干嘛的?

:让编译器把函数体直接插入调用处,避免高阶函数每次调用都创建 Function 对象。

kotlin
inline fun <T> List<T>.myForEach(action: (T) -> Unit) {
    for (item in this) action(item)
}

调用 list.myForEach { println(it) } 编译后等价于:

kotlin
for (item in list) { println(item) }     // lambda 直接展开

好处:消除 lambda 创建开销 + 允许在 lambda 里 return 跳出外层函数(non-local return)。

代价:编译产物变大;不能用于递归。


Q8. Java 里 lambda 能修改外部变量吗?Kotlin 呢?

  • Java:lambda 只能捕获 final 或 effectively final 的变量。想修改要用 AtomicInteger 等容器。
  • Kotlin:lambda 可以读写捕获的 var 变量(编译器把它包成一个 Ref 对象)。
kotlin
fun makeCounter(): () -> Int {
    var count = 0
    return { count++ }
}
// Kotlin 编译会把 count 包成 Ref.IntRef 对象,lambda 通过引用访问

Q9. vararg 和 Java 的可变参数有啥区别?

:基本一样,都是接受多个参数 + 内部当数组用。两个差异:

  1. Kotlin 用 vararg 关键字声明,类型是 Array<T> 或基本类型 IntArray / LongArray
  2. 传递展开:Kotlin 用 *args 传数组当 vararg,Java 直接传数组也行
kotlin
fun sum(vararg nums: Int) = nums.sum()
val arr = intArrayOf(1, 2, 3)
sum(*arr)                                // 必须 * 号

Q10. 高阶函数频繁创建 Lambda 对象会不会有性能问题?怎么优化?

:会有。每个 lambda 都要:

  1. 创建 Function 接口的实现类
  2. 实例化(lambda 闭包变量越多,对象越大)
  3. 调用 invoke() 有虚方法调度

优化办法

  1. inline:编译期把函数体展开,零运行时开销(标准库 forEach/map/filter 都是)
  2. crossinline:lambda 不能 non-local return(用于嵌套场景)
  3. noinline:某个 lambda 参数不内联(用于把 lambda 当对象返回)
  4. 使用对象池 / 复用:避免 hot loop 里创建 lambda

📌 实际项目里,先用普通高阶函数,profile 发现 hot path 后再加 inline


下一章 → 第 5 章 · Null 安全 →

🎬 可视化演示

演示加载缓慢或样式异常?点此在新标签页打开 ↗

💻 示例代码

kotlin
/**
 * 第 4 章 · 函数与 Lambda — 综合示例
 *
 * 涵盖:
 *   - 单表达式函数
 *   - 默认参数 / 命名参数
 *   - vararg / spread (*)
 *   - infix 中缀函数
 *   - 扩展函数
 *   - 高阶函数 + Lambda
 *   - 闭包
 *   - inline 函数
 */

// ========== 1. 默认参数 + 命名参数 ==========
fun connect(
    host: String = "localhost",
    port: Int = 8080,
    timeout: Int = 5000,
    useTls: Boolean = false
) {
    println("Connecting → $host:$port (timeout=${timeout}ms, tls=$useTls)")
}

// ========== 2. 单表达式函数 ==========
fun max(a: Int, b: Int) = if (a > b) a else b
fun isAdult(age: Int) = age >= 18

// ========== 3. vararg ==========
fun sum(vararg numbers: Int) = numbers.sum()

// ========== 4. infix 中缀函数 ==========
infix fun Int.repeatStr(s: String) = s.repeat(this)

// ========== 5. 扩展函数 ==========
fun String.toCamelCase(): String =
    this.split("_", "-", " ").joinToString("") { word ->
        word.replaceFirstChar { it.uppercase() }
    }

fun List<Int>.average2(): Double = if (isEmpty()) 0.0 else sum().toDouble() / size

// ========== 6. 高阶函数 ==========
fun applyOperation(x: Int, y: Int, op: (Int, Int) -> Int): Int = op(x, y)

inline fun <T> retry(times: Int = 3, block: (attempt: Int) -> T): T {
    var lastError: Throwable? = null
    repeat(times) { attempt ->
        try {
            return block(attempt + 1)
        } catch (e: Exception) {
            lastError = e
            println("  ⚠️ 第 ${attempt + 1} 次失败: ${e.message}")
        }
    }
    throw lastError ?: IllegalStateException("retry exhausted")
}

// ========== 7. 闭包 ==========
fun makeCounter(): () -> Int {
    var count = 0
    return {
        count++
        count
    }
}

// ========== main ==========
fun main() {
    println("=== 1. 默认参数 + 命名参数 ===")
    connect()
    connect("example.com")
    connect(port = 9090)                              // 命名参数跳过 host
    connect(host = "example.com", useTls = true)      // 顺序无所谓

    println("\n=== 2. 单表达式函数 ===")
    println("max(3, 7) = ${max(3, 7)}")
    println("isAdult(20) = ${isAdult(20)}")

    println("\n=== 3. vararg ===")
    println("sum(1,2,3) = ${sum(1, 2, 3)}")
    val nums = intArrayOf(10, 20, 30)
    println("sum(*nums) = ${sum(*nums)}")             // spread 展开

    println("\n=== 4. infix 中缀 ===")
    println(3 repeatStr "Hi ")                        // "Hi Hi Hi "
    println("a" to 1)                                  // Pair("a", 1),to 也是 infix

    println("\n=== 5. 扩展函数 ===")
    println("hello_kotlin_world".toCamelCase())       // "HelloKotlinWorld"
    println("my-app-name".toCamelCase())              // "MyAppName"
    println(listOf(1, 2, 3, 4, 5).average2())          // 3.0

    println("\n=== 6. 高阶函数 + Lambda ===")
    println(applyOperation(5, 3) { a, b -> a + b })   // 8 (加法 lambda)
    println(applyOperation(5, 3) { a, b -> a * b })   // 15 (乘法 lambda)
    val sub: (Int, Int) -> Int = { a, b -> a - b }
    println(applyOperation(10, 4, sub))               // 6

    println("\n=== 7. retry 高阶函数 ===")
    val result = retry(times = 5) { attempt ->
        if (attempt < 3) throw RuntimeException("假装失败")
        "✅ 成功!第 $attempt 次"
    }
    println(result)

    println("\n=== 8. 闭包:counter 记住 count 变量 ===")
    val counter = makeCounter()
    println(counter())                                // 1
    println(counter())                                // 2
    println(counter())                                // 3

    println("\n=== 9. 集合的高阶函数 ===")
    val list = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    println("偶数: ${list.filter { it % 2 == 0 }}")
    println("平方: ${list.map { it * it }}")
    println("总和: ${list.sum()}")
    println("第一个 > 5: ${list.first { it > 5 }}")
    println("分组奇偶: ${list.groupBy { if (it % 2 == 0) "偶" else "奇" }}")
}

FunctionDemo.kt ↗