Lambda表达式与函数式接口
Lambda 表达式是 Java 8 引入的重要语法,用于把“行为”作为值传递。它让匿名内部类变得更简洁,也为 Stream、CompletableFuture、集合排序等 API 提供了基础。
学习 Lambda 不要只看语法糖,更要理解函数式接口、变量捕获、方法引用和工程边界。
# 1. 从匿名内部类到 Lambda
匿名内部类:
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("run");
}
};
Lambda:
Runnable task = () -> System.out.println("run");
本质上,Lambda 是对函数式接口实例的简洁表达。
# 2. 函数式接口
函数式接口是只有一个抽象方法的接口。
@FunctionalInterface
public interface Converter<T, R> {
R convert(T value);
}
使用:
Converter<String, Integer> converter = value -> Integer.parseInt(value);
@FunctionalInterface 不是必须的,但推荐添加,让编译器帮你检查接口是否仍然只有一个抽象方法。
# 3. 常用函数式接口
| 接口 | 抽象方法 | 含义 |
|---|---|---|
Runnable | run() | 无入参无返回 |
Supplier<T> | get() | 提供一个值 |
Consumer<T> | accept(T) | 消费一个值 |
Function<T,R> | apply(T) | 输入 T 输出 R |
Predicate<T> | test(T) | 判断条件 |
BiFunction<T,U,R> | apply(T,U) | 两个输入一个输出 |
示例:
Predicate<User> activeUser = User::isActive;
Function<User, String> nameMapper = User::getName;
Consumer<User> printer = user -> System.out.println(user.getName());
Supplier<List<User>> supplier = ArrayList::new;
# 4. Lambda 语法
() -> expression
value -> expression
(a, b) -> expression
(a, b) -> {
statement;
return result;
}
示例:
Comparator<User> byAge = (u1, u2) -> Integer.compare(u1.getAge(), u2.getAge());
如果只有一个参数,可以省略括号:
user -> user.getName()
如果方法体只有一个表达式,可以省略 return 和大括号。
# 5. 方法引用
方法引用是 Lambda 的进一步简化。
| 形式 | 示例 |
|---|---|
| 静态方法 | Integer::parseInt |
| 实例方法 | user::getName |
| 类的实例方法 | User::getName |
| 构造方法 | ArrayList::new |
等价关系:
Function<String, Integer> f1 = value -> Integer.parseInt(value);
Function<String, Integer> f2 = Integer::parseInt;
方法引用不是新能力,只是更简洁的写法。
# 6. 变量捕获
Lambda 可以访问外部局部变量,但变量必须是 final 或 effectively final。
String prefix = "user:";
Function<Long, String> mapper = id -> prefix + id;
不能这样:
String prefix = "user:";
prefix = "order:";
Function<Long, String> mapper = id -> prefix + id; // 编译失败
原因是 Lambda 可能在稍后执行,如果外部局部变量随意变化,会破坏语义清晰性。
# 7. Lambda 与 this
匿名内部类中的 this 指向匿名内部类对象;Lambda 中的 this 指向外部对象。
public class Demo {
public void test() {
Runnable task = () -> System.out.println(this.getClass());
}
}
这点在重构匿名内部类为 Lambda 时需要注意。
# 8. 常见场景
集合排序:
users.sort(Comparator.comparing(User::getAge));
过滤:
List<User> activeUsers = users.stream()
.filter(User::isActive)
.toList();
异步任务:
CompletableFuture.supplyAsync(() -> queryUser(userId), executor);
回调:
button.onClick(event -> submit());
# 9. 使用边界
Lambda 适合短小、语义明确的行为表达。
不推荐:
users.forEach(user -> {
validate(user);
remoteCall(user);
updateDatabase(user);
writeAuditLog(user);
});
复杂业务逻辑应抽成具名方法:
users.forEach(this::processUser);
# 10. 底层机制与工程取舍
Lambda 在源码层面像是“匿名函数”,但 Java 没有直接把函数变成一等公民。编译器会根据目标类型进行类型推断,把 Lambda 绑定到某个函数式接口上。
Lambda 表达式
│ 编译期目标类型推断
▼
函数式接口
│ invokedynamic / LambdaMetafactory
▼
运行期生成或复用接口实例
从工程角度看,Lambda 的价值是降低样板代码,但它也会隐藏调用栈和业务语义。过长的 Lambda、带副作用的 Lambda、在 Stream 中做远程调用,都会让排查变难。
| 场景 | 推荐 | 原因 |
|---|---|---|
| 排序、过滤、映射 | Lambda 或方法引用 | 行为短小清晰 |
| 复杂业务流程 | 具名方法 | 可读性、复用、日志更好 |
| 需要异常上下文 | 具名方法或显式 try/catch | Lambda 中异常定位弱 |
| 公共策略扩展 | 函数式接口 | 比匿名类更轻量 |
| 有状态逻辑 | 谨慎 | 容易引入共享状态和并发问题 |
设计函数式接口时要注意接口语义,而不是只看参数和返回值。例如 Predicate<User> 能表达判断,但 UserMatcher 这种具名接口在复杂领域规则中可能更清楚。
# Tips 快问快答
Q:Lambda 是什么? A:Lambda 是函数式接口实例的简洁写法,用于传递行为。
Q:什么是函数式接口? A:只有一个抽象方法的接口。
Q:@FunctionalInterface 必须写吗?
A:不是必须,但推荐写,便于编译器检查。
Q:Lambda 能访问外部变量吗? A:能,但局部变量必须是 final 或 effectively final。
Q:方法引用和 Lambda 有什么关系? A:方法引用是某些 Lambda 的简化写法。
Q:Lambda 中的 this 指向谁? A:指向外部对象,不是 Lambda 自身。
Q:Lambda 会让代码一定更好吗? A:不一定。逻辑复杂时具名方法更清晰。
Q:Lambda 和 Stream 是一回事吗? A:不是。Stream 常使用 Lambda 表达处理逻辑。