Lambda 表达式与方法引用从 Java 8 开始提供,用来为函数式接口创建简洁的实现。它们广泛应用于集合排序、Stream API、异步任务和事件处理。
Lambda 不是脱离类型独立存在的“函数值”。编译器必须从赋值、参数、返回值或强制类型转换等上下文中推断出它的目标函数式接口。
一、函数式接口是基础
函数式接口在概念上只有一个抽象方法。它可以包含 default、static 方法,也可以包含与 Object 公共方法签名相同的方法。@FunctionalInterface 不是必需的,但推荐使用,让编译器帮助维护这一约束。
@FunctionalInterface
interface Calculator {
int apply(int a, int b);
default int twice(int value) {
return apply(value, value);
}
}下面两个 Lambda 的形状相同,但目标类型不同,因此具有不同含义:
Runnable task = () -> System.out.println("done");
Callable<String> loader = () -> "done";二、Lambda 基本语法
(参数列表) -> 表达式
(参数列表) -> {
语句;
}常见简化规则
- 参数类型通常可以省略,由目标类型推断。
- 只有一个未显式声明类型的参数时,可以省略圆括号。
- 方法体只有一个表达式时,可以省略大括号和 return。
- 多条语句必须放入大括号;需要返回值时应显式 return。
Predicate<String> nonEmpty = s -> !s.isEmpty();
Function<String, Integer> length = String::length;
BinaryOperator<Integer> add = (a, b) -> a + b;
Function<Integer, Integer> abs = value -> {
if (value >= 0) {
return value;
}
return -value;
};三、常用函数式接口
- Supplier<T>:无参数,返回 T;核心方法 get()。
- Consumer<T>:接收 T,不返回结果;核心方法 accept(T)。
- Function<T, R>:把 T 转换为 R;核心方法 apply(T)。
- Predicate<T>:接收 T,返回 boolean;核心方法 test(T)。
- UnaryOperator<T>:接收并返回同一种类型 T。
- BiFunction<T, U, R>:接收两个参数并返回 R。
- BinaryOperator<T>:接收两个 T,并返回 T。
- Runnable、Callable<V>、Comparator<T> 也都是常见函数式接口。
处理大量基本类型数据时,可以使用 IntFunction、IntPredicate、ToIntFunction 等特化接口,减少不必要的装箱和拆箱。
四、典型使用场景
简化 Runnable
// 匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
}).start();
// Lambda
new Thread(() -> System.out.println("Hello")).start();集合排序
List<String> languages = new ArrayList<>(
List.of("java", "python", "go")
);
languages.sort(
Comparator.comparingInt(String::length)
.thenComparing(Comparator.naturalOrder())
);Stream API
List<String> result = languages.stream()
.filter(s -> s.length() > 3)
.map(String::toUpperCase)
.sorted()
.toList();
result.forEach(System.out::println);自定义函数式接口
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;
System.out.println(add.apply(1, 2)); // 3
System.out.println(multiply.apply(3, 4)); // 12五、什么是方法引用
当 Lambda 的主要工作只是调用一个已经存在的方法时,可以使用 :: 写成方法引用。方法引用仍然需要目标函数式接口,编译器会根据接口方法的参数和返回类型完成适配。
Consumer<String> printer = value -> System.out.println(value);
Consumer<String> samePrinter = System.out::println;六、四类方法引用
1. 静态方法:ClassName::staticMethod
Function<String, Integer> parser = Integer::parseInt;
Integer value = parser.apply("42");2. 特定对象的实例方法:object::instanceMethod
PrintStream output = System.out;
Consumer<String> printer = output::println;
List<String> names = List.of("Tom", "Jack");
names.forEach(output::println);3. 某类型任意对象的实例方法:TypeName::instanceMethod
这种形式会把函数式接口的第一个参数作为方法接收者,其余参数作为方法参数。
BiFunction<String, String, Integer> comparator =
String::compareToIgnoreCase;
// 等价于:
BiFunction<String, String, Integer> sameComparator =
(left, right) -> left.compareToIgnoreCase(right);
names.stream()
.sorted(String::compareToIgnoreCase)
.forEach(System.out::println);4. 构造器引用:ClassName::new
Supplier<List<String>> listFactory = ArrayList::new;
List<String> list = listFactory.get();
Function<String, User> userFactory = User::new;
User user = userFactory.apply("Tom");七、变量捕获与作用域
Lambda 可以读取外层局部变量,但该变量必须是 final 或 effectively final,即赋值后不再发生变化。实例字段和静态字段不受这一局部变量限制。
int minLength = 4;
Predicate<String> longEnough =
value -> value.length() >= minLength;
// minLength = 5; // 如果重新赋值,上面的 Lambda 将无法编译- Lambda 不创建新的 this 语义;其中的 this 指向外层实例。
- Lambda 参数不能与外层局部变量重名。
- 并发执行 Lambda 时,捕获对象的可变状态仍需考虑线程安全。
八、常见易错点
- 不要用 a.length() - b.length() 实现通用比较,极端数值比较可能溢出;优先使用 Comparator.comparingInt。
- 方法引用只有在意图更清晰时才值得使用;复杂转换继续使用 Lambda。
- 重载方法可能导致目标类型歧义,可通过显式变量类型或强制转换解决。
- 标准 Function、Consumer 等接口不会自动处理受检异常,需要捕获、包装或定义允许抛出异常的接口。
- 避免在 Stream 的中间操作中依赖副作用,可读性和并行执行都更可靠。
九、快速记忆
Lambda:
(参数) -> 表达式 / 语句块
方法引用:
ClassName::staticMethod
object::instanceMethod
TypeName::instanceMethod
ClassName::new先确认目标函数式接口,再判断 Lambda 是否只是调用现有方法;如果是,并且参数能够自然对应,就可以考虑改成方法引用。