Kotlin 函数与 Lambda
问题
Kotlin 函数式编程有哪些特性?扩展函数、高阶函数、Lambda 各有什么用途?
答案
1. 函数声明与调用
// 基本函数
fun sum(a: Int, b: Int): Int {
return a + b
}
// 单表达式函数(省略花括号和返回类型)
fun sum(a: Int, b: Int) = a + b
// 默认参数 + 命名参数 —— 减少方法重载
fun createUser(
name: String,
age: Int = 0,
email: String = ""
) = User(name, age, email)
createUser("Alice") // 使用默认值
createUser("Bob", email = "bob@test.com") // 命名参数,跳过 age
默认参数 vs Java 重载
Java 中为了提供多种调用方式需要写多个重载方法。Kotlin 用默认参数一个函数搞定。给 Java 调用时加 @JvmOverloads 注解即可自动生成重载方法。
2. 扩展函数
扩展函数可以为已有类添加新方法,无需继承或使用装饰器模式:
// 为 String 添加 isEmail 方法
fun String.isEmail(): Boolean {
return Regex("^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$").matches(this)
}
"test@example.com".isEmail() // true
"not-email".isEmail() // false
// 实战:为 View 添加便捷方法
fun View.visible() { visibility = View.VISIBLE }
fun View.gone() { visibility = View.GONE }
fun View.invisible() { visibility = View.INVISIBLE }
// 带泛型的扩展函数
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null
扩展函数的本质
扩展函数是静态分发的,编译后变成静态方法:
// fun String.isEmail() = ...
// 编译后等价于:
public static boolean isEmail(String $this) { ... }
因此扩展函数不能被子类重写(没有多态),调用哪个版本取决于编译时类型而非运行时类型。
3. 高阶函数
高阶函数是以函数作为参数或返回值的函数:
// 函数类型参数
fun performOperation(x: Int, y: Int, operation: (Int, Int) -> Int): Int {
return operation(x, y)
}
performOperation(5, 3) { a, b -> a + b } // 8
performOperation(5, 3) { a, b -> a * b } // 15
// 返回函数
fun multiplier(factor: Int): (Int) -> Int {
return { number -> number * factor }
}
val triple = multiplier(3)
triple(5) // 15
4. Lambda 表达式
// 完整写法
val sum: (Int, Int) -> Int = { a: Int, b: Int -> a + b }
// 类型推断(参数类型可省略)
val sum = { a: Int, b: Int -> a + b }
// 单参数时可用 it
val double: (Int) -> Int = { it * 2 }
// 尾随 Lambda —— 最后一个参数是函数时可放在括号外
listOf(1, 2, 3).map { it * 2 } // [2, 4, 6]
listOf(1, 2, 3).filter { it > 1 } // [2, 3]
// 多行 Lambda,最后一个表达式是返回值
val transform = { x: Int ->
val doubled = x * 2
val result = doubled + 1
result // 返回值
}
5. 作用域函数
Kotlin 标准库提供 5 个作用域函数,是面试高频考点:
| 函数 | 对象引用 | 返回值 | 主要场景 |
|---|---|---|---|
let | it | Lambda 结果 | 空安全调用、转换 |
run | this | Lambda 结果 | 对象配置 + 计算结果 |
with | this | Lambda 结果 | 对已有对象执行操作 |
apply | this | 对象本身 | 对象初始化配置 |
also | it | 对象本身 | 附加操作(日志、验证) |
// let —— 空安全 + 转换
val length = str?.let {
println("Processing: $it")
it.length // 返回长度
}
// apply —— 对象初始化(Builder 风格)
val textView = TextView(context).apply {
text = "Hello"
textSize = 16f
setTextColor(Color.BLACK)
}
// also —— 附加操作(不改变链式调用)
fun getUser() = User("Alice").also {
Log.d("TAG", "Created user: ${it.name}")
}
// run —— 配置 + 返回结果
val result = service.run {
port = 8080
connect() // 返回连接结果
}
// with —— 对已有对象执行多个操作
with(canvas) {
drawColor(Color.WHITE)
drawCircle(100f, 100f, 50f, paint)
drawText("Hello", 200f, 200f, paint)
}
6. inline 函数
高阶函数每次调用会创建 Lambda 对象,inline 关键字可消除这个开销:
// inline —— Lambda 代码内联到调用处,消除对象创建开销
inline fun <T> measureTime(block: () -> T): T {
val start = System.nanoTime()
val result = block()
println("Time: ${System.nanoTime() - start}ns")
return result
}
// noinline —— 不内联某个参数(需要作为对象传递时)
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
// notInlined 可以赋值给变量,inlined 不行
}
// crossinline —— 禁止 Lambda 中非局部返回
inline fun runOnUiThread(crossinline action: () -> Unit) {
Handler(Looper.getMainLooper()).post { action() }
}
为什么标准库的 map、filter 都是 inline 的?
集合操作频繁创建 Lambda 对象会造成性能损耗。inline 将 Lambda 代码直接内联到调用处,零额外对象分配,这就是 Kotlin "零成本抽象" 的体现。
常见面试问题
Q1: 扩展函数是静态分发还是动态分发?
答案:
静态分发。扩展函数编译后是静态方法,调用哪个版本由编译时类型决定:
open class Animal
class Dog : Animal()
fun Animal.name() = "Animal"
fun Dog.name() = "Dog"
val animal: Animal = Dog()
println(animal.name()) // "Animal" —— 不是 "Dog"!
因为 animal 的编译时类型是 Animal,所以调用了 Animal.name()。如果需要多态行为,应该使用成员函数而非扩展函数。
Q2: let、apply、run、with、also 怎么选?
答案:
选择流程:
- 需要返回对象本身? →
apply(配置)或also(副作用) - 需要返回 Lambda 结果? →
let(用 it)或run/with(用 this) - 空安全调用? →
let(?.let {}) - 对象初始化? →
apply - 日志/验证等副作用? →
also
Q3: inline 函数中的非局部返回是什么?
答案:
inline Lambda 中的 return 会返回外层函数(非局部返回):
inline fun forEach(list: List<Int>, action: (Int) -> Unit) {
for (item in list) action(item)
}
fun findFirst(): Int {
forEach(listOf(1, 2, 3)) {
if (it == 2) return it // ⚠️ 直接返回 findFirst 函数!
}
return -1
}
如果不想允许非局部返回,用 crossinline 标记参数。
Q4: Kotlin 中如何实现函数柯里化?
答案:
// 返回函数的函数
fun add(a: Int): (Int) -> Int = { b -> a + b }
val add5 = add(5)
println(add5(3)) // 8
// 通用柯里化扩展
fun <A, B, C> ((A, B) -> C).curried(): (A) -> (B) -> C =
{ a -> { b -> this(a, b) } }
val curriedSum = { a: Int, b: Int -> a + b }.curried()
curriedSum(1)(2) // 3
Q5: Kotlin 的函数类型在 JVM 上是如何表示的?
答案:
Kotlin 函数类型编译后对应 FunctionN 接口:
() -> Unit→Function0<Unit>(Int) -> String→Function1<Int, String>(Int, String) -> Boolean→Function2<Int, String, Boolean>
Lambda 表达式编译后是实现了对应 FunctionN 接口的匿名内部类。inline 函数通过将 Lambda 代码内联到调用处,避免了这个匿名类的创建。