CodeGym /課程 /JAVA 25 SELF /函式程式設計錯誤剖析

函式程式設計錯誤剖析

JAVA 25 SELF
等級 49 , 課堂 4
開放

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 中被使用,因此必須是 finaleffectively 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。

正確作法?

  • 使用執行緒安全的集合:ConcurrentLinkedQueueCopyOnWriteArrayList
  • 更好的方式 —— 乾脆避免副作用,透過 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. 不佳的變數與函式命名

過度簡短的名稱(xyz)會讓理解變得困難。

list.stream()
    .map(x -> x.trim())
    .filter(y -> y.length() > 3)
    .map(z -> z.toUpperCase())
    .forEach(System.out::println);

請使用有意義的名稱,尤其當 Lambda 是多行或包含不那麼直觀的邏輯時。

8. 關於 nullOptional 的錯誤

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. 在組合函式中的回傳型別問題

使用 composeandThen 時,很容易搞錯套用順序與預期型別。

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 或集合中使用這樣的函式。

1
任務
JAVA 25 SELF, 等級 49, 課堂 4
上鎖
計算新創公司 "太空建構者" 🚀
計算新創公司 "太空建構者" 🚀
1
任務
JAVA 25 SELF, 等級 49, 課堂 4
上鎖
在 "銀河論壇" 的訊息審核 💬
在 "銀河論壇" 的訊息審核 💬
1
問卷/小測驗
函數式程式設計,等級 49,課堂 4
未開放
函數式程式設計
函數式程式設計
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION