1. lambda 表达式中的错误:变量捕获
在 Java 中,lambda 表达式可以使用来自外部上下文的变量。但有一个限制:这些变量必须是 final 或“有效地 final”(effectively final),也就是说在初始化之后不再被修改。
错误示例
int sum = 0;
List<Integer> list = List.of(1, 2, 3, 4, 5);
list.forEach(n -> sum += n); // 编译错误!
为什么?
编译器会报错:变量在 lambda 中被使用,因此它必须是 final 或 effectively final,而 sum 在 lambda 内被修改了。
如何避免?
- 使用不依赖外部变量的流终端操作:mapToInt + sum()。
- 在极端情况下——使用类似 AtomicInteger 的容器或一个元素的数组(但这更像是 hack)。
int sum = list.stream().mapToInt(Integer::intValue).sum();
类比
可以把 lambda 想象成“时空旅行者”:它“记住”创建时变量的值,无法观察后续的变化。试图去修改就像“祖父悖论”,编译器不会让你通过编译。
2. 作用域与 this 的错误
在 lambda 表达式中,关键字 this 指向的是外部对象,而不是匿名类(匿名类中 this 会指向匿名类实例)。
示例
public class Example {
int value = 42;
void foo() {
Runnable r = () -> {
System.out.println(this.value); // 这里的 this 指向 Example,而不是 Runnable!
};
r.run();
}
}
重要:当把匿名类改写为 lambda 时,this 的语义发生了变化——务必考虑这一点,避免得到意料之外的结果。
3. 可变状态(副作用)的问题
函数式风格提倡无副作用:函数不改变自身之外的状态,也不突变外部的集合或变量。
List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Vika"));
List<String> newNames = new ArrayList<>();
names.forEach(name -> {
if (name.startsWith("A")) {
newNames.add(name); // 副作用!
}
});
这段代码“能跑”,但可预测性较差,并且在使用 parallelStream() 时更危险(存在竞态与抛异常的风险)。测试和维护也更困难。
更好的做法:使用那些显式构造新结果、而不修改外部状态的操作。
List<String> newNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
4. 类型与泛型的问题
Java 是强类型语言。有时编译器无法从过于复杂的 lambda 或链式调用中推断类型。
示例
List<Object> objects = List.of(1, "字符串", 3.14);
List<String> strings = objects.stream()
.filter(obj -> obj instanceof String)
.map(obj -> (String) obj)
.collect(Collectors.toList());
看起来合乎逻辑,但任何笔误或错误的强制类型转换都可能导致编译错误,或者更糟——在运行时抛出 ClassCastException。
如何避免?
- 当类型推断“绊倒”时,添加显式类型。
- 不必害怕写 <String>,或显式标注 lambda 的参数类型:(String s) -> ...。
- 在进行类型转换时检查类型兼容性。
关于 Optional 的典型情形
Optional<String> opt = Optional.of("hello");
opt.map(s -> s.length()); // 结果是 Optional<Integer>
如果你期望得到 Optional<String>,却得到了 Optional<Integer>,请检查你的函数返回了什么。
5. lambda 中的副作用与并行
并行流(parallelStream())与副作用的组合是危险的。
示例
List<Integer> numbers = IntStream.range(0, 1000).boxed().collect(Collectors.toList());
List<Integer> results = new ArrayList<>();
numbers.parallelStream().forEach(n -> results.add(n)); // 危险!
可能发生什么?
- 数据丢失或重复。
- ConcurrentModificationException 或“玄学”问题。
正确姿势:
- 使用线程安全的集合:ConcurrentLinkedQueue、CopyOnWriteArrayList。
- 更好的是——完全避免副作用,通过 collect(...) 来收集结果。
List<Integer> results = numbers.parallelStream()
.map(n -> n)
.collect(Collectors.toList());
6. 可读性下降:“流式意大利面”和超长链
函数式风格很好,但当链条变成“超市长小票”时就糟糕了。
List<String> result = list.stream()
.filter(s -> s.length() > 2)
.map(String::trim)
.map(s -> s.toUpperCase())
.filter(s -> s.contains("JAVA"))
.sorted()
.distinct()
.collect(Collectors.toList());
建议:
- 把链式调用拆分为若干逻辑块。
- 将复杂的 lambda 提取到具名且清晰的独立方法中。
- 必要时添加注释——即使是在 Stream 代码里。
7. 糟糕的变量与函数命名
过短的名称(x、y、z)会降低理解难度。
list.stream()
.map(x -> x.trim())
.filter(y -> y.length() > 3)
.map(z -> z.toUpperCase())
.forEach(System.out::println);
请使用有意义的名称,尤其当 lambda 跨多行或表达了非平凡逻辑时。
8. 关于 null 和 Optional 的错误
Stream API 和函数式接口“厌恶” null。把 null 传入 lambda 或流,是导致 NullPointerException 的常见原因。
List<String> list = Arrays.asList("a", null, "b");
list.stream()
.map(String::toUpperCase) // 砰!第二个元素触发 NPE
.forEach(System.out::println);
正确做法:
- 提前过滤 null:.filter(Objects::nonNull)。
- 使用 Optional 明确表示值的缺失。
9. 组合函数中的返回类型问题
在使用 compose 和 andThen 时,很容易搞混函数应用的顺序和期望的类型。
Function<String, Integer> parse = Integer::parseInt;
Function<Integer, Integer> square = x -> x * x;
Function<String, Integer> parseAndSquare = parse.andThen(square);
// 可行:先 parse,然后 square
Function<String, Integer> squareThenParse = parse.compose(square);
// 错误!square 接受 Integer,而 parse 期望 String
结论:务必检查应用顺序以及类型是否匹配。
10. lambda 中的受检异常问题
来自 java.util.function 包的函数式接口不允许抛出受检异常(例如 IOException)。如果在 lambda 中需要调用会抛出受检异常的代码,请手动处理异常。
Function<String, String> readFile = path -> {
try {
return Files.readString(Path.of(path));
} catch (IOException e) {
throw new RuntimeException(e); // 或以其他方式处理
}
};
否则,编译器将不允许你在流或集合中使用这样的函数。
GO TO FULL VERSION