Lambda 表达式和函数式接口是什么?有哪些使用限制?
简化版
Lambda 是函数式接口实例的简洁写法,函数式接口只包含一个抽象方法。Lambda 的具体类型由目标上下文决定,捕获的局部变量必须是 final 或 effectively final,但可以读取和修改对象内部的状态。
详细版
@FunctionalInterface 用于声明设计意图并让编译器检查接口是否满足规则,但不是函数式接口成立的必要条件。default、static 方法以及与 Object 公有方法签名一致的方法不计入唯一抽象方法,因此接口仍可提供多个非抽象方法。
常用内置接口包括:
Predicate<T>:接收参数并返回boolean。Function<T, R>:把T转换为R。Consumer<T>:消费参数,不返回结果。Supplier<T>:不接收参数,提供一个结果。
Lambda 依赖目标类型,同一个 (x) -> x + 1 可以赋给不同的兼容函数式接口。它不是匿名内部类的语法糖:Lambda 中的 this 指向外围对象,也不会创建一套新的 this 语义。
完整版教学
一、函数式接口如何判定
函数式接口的核心是只有一个抽象方法,也称 SAM(Single Abstract Method)。例如:
@FunctionalInterface
interface Converter<T, R> {
R convert(T value);
default R convertTwice(T value) {
return convert(value);
}
}
default 方法已经有实现,不会破坏 SAM 约束;静态方法属于接口本身,也不参与计算。@FunctionalInterface 可省略,但建议保留,因为后来误加第二个抽象方法时编译器会直接报错。
二、Lambda 依赖目标类型
Lambda 本身没有脱离上下文的独立类型,赋值、传参、返回值或强制类型转换会提供目标函数式接口:
Predicate<String> empty = text -> text.isEmpty();
Function<String, Integer> length = String::length;
Consumer<String> printer = System.out::println;
Supplier<List<String>> factory = ArrayList::new;
方法引用是 Lambda 的另一种表达形式,前提是参数和返回值能适配目标接口。它不是调用方法,而是描述稍后如何调用方法。
三、为什么局部变量必须 effectively final
Lambda 可以捕获外围作用域的局部变量,但该变量在初始化后不能再赋值:
int threshold = 10;
Predicate<Integer> large = value -> value > threshold;
// threshold++; // 编译失败
局部变量随方法栈帧结束而消失,Lambda 捕获的是其值而不是共享一个可变局部变量槽。限制重新赋值可以避免生命周期和并发可见性产生歧义。
注意,引用不能重新赋值不等于对象不可变。下面代码可以编译,但共享可变集合仍可能带来线程安全问题:
List<String> names = new ArrayList<>();
Consumer<String> add = names::add;
四、异常与重载陷阱
Lambda 能否抛出受检异常,取决于函数式接口抽象方法的 throws 声明。Function.apply 没有声明受检异常,因此直接调用会抛受检异常的方法无法通过编译,需要在 Lambda 内处理或定义合适的函数式接口。
当重载方法分别接收形状相同的函数式接口时,Lambda 可能无法让编译器唯一确定目标类型。这时应减少含糊重载,或通过显式参数类型、变量声明、类型转换帮助编译器选择。
五、Lambda 与匿名内部类的区别
匿名内部类会创建一个新的类作用域,其 this 指向匿名对象;Lambda 不引入新的 this,其中的 this 与外围上下文相同。两者的类型推断、作用域和运行时实现都不应视为完全等价,只是在许多回调场景中可以表达相似行为。
六、用捕获与调用次数理解代价
无捕获 Lambda 与捕获 Lambda 的运行时实例化策略属于实现细节,不能承诺每次都创建新对象或一定复用单例。正确做法是按语义写代码,再用 JMH 和分析工具测量热点;不要依据匿名内部类的对象模型推断 Lambda 身份。
假设一个流处理 1,000,000 个元素,map 中的 Lambda 最多可能执行 1,000,000 次,但短路、优化和数据过滤会改变实际次数。Lambda 若每次做网络调用,API 写法再简洁也仍是 1,000,000 次潜在 I/O;函数式风格不会自动批处理或缓存。
| 形式 | this 含义 | 是否有独立类作用域 | 主要用途 |
|---|---|---|---|
| Lambda | 外围实例 | 否 | SAM 行为参数 |
| 匿名内部类 | 匿名对象 | 是 | 需要独立对象语义/多方法 |
| 方法引用 | 由目标签名适配调用 | 否 | 已有方法的简写 |
记忆钩子:Lambda 先找目标 SAM,再检查参数、返回值和 throws 是否兼容;捕获局部变量是捕获值,不是共享可变栈槽。
七、常见误区与追问
- 误区:标注
@FunctionalInterface才能使用 Lambda。 注解不是成立条件,只是让编译器保护设计意图。 - 误区:函数式接口只能声明一个方法。 限制的是一个抽象方法,default、static 和符合规则的 Object 方法不计入。
- 误区:Lambda 就是匿名内部类的纯语法糖。 两者的 this、作用域、类型和运行时实现都不同。
- 误区:effectively final 会让捕获对象变成不可变对象。 只限制局部变量引用重新赋值,对象内部仍可变并可能存在并发风险。
- 追问:方法引用何时不能替代 Lambda? 当需要额外参数变换、多步逻辑或 throws 不兼容时,不能仅靠方法引用适配。
- 追问:Lambda 为什么可能导致重载歧义? 形状相同的多个函数式接口都可能成为目标类型,需要显式变量、参数类型或强制转换消歧。
- 追问:Lambda 能否抛受检异常? 取决于目标 SAM 的 throws;标准 Function 未声明时必须捕获、包装或改用自定义接口。
八、加强记忆
函数式接口看的是唯一抽象方法,不是方法总数;@FunctionalInterface 是编译期护栏。Lambda 必须依附目标类型,捕获的局部变量要保持 final 或 effectively final,方法引用则是与目标接口签名兼容的简写。