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 內被修改了。
如何避免?
- 使用不需要外部變數的 Stream 終端操作:mapToInt + sum()。
- 在少數情況下 — 可用 AtomicInteger 或單元素陣列做容器(但這更像是 hack)。
int sum = list.stream().mapToInt(Integer::intValue).sum();
類比
想像 Lambda 是「時間旅行者」:它「記住」建立當下的變數值,無法觀察其後續變化。若嘗試修改——就像「祖父悖論」,編譯器不會讓你通過。
2. 作用域與 this 的錯誤
在 Lambda 中,關鍵字 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. 型別與 generics 的錯誤
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 的副作用與平行化
平行 Stream(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,或「神祕」的 bug。
正確作法?
- 使用執行緒安全的集合:ConcurrentLinkedQueue、CopyOnWriteArrayList。
- 更好的方式 —— 乾脆避免副作用,透過 collect(...) 收集結果。
List<Integer> results = numbers.parallelStream()
.map(n -> n)
.collect(Collectors.toList());
6. 可讀性下降:「Stream 義大利麵」與過長的鏈結
函式風格很棒,但當鏈結長到像「超市長收據」一樣時,就不再友善了。
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 或 Stream,常會導致 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 中的 checked 例外問題
來自 java.util.function 的函式式介面,不允許拋出 checked 例外(例如 IOException)。若 Lambda 內需要呼叫會拋出這類例外的方法,請手動處理。
Function<String, String> readFile = path -> {
try {
return Files.readString(Path.of(path));
} catch (IOException e) {
throw new RuntimeException(e); // 或採用其他處理方式
}
};
否則編譯器不會允許你在 Stream 或集合中使用這樣的函式。
GO TO FULL VERSION