Lambda 表达式

在数学里,函数是个一等公民——可以赋值、可以传参、可以返回。但 Java 长久以来,函数只能”寄生”在对象里——你要传一个”行为”,得先写个类,再 new 个对象,再调它的方法。这种”为传一个动作而写一个类”的繁琐,让 Java 在函数式编程的浪潮中长期扮演追赶者。

Java 8 的 Lambda 表达式,终于把”函数”从对象的束缚中解放出来。它像数学公式一样简洁——(x) -> x * x 就是平方函数。这一章,我们从思想到语法,从对比到陷阱,把 Lambda 彻底讲透。

一、函数式编程思想

1.1 函数是”一等公民”

在函数式编程(Functional Programming)里,函数和整数、字符串一样,是”一等公民”(first-class citizen):

  • 可以赋值给变量。
  • 可以作为参数传递。
  • 可以作为返回值。
  • 可以存储在数据结构里。

Java 之前做不到——“行为”必须封装在对象里。要传一个”比较器”给 Collections.sort,得写个匿名内部类:

Collections.sort(list, new Comparator<Integer>() {
    @Override
    public int compare(Integer a, Integer b) {
        return b - a;   // 降序
    }
});

为了传一行 b - a 的逻辑,写了 5 行模板——4 行是噪音。

1.2 Lambda 的解救

Lambda 让你直接把”行为”写成表达式:

Collections.sort(list, (a, b) -> b - a);

5 行变 1 行,噪音消失了,逻辑一目了然。Lambda 的本质是:把函数当作值传递

二、Lambda 语法

2.1 基本结构

Lambda 的基本语法:

(参数列表) -> 表达式或语句块

-> 是 Lambda 操作符,左侧是参数,右侧是函数体。

几种形式:

// 1. 无参数
() -> System.out.println("hello")

// 2. 一个参数(可省括号)
x -> x * x
(x) -> x * x

// 3. 多个参数
(a, b) -> a + b

// 4. 表达式体(自动返回,不加分号)
(x, y) -> x + y

// 5. 语句块(需要 return 和分号)
(x, y) -> {
    int sum = x + y;
    return sum;
}

2.2 类型可以省略

参数类型通常可以省略,编译器根据”目标类型”推断:

// 完整写法
Comparator<Integer> c = (Integer a, Integer b) -> a - b;
// 省略类型
Comparator<Integer> c = (a, b) -> a - b;

2.3 单参数可省括号

只有一个参数时,括号可以省:

Function<Integer, Integer> square = x -> x * x;
// 等价于
Function<Integer, Integer> square = (x) -> x * x;

零个或多个参数,括号不能省。

2.4 表达式 vs 语句块

// 表达式形式:简洁,自动返回
(x, y) -> x + y

// 语句块形式:可多条语句,需手动 return
(x, y) -> {
    System.out.println("计算中...");
    return x + y;
}

能用表达式就用表达式——更简洁。需要多条语句或副作用时才用语句块。

三、Lambda 与匿名内部类对比

3.1 简洁性对比

// 匿名内部类
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("hello");
    }
};

// Lambda
Runnable r2 = () -> System.out.println("hello");

6 行变 1 行。

3.2 this 指向不同

这是最容易被忽视的区别:

  • 匿名内部类的 this:指向匿名类自己的实例。
  • Lambda 的 this:指向”外围类”的实例(即定义 Lambda 的那个类)。
public class Outer {
    String name = "Outer";

    void test() {
        // 匿名内部类
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println(this.getClass());   // Outer$1(匿名类)
                System.out.println(this);              // 匿名类实例
            }
        };

        // Lambda
        Runnable r2 = () -> {
            System.out.println(this.getClass());       // Outer
            System.out.println(this);                  // Outer 实例
            System.out.println(name);                  // "Outer"(直接访问外围字段)
        };
    }
}

Lambda 不引入新的 this 作用域——它”透明地”使用外围的 this。这让 Lambda 访问外围字段更自然。

3.3 实现机制不同

  • 匿名内部类:编译时生成一个 .class 文件(如 Outer$1.class),运行时 new 出实例。
  • Lambda:Java 8 用 invokedynamic 指令动态生成——不生成额外 .class 文件,更省内存。

