跳到内容

第 1 卷 / 第 8 章 / 第 6 课

8.6 Lambda 表达式与函数式接口 ​

前置知识:第5章的方法、第8章的接口

预计时间:25 分钟

运行环境:Java 17

完成标志:能声明函数式接口、写 Lambda,并解释目标类型与变量捕获

告警系统要处理三类事件:磁盘快满了要通知值班人员,任务完成要记录结果,连接断开要提示重试。先为每种事件写一个 EventHandler 实现类当然能用,可这些类都只出现一次,真正变化的只有 handle 里的几行行为。

Lambda 解决的正是这个问题:在需要一个特定接口实现的地方,直接给出那段行为。

1. 函数式接口约定参数和结果 ​

Lambda 不能脱离类型单独存在。它需要一个只有一个抽象方法的接口作为目标类型:

java
@FunctionalInterface
interface EventHandler {
    void handle(Event event);
}

@FunctionalInterface 不是必需语法,却很值得保留。以后若有人加入第二个抽象方法,编译器会立刻指出这个接口已不能承接 Lambda。

“一个抽象方法”要按接口契约来数:

  • default、static 和 private 方法不算抽象方法;
  • 与 Object 的 public 方法签名相同的声明不计入这个数量;
  • 从父接口继承来的抽象方法也要一起考虑;
  • 两个声明若能合并为同一个方法契约,接口仍可能是函数式接口。
java
@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 ​

匿名内部类写法强调“创建一个对象并实现方法”:

java
EventHandler handler = new EventHandler() {
    @Override
    public void handle(Event event) {
        System.out.println("事件:" + event.name());
    }
};

Lambda 写法强调“收到参数后做什么”:

java
EventHandler handler = event -> System.out.println("事件:" + event.name());

以下片段需导入 java.util.function 中的相关接口,以及 java.util.Comparator。Event 的完整定义见后面的运行示例;各个同名变量片段应分开试。常见形式如下:

java
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。减法在极端整数上可能溢出,得到错误顺序。

语法可以这样读:

text
(参数列表) -> 表达式
(参数列表) -> { 语句; return 结果; }
  • 没有参数时必须写 ();
  • 只有一个推断类型的参数时可省略括号;
  • 多个参数必须放在括号里;
  • 目标方法有返回值时,表达式体直接提供该值,不写 return;目标返回 void 时,可用方法调用等语句表达式,例如 () -> System.out.println("开始");
  • 语句块若需要返回值,所有正常结束路径都要显式 return。

3. 目标类型决定 Lambda 是什么 ​

下面的 Lambda 单独看没有完整类型:

java
text -> text.length()

赋值、传参或强制类型转换给它目标类型后,编译器才能确定参数类型和返回类型:

java
Function<String, Integer> length = text -> text.length();
ToIntFunction<String> fastLength = text -> text.length();

同一形状的 Lambda 可能匹配多个重载,这时会产生歧义:

java
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:只赋值一次,之后不再重新赋值。

java
String prefix = "[消息]";
EventHandler handler = event ->
        System.out.println(prefix + " " + event.name());

// prefix = "[紧急]"; // 若取消注释,prefix 不再 effectively final

这是 Java 对局部变量捕获的语言规则。不要把它简单解释成“Lambda 一定会在另一个线程执行”:Lambda 可以完全同步地运行,规则仍然成立。

注意,不能重新给局部变量赋值,不等于捕获的对象不可变:

java
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 不自动校验空值或数值范围;本例使用固定的有效输入。

java
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) { }
}

编译并运行:

bash
javac --release 17 -encoding UTF-8 LambdaDemo.java
java LambdaDemo

预期输出:

text
报警系统就绪
[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,不要用可能溢出的减法。

→ 下一步:Stream API 集合流水线

规范入口:JLS §15.27:Lambda 表达式。

Built with VitePress | Software Systems Atlas