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 已经内置了一组常用函数式接口(Function、Predicate、Consumer、Supplier 等),下章详述。自定义前先看内置的够不够用,避免重复造轮子。
五、类型推断
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 故意为难人,而是基于并发安全和语义清晰的考虑:
-
并发问题:Lambda 可能在另一个线程执行。如果它能修改捕获的局部变量,多线程并发修改变量会导致数据竞争。Java 没有 C# 的
ref参数机制,无法安全地”传引用”。 -
一致性:局部变量在方法栈上,方法返回后栈帧销毁。如果 Lambda 在方法返回后才执行(如异步回调),它访问的”局部变量”实际是一份副本。如果允许修改,副本和原变量的不一致会让人困惑。
-
状态管理:函数式编程倾向于”无副作用”。允许修改捕获变量会引入隐式状态,违背这一理念。
注意:字段没有这个限制——字段是对象状态,存在堆上,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();
最后一种最推荐——它没有副作用,是真正的函数式风格。
七、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 不是万能的,它有几个限制:
- 只能用于函数式接口:要传给”多抽象方法”的接口(如
List),还得用匿名内部类。 - 不能有状态字段:Lambda 不能像匿名内部类那样定义字段。要存状态用匿名内部类。
- 不能递归调用自己:Lambda 没有名字,无法直接调用自己(要靠方法引用或赋值给变量间接实现)。
- 不能抛检查异常:除非函数式接口的抽象方法声明了该异常。
// ❌ 不能抛检查异常(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 内置的函数式接口——Function、Predicate、Consumer、Supplier 等。它们是 Lambda 的”舞台”,也是 Stream API 的基石。