可以用 javap 看到:匿名内部类编译后有独立的类文件,Lambda 没有。

四、函数式接口

4.1 什么是函数式接口

Lambda 不能凭空存在——它需要一个”目标类型”(target type)。这个目标就是函数式接口(Functional Interface):

只有一个抽象方法的接口(default 方法、static 方法不计)。

@FunctionalInterface
public interface Runnable {
    void run();   // 唯一的抽象方法
}

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);   // 唯一的抽象方法
    // equals 来自 Object,不计
    // 多个 default 方法不计
}

@FunctionalInterface 注解是可选的——加上后编译器会检查”是否只有一个抽象方法”,防止意外破坏。强烈建议加上,相当于”接口契约的合同章”。

4.2 Lambda 与函数式接口的对应

Lambda 的签名(参数个数、类型、返回值)必须与目标接口的唯一抽象方法匹配:

// Runnable.run() 无参无返回
Runnable r = () -> System.out.println("hi");

// Comparator.compare(T, T) 两参返回 int
Comparator<Integer> c = (a, b) -> a - b;

// Callable<V>.call() 无参返回 V
Callable<String> call = () -> "result";

编译器根据”目标类型”(左侧声明的接口)推断 Lambda 的参数类型和返回类型——这就是”目标类型推断”。

4.3 自定义函数式接口

自己写函数式接口很简单:

@FunctionalInterface
interface StringProcessor {
    String process(String input);
}

StringProcessor toUpper = s -> s.toUpperCase();
StringProcessor reverse = s -> new StringBuilder(s).reverse().toString();

System.out.println(toUpper.process("hello"));    // HELLO
System.out.println(reverse.process("hello"));    // olleh

不过 Java 已经内置了一组常用函数式接口(FunctionPredicateConsumerSupplier 等),下章详述。自定义前先看内置的够不够用,避免重复造轮子。

五、类型推断

5.1 目标类型推断

编译器根据上下文推断 Lambda 的类型:

// 同样的 (x, y) -> x + y,目标不同,类型不同
Comparator<Integer> c = (x, y) -> x - y;        // x, y 是 Integer,返回 int
IntBinaryOperator op = (x, y) -> x + y;          // x, y 是 int,返回 int
BiFunction<String, String, String> f = (x, y) -> x + y;  // x, y 是 String

5.2 参数类型可省

// 显式类型
BinaryOperator<Integer> add = (Integer a, Integer b) -> a + b;
// 推断类型
BinaryOperator<Integer> add = (a, b) -> a + b;

混合写法(一个声明类型一个不声明)不行:(Integer a, b) -> ... 编译错误——要么全声明,要么全不声明。

5.3 返回类型推断

返回类型由函数体的”返回表达式”推断:

// 推断返回 String
Supplier<String> s = () -> "hello";

// 推断返回 int
IntSupplier is = () -> 42;

如果函数体有多个 return,类型必须一致。

六、变量捕获

6.1 Lambda 能访问外围变量

Lambda 可以访问它所在方法的局部变量、外围类的字段、方法的参数:

public class Example {
    int field = 10;   // 外围字段

    void method(int param) {
        int local = 20;   // 局部变量

        Runnable r = () -> {
            System.out.println(field);     // ✅ 访问字段
            System.out.println(param);     // ✅ 访问参数
            System.out.println(local);     // ✅ 访问局部变量
        };
    }
}

6.2 effectively final 限制

关键规则:Lambda 捕获的局部变量必须是 effectively final(事实上的 final)——即声明后不再修改。

int x = 10;
Runnable r = () -> System.out.println(x);   // ✅ x 没修改,effectively final

int y = 10;
y = 20;   // 修改了!
Runnable r2 = () -> System.out.println(y);  // ❌ 编译错误

6.3 为什么有这个限制

这个限制不是 Java 故意为难人,而是基于并发安全语义清晰的考虑:

  1. 并发问题:Lambda 可能在另一个线程执行。如果它能修改捕获的局部变量,多线程并发修改变量会导致数据竞争。Java 没有 C# 的 ref 参数机制,无法安全地”传引用”。

  2. 一致性:局部变量在方法栈上,方法返回后栈帧销毁。如果 Lambda 在方法返回后才执行(如异步回调),它访问的”局部变量”实际是一份副本。如果允许修改,副本和原变量的不一致会让人困惑。

  3. 状态管理:函数式编程倾向于”无副作用”。允许修改捕获变量会引入隐式状态,违背这一理念。

注意:字段没有这个限制——字段是对象状态,存在堆上,Lambda 访问的是引用,不是副本。但多线程修改字段仍需自己保证安全。

6.4 绕过限制的技巧

如果需要在 Lambda 中”累加”一个值,用数组或 Atomic 类”包装”:

// ❌ 不行
int sum = 0;
list.forEach(x -> sum += x);   // 编译错误

// ✅ 用数组
int[] sum = {0};
list.forEach(x -> sum[0] += x);

// ✅ 用 Atomic(线程安全)
AtomicInteger sum = new AtomicInteger();
list.forEach(x -> sum.addAndGet(x));

// ✅ 更好的方式:用 Stream
int sum = list.stream().mapToInt(Integer::intValue).sum();

最后一种最推荐——它没有副作用,是真正的函数式风格。

Java · 在线运行

七、Lambda 的常见用法

7.1 集合排序

list.sort((a, b) -> a.length() - b.length());   // 按长度升序
list.sort(Comparator.comparingInt(String::length));   // 更地道

7.2 集合遍历

list.forEach(s -> System.out.println(s));
list.forEach(System.out::println);   // 方法引用更简洁

7.3 集合删除

list.removeIf(s -> s.isEmpty());   // 删除空字符串

7.4 Stream 操作

list.stream()
    .filter(s -> s.length() > 3)
    .map(String::toUpperCase)
    .forEach(System.out::println);

7.5 线程创建

new Thread(() -> {
    System.out.println("在子线程运行");
}).start();

7.6 事件监听(GUI、回调)

button.addActionListener(e -> handleClick(e));

八、Lambda 的限制

Lambda 不是万能的,它有几个限制:

  1. 只能用于函数式接口:要传给”多抽象方法”的接口(如 List),还得用匿名内部类。
  2. 不能有状态字段:Lambda 不能像匿名内部类那样定义字段。要存状态用匿名内部类。
  3. 不能递归调用自己:Lambda 没有名字,无法直接调用自己(要靠方法引用或赋值给变量间接实现)。
  4. 不能抛检查异常:除非函数式接口的抽象方法声明了该异常。
// ❌ 不能抛检查异常(Runnable.run 不声明异常)
Runnable r = () -> { throw new IOException(); };

// ✅ 抛运行时异常可以
Runnable r2 = () -> { throw new RuntimeException(); };

// ✅ 接口方法声明了检查异常才行
interface Task { void run() throws IOException; }
Task t = () -> { throw new IOException(); };

九、本章小结

主题要点
函数是一等公民可赋值、传参、返回、存储
Lambda 语法(参数) -> 表达式(参数) -> { 语句; }
类型推断参数类型可省,由目标接口推断
函数式接口只有一个抽象方法的接口
@FunctionalInterface注解,让编译器检查
与匿名内部类对比更简洁,但 this 指向不同
this 指向Lambda 的 this 是外围类,匿名内部类的 this 是自己
实现机制Lambda 用 invokedynamic,不生成 .class
变量捕获可访问外围变量
effectively final捕获的局部变量必须不再修改
限制只能函数式接口、不能有字段、不能抛检查异常

结语:函数的自由

Lambda 表达式是 Java 走向函数式编程的第一步,也是最重要的一步。它把”行为”从”对象”的厚重外壳中解放出来,让代码回归到”逻辑”本身——一行 (a, b) -> a + b 比一个匿名内部类清爽得多。

但 Lambda 的简洁背后有规则:它需要一个函数式接口作为目标类型,捕获的局部变量必须 effectively final。理解这些规则,你才能写出既优雅又正确的 Lambda。

下一章,我们看 Java 内置的函数式接口——FunctionPredicateConsumerSupplier 等。它们是 Lambda 的”舞台”,也是 Stream API 的基石。