8.6 Lambda 表达式与函数式接口
前置知识:第5章的方法、第8章的接口
预计时间:25 分钟
运行环境:Java 17
完成标志:能声明函数式接口、写 Lambda,并解释目标类型与变量捕获
告警系统要处理三类事件:磁盘快满了要通知值班人员,任务完成要记录结果,连接断开要提示重试。先为每种事件写一个 EventHandler 实现类当然能用,可这些类都只出现一次,真正变化的只有 handle 里的几行行为。
Lambda 解决的正是这个问题:在需要一个特定接口实现的地方,直接给出那段行为。
1. 函数式接口约定参数和结果
Lambda 不能脱离类型单独存在。它需要一个只有一个抽象方法的接口作为目标类型:
@FunctionalInterface
interface EventHandler {
void handle(Event event);
}@FunctionalInterface 不是必需语法,却很值得保留。以后若有人加入第二个抽象方法,编译器会立刻指出这个接口已不能承接 Lambda。
“一个抽象方法”要按接口契约来数:
default、static和private方法不算抽象方法;- 与
Object的public方法签名相同的声明不计入这个数量; - 从父接口继承来的抽象方法也要一起考虑;
- 两个声明若能合并为同一个方法契约,接口仍可能是函数式接口。
@FunctionalInterface
interface NamedTask {
void run();
default String label() { return "未命名任务"; }
static NamedTask idle() { return () -> { }; }
boolean equals(Object other); // 对应 Object 的 public 方法,不增加函数方法数量
}Runnable、Callable<T>、Comparator<T> 都是标准库中的函数式接口。Lambda 最终实现的是其中唯一的函数方法。
2. 从匿名类缩到 Lambda
匿名内部类写法强调“创建一个对象并实现方法”:
EventHandler handler = new EventHandler() {
@Override
public void handle(Event event) {
System.out.println("事件:" + event.name());
}
};Lambda 写法强调“收到参数后做什么”:
EventHandler handler = event -> System.out.println("事件:" + event.name());以下片段需导入 java.util.function 中的相关接口,以及 java.util.Comparator。Event 的完整定义见后面的运行示例;各个同名变量片段应分开试。常见形式如下:
Runnable start = () -> System.out.println("开始任务");
Consumer<String> print = message -> System.out.println(message);
Comparator<Integer> order = (left, right) -> Integer.compare(left, right);
Function<String, Integer> length = text -> {
String clean = text.strip();
return clean.length();
};这里使用 Integer.compare,而不是 left - right。减法在极端整数上可能溢出,得到错误顺序。
语法可以这样读:
(参数列表) -> 表达式
(参数列表) -> { 语句; return 结果; }- 没有参数时必须写
(); - 只有一个推断类型的参数时可省略括号;
- 多个参数必须放在括号里;
- 目标方法有返回值时,表达式体直接提供该值,不写
return;目标返回 void 时,可用方法调用等语句表达式,例如() -> System.out.println("开始"); - 语句块若需要返回值,所有正常结束路径都要显式
return。
3. 目标类型决定 Lambda 是什么
下面的 Lambda 单独看没有完整类型:
text -> text.length()赋值、传参或强制类型转换给它目标类型后,编译器才能确定参数类型和返回类型:
Function<String, Integer> length = text -> text.length();
ToIntFunction<String> fastLength = text -> text.length();同一形状的 Lambda 可能匹配多个重载,这时会产生歧义:
static void use(Function<String, Integer> function) { }
static void use(ToIntFunction<String> function) { }
// use(text -> text.length()); // 编译错误:两个重载都可能匹配
use((ToIntFunction<String>) text -> text.length());工程代码里最好用有含义的变量或不同方法名消除歧义;强制转换适合解释规则,不宜到处堆。
4. 四个最常见的标准接口
它们都在 java.util.function 包中:
| 接口 | 函数方法 | 提问方式 | 示例 |
|---|---|---|---|
Supplier<T> | T get() | 生产什么 | () -> "系统就绪" |
Consumer<T> | void accept(T) | 怎样消费 | System.out::println |
Function<T, R> | R apply(T) | 怎样转换 | String::length |
Predicate<T> | boolean test(T) | 是否满足 | text -> !text.isBlank() |
还有 UnaryOperator<T>、BinaryOperator<T>、BiFunction<T,U,R> 等组合,以及 IntPredicate、ToIntFunction<T> 等原始类型特化。大量处理 int 时,特化接口可以避免反复装箱为 Integer。
5. 捕获的是局部值,限制的是局部变量
Lambda 可以读取包围方法里的局部变量,但该局部变量必须是 final 或 effectively final:只赋值一次,之后不再重新赋值。
String prefix = "[消息]";
EventHandler handler = event ->
System.out.println(prefix + " " + event.name());
// prefix = "[紧急]"; // 若取消注释,prefix 不再 effectively final这是 Java 对局部变量捕获的语言规则。不要把它简单解释成“Lambda 一定会在另一个线程执行”:Lambda 可以完全同步地运行,规则仍然成立。
注意,不能重新给局部变量赋值,不等于捕获的对象不可变:
List<String> audit = new ArrayList<>();
EventHandler handler = event -> audit.add(event.name()); // 引用没变,对象内容变了这段代码能编译,但共享可变对象会增加推理难度,在并发环境中还可能产生竞态。优先让 Lambda 返回结果,或把状态管理交给明确的对象;不要把单元素数组当成绕过规则的默认技巧。
Lambda 中的 this 也沿用外层词法作用域,指向包围对象;匿名内部类则拥有自己的 this。两者并非只差几行语法。
6. 可运行的报警示例
把下面内容保存为 LambdaDemo.java。这里用 record Event(String name, int level) 简写一个数据类:它生成构造方法和 name()、level() 访问器,组件字段为 final。Record 不自动校验空值或数值范围;本例使用固定的有效输入。
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class LambdaDemo {
public static void main(String[] args) {
List<Event> events = List.of(
new Event("磁盘容量不足", 5),
new Event("任务已完成", 1),
new Event("连接已断开", 4));
Predicate<Event> urgent = event -> event.level() >= 4;
Function<Event, String> format = event ->
"[" + event.level() + "] " + event.name();
Consumer<String> announce = System.out::println;
Supplier<String> ready = () -> "报警系统就绪";
System.out.println(ready.get());
for (Event event : events) {
if (urgent.test(event)) {
announce.accept(format.apply(event));
}
}
EventHandler handler = event ->
System.out.println("已记录:" + event.name());
handler.handle(events.get(0));
}
@FunctionalInterface
interface EventHandler {
void handle(Event event);
}
record Event(String name, int level) { }
}编译并运行:
javac --release 17 -encoding UTF-8 LambdaDemo.java
java LambdaDemo预期输出:
报警系统就绪
[5] 磁盘容量不足
[4] 连接已断开
已记录:磁盘容量不足7. 异常与适用边界
受检异常不会凭空消失
Lambda 能否抛出受检异常,由目标接口的方法签名决定。Runnable.run() 不声明受检异常,因此不能直接把会抛 IOException 的操作塞进去;要么当场处理,要么改用声明了合适异常的接口。不要用大范围 catch (Exception) 隐藏失败。
Lambda 不是所有小类的替代品
一次性的无状态行为很适合 Lambda。若行为有清晰领域名称、多个相关操作、需要独立状态或复杂测试,命名类通常更清楚。目标是让意图更直接,不是把所有代码都压成一行。
8. 语言边界
- Python 的
lambda只能包含一个表达式,多行逻辑通常写def;Python 不需要 Java 式函数式接口。 - C++ Lambda 用捕获列表显式选择值捕获、引用捕获或
this;Java 没有等价的引用捕获语法。 - 本章代码只以 Java 17 为可运行主线,其他语言只用于辨认差异,不能逐字翻译语法。
离开这一页前
给一个 Lambda 分别换上两个可能的目标类型,观察何时需要显式类型才能消除重载歧义。再让它捕获局部变量:重新赋值该变量应编译失败,修改所指可变对象却可能编译成功。后者不是推荐写法,只说明 effectively final 约束的是变量绑定。整数比较器使用 Integer.compare,不要用可能溢出的减法。
规范入口:JLS §15.27:Lambda 表达式。