1. 引言
坦白说:为了写一两行代码去使用匿名类,就像为把面包店的一只面包搬到商店而雇一辆大卡车一样夸张。
例如,如果你想按长度对字符串列表排序,在 Java 8 之前通常要这样写:
List<String> list = Arrays.asList("苹果", "香蕉", "猕猴桃");
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
看起来任务很简单,代码却占了半个屏幕。尤其当这种操作很多时,代码会变得“嘈杂”,问题的本质被淹没。程序员们抱怨又痛苦,后来就发明了 Lambda 表达式。我们之前已经学过一些,现在来复习并进一步深入。
什么是 Lambda 表达式
Lambda 表达式是一种实现函数式接口的简洁写法,即只包含一个抽象方法的接口(例如 Comparator、Runnable、Consumer 等)。
简单来说,Lambda 让你可以在需要的地方“现场”写出一个函数,而无需声明单独的类或方法。
通用语法:
(params) -> { body }
示例:
- (a, b) -> a + b — 两数相加的函数
- x -> x * x — 求平方的函数
- () -> System.out.println("Hello!") — 无参数的函数
与函数式接口的关系:
Lambda 表达式总是可以赋给函数式接口类型的变量,或作为参数传给期望该接口的方法。
2. Lambda 表达式的语法
无参数
Runnable r = () -> System.out.println("你好,世界!");
r.run(); // 输出:你好,世界!
单个参数
如果只有一个参数,可以省略括号:
Consumer<String> print = s -> System.out.println(s);
print.accept("Java 很酷!");
多个参数
必须加括号:
Comparator<String> cmp = (a, b) -> a.length() - b.length();
单表达式主体
如果主体只有一个表达式,则无需花括号和 return:
Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25
代码块作为主体
如果需要多条语句,使用花括号,并在有返回值时使用 return:
Function<Integer, Integer> abs = x -> {
if (x < 0) {
return -x;
}
return x;
};
System.out.println(abs.apply(-3)); // 3
参数类型
大多数情况下可由上下文推断,无需显式声明;如果愿意,也可以显式写出:
Comparator<String> cmp = (String a, String b) -> a.length() - b.length();
无返回值的 Lambda
如果接口返回 void,只需写语句:
list.forEach(s -> System.out.println("元素:" + s));
表:Lambda 语法的不同写法
| 要做什么 | 示例 | 说明 |
|---|---|---|
| 无参数 | |
例如用于 Runnable |
| 单个参数 | |
可省略括号 |
| 多个参数 | |
必须带括号 |
| 单表达式 | |
无需 return 和花括号 |
| 代码块 | |
有返回值时需要 return |
3. 应用:Lambda 表达式的使用场景与方式
当需要将“行为”(函数)作为参数传递时,最常用的就是 Lambda 表达式。这为集合、流(Stream API)、事件等带来了真正的变革。
列表排序
在 Java 8 之前:
list.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
使用 Lambda:
list.sort((a, b) -> a.length() - b.length());
创建线程(Thread)
Thread t = new Thread(() -> System.out.println("线程已启动!"));
t.start();
处理集合
list.forEach(s -> System.out.println(s.toUpperCase()));
过滤列表
List<String> longWords = list.stream()
.filter(s -> s.length() > 5)
.collect(Collectors.toList());
示例:扩展我们的教学应用
假设我们有一个用户列表:
List<String> users = Arrays.asList("Alice", "Bob", "Charlie");
输出所有名字长度大于 4 的用户:
users.stream()
.filter(name -> name.length() > 4)
.forEach(name -> System.out.println("用户:" + name));
4. Lambda 表达式中的变量作用域
Lambda 可以使用外层方法中的变量,但有一些细节需要注意!
变量与 Lambda
Java 中的 Lambda 只能“捕获”初始化后不再改变的变量。如果变量声明为 final——很直观。即使没有写出 final,编译器也会检查其值是否被修改;如果没有修改,就将其视为“有效 final”,可以放心地在 Lambda 中使用。
示例:
int minLength = 4; // 值在任何地方都未改变
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
这里可以正常工作,因为 minLength 始终保持不变。
但如果在 Lambda 使用之后尝试重新给 minLength 赋值,就会得到编译错误:
int minLength = 4;
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
minLength = 10; // 错误!Lambda 已经“固定”了该值
本质上规则很简单:进入 Lambda 的变量必须是不可变的。
与匿名类的区别
在匿名类和 Lambda 表达式中,来自外部方法的变量规则相同:只能是 final/有效 final。
但是!
在 Lambda 中,this 指向外部对象(当前类实例);而在匿名类中,this 指向匿名类的实例。如果你在 Lambda 中访问当前类的字段或方法,这一点非常重要。
示例:
public class Example {
String name = "外部类";
void demo() {
Runnable r1 = new Runnable() {
String name = "匿名类";
@Override
public void run() {
System.out.println(this.name); // "匿名类"
}
};
Runnable r2 = () -> System.out.println(this.name); // "外部类"
r1.run();
r2.run();
}
}
5. 使用 Lambda 时的常见错误
错误 1:使用非 final/非“有效 final”的变量。 如果你决定在 Lambda 使用之后再修改变量,编译器会立刻阻止。这是为了安全:否则就不清楚应当使用变量的哪个值。
错误 2:把 this 搞混。 在 Lambda 中,this 指外部类;在匿名类中,this 指匿名类本身。如果你想从 Lambda 调用外部类的方法可以正常工作,而在匿名类中则不行(如果你指望外部类的上下文)。
错误 3:脱离上下文的 Lambda。 Lambda 不能单独使用——要么赋给函数式接口类型的变量,要么传入一个期望该接口的地方。尝试在没有上下文的情况下直接写 x -> x + 1 会导致错误。
错误 4:Lambda 过于复杂。 当 Lambda 超过 3–5 行时就会难以阅读,此时最好把逻辑提取到单独的方法中。
GO TO FULL VERSION