CodeGym /課程 /JAVA 25 SELF /處理串流時的常見錯誤解析

處理串流時的常見錯誤解析

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

1. 錯誤:重複使用串流

在 Java 中,Stream 是一次性工具。一旦你執行了終端操作(例如 collect()forEach()count()),串流就會關閉並且無法再繼續使用。

錯誤範例

Stream<String> stream = Stream.of("a", "b", "c");
stream.forEach(System.out::println); // 一切正常
stream.forEach(System.out::println); // 會丟出 IllegalStateException!

輸出:

a
b
c
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed

如何避免

如果需要執行多個操作——每次都建立新的串流:

List<String> list = List.of("a", "b", "c");
list.stream().forEach(System.out::println); // OK
list.stream().count(); // OK

小撇步: 串流就像餐廳的拋棄式杯子:用完就丟,下一杯請拿新的。

2. 錯誤:在串流處理期間修改集合

如果你在透過串流遍歷時修改原始集合,會得到 ConcurrentModificationException。這在篩選與刪除元素時很常見。

錯誤範例

List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
names.stream().forEach(name -> {
    if (name.startsWith("A")) {
        names.remove(name); // 砰!
    }
});

結果:

Exception in thread "main" java.util.ConcurrentModificationException

正確作法

使用不會在遍歷期間修改集合的方法,或將結果收集到新集合:

names.removeIf(name -> name.startsWith("A")); // OK,removeIf 是安全的

// 或使用 Stream API:
List<String> filtered = names.stream()
    .filter(name -> !name.startsWith("A"))
    .collect(Collectors.toList());

記住: 串流講究純淨——當串流正在「吃」原集合時,別去動它。

3. 錯誤:在無限串流中忘了加 limit

Stream.iterateStream.generate 可能會建立無限串流。若忘了用 limit 限制大小,程式可能會永遠卡住(或直到 OutOfMemoryError)。

錯誤範例

Stream<Integer> infinite = Stream.iterate(0, n -> n + 1);
infinite.forEach(System.out::println); // 程式將永遠不會結束!

正確作法

使用 limit 限制串流:

Stream<Integer> finite = Stream.iterate(0, n -> n + 1).limit(10);
finite.forEach(System.out::println); // 會輸出 0 到 9

建議: 無限串流就像沒有大結局的影集:總得有人喊停,最好是你提前加上限制!

4. 錯誤:對 null 的處理不當

Stream API 不喜歡 null。如果集合中出現 null,許多操作可能會丟出 NullPointerException,尤其是當你在元素上呼叫方法時。

錯誤範例

List<String> names = Arrays.asList("Alice", null, "Bob");
names.stream()
    .map(String::toUpperCase) // 砰!遇到 null
    .forEach(System.out::println);

結果:

Exception in thread "main" java.lang.NullPointerException

正確作法

先過濾掉 null 值:

names.stream()
    .filter(Objects::nonNull)
    .map(String::toUpperCase)
    .forEach(System.out::println);

搭配 Optional

如果你有 Stream<Optional<T>>,請使用 flatMap(Optional::stream)(Java 9+)來「展開」:

List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty(), Optional.of("B"));
optionals.stream()
    .flatMap(Optional::stream)
    .forEach(System.out::println); // 只會輸出 "A" 與 "B"

建議: 盡量不要把 null 放進集合;若非得如此,請在一開始就把它們過濾掉。

5. 錯誤:元素順序丟失

某些操作(例如 flatMapconcat)可能會影響元素順序,特別是當你使用平行串流或在合併不同集合時。

錯誤範例

List<Integer> list1 = List.of(1, 2, 3);
List<Integer> list2 = List.of(4, 5, 6);
Stream<Integer> combined = Stream.concat(list1.stream(), list2.stream());
combined.forEach(System.out::print); // 123456,順序不變

// 但如果使用平行串流:
Stream<Integer> par = Stream.concat(list1.parallelStream(), list2.parallelStream());
par.forEach(System.out::print); // 順序可能會出乎意料!

正確作法

如果順序很重要——請避免平行串流,或使用保證順序的方法(forEachOrdered):

par.forEachOrdered(System.out::print);

事實: Stream API 預設會保留順序,除非你自己把它「打亂」了。

6. 錯誤:集合合併的低效作法

對於集合運算,最好使用 Set 而不是 List,以避免重複並提升效能。在可以使用 flatMap 的情況下,別寫巢狀迴圈。

錯誤範例

List<String> list1 = List.of("a", "b", "c");
List<String> list2 = List.of("b", "c", "d");

// 低效:
List<String> union = new ArrayList<>(list1);
for (String s : list2) {
    if (!union.contains(s)) { // List 的 contains 是 O(n)
        union.add(s);
    }
}

// 使用 Set 與 Stream API 的有效作法:
Set<String> unionSet = Stream.concat(list1.stream(), list2.stream())
    .collect(Collectors.toSet());

或用於差集:

// 低效:
List<String> diff = new ArrayList<>(list1);
diff.removeAll(list2); // OK,但如果改用 stream 搭配 list.contains,會很慢

// 高效:
Set<String> set2 = new HashSet<>(list2);
List<String> difference = list1.stream()
    .filter(s -> !set2.contains(s))
    .collect(Collectors.toList());

建議: 需要處理唯一元素時請使用 Set。對大型集合,避免巢狀迴圈,優先考慮 stream + flatMap

7. 錯誤:重複使用終端操作

有時你可能想把串流「分叉」成兩個分支,例如同時收集成清單又統計數量。但串流是一次性的,第二個操作會導致錯誤。

錯誤範例

Stream<String> stream = Stream.of("a", "b", "c");
List<String> list = stream.collect(Collectors.toList());
long count = stream.count(); // 砰!IllegalStateException

正確作法

如果需要多次使用結果——先把它收集成集合:

List<String> list = Stream.of("a", "b", "c").collect(Collectors.toList());
long count = list.size();

建議: 想同時兩邊都要?那就先收集成集合再說。

8. 錯誤:忘記關閉資源(close())

如果你使用與資源相關的串流(例如 Files.lines(Path)),請務必在使用後關閉它們,避免資源洩漏。

錯誤範例

Stream<String> lines = Files.lines(Path.of("file.txt"));
lines.forEach(System.out::println); // OK,但資源沒有被關閉!

正確作法

使用 try-with-resources:

try (Stream<String> lines = Files.lines(Path.of("file.txt"))) {
    lines.forEach(System.out::println);
}

建議: 檔案串流就像熱水壺:燒開了就要關!

9. 錯誤:在 flatMap 中錯誤處理 Optional 與 null

處理 List<Optional<T>> 時,很多人會使用 map(Optional::get),當 Optional 內部為空時就會拋出例外。

錯誤範例

List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty());
optionals.stream()
    .map(Optional::get) // 砰!NoSuchElementException
    .forEach(System.out::println);

正確作法

使用 flatMap(Optional::stream)(Java 9+):

optionals.stream()
    .flatMap(Optional::stream)
    .forEach(System.out::println); // 只會有 "A"

建議: Optional 不只是包裝,而是用來安全處理值缺席的工具。

10. 錯誤:equals/hashCode 實作不正確

集合運算(Setdistinct、聯集、交集)仰賴正確實作的 equalshashCode 方法。

錯誤範例

class Student {
    String name;
    int age;
    // 缺少 equals 與 hashCode!
}

Set<Student> students = new HashSet<>();
students.add(new Student("Alice", 20));
students.add(new Student("Alice", 20));
System.out.println(students.size()); // 2,明明應該視為相同!

正確作法

若要在 SetMap 中使用,或進行 distinct 等操作,請為你的類別覆寫 equalshashCode

11. 錯誤:在 collect 之後繼續使用同一個 stream

在執行完終端操作(例如 collect)之後,串流會被關閉。任何嘗試繼續使用同一個串流的行為都會導致錯誤。

錯誤範例

Stream<String> stream = Stream.of("a", "b", "c");
List<String> list = stream.collect(Collectors.toList());
stream.filter(s -> s.startsWith("a")).forEach(System.out::println); // Exception!

正確作法

從集合建立新的串流:

list.stream().filter(s -> s.startsWith("a")).forEach(System.out::println);

12. 錯誤:錯誤使用 parallelStream

許多人以為 parallelStream() 一定會加速。其實對小型集合或簡單操作,平行化可能會因執行緒管理的額外開銷而變慢。

錯誤範例

List<Integer> numbers = IntStream.range(0, 10).boxed().collect(Collectors.toList());
numbers.parallelStream().forEach(System.out::println); // 可能比一般的 stream() 還慢

正確作法

僅在集合足夠大或計算足夠耗時計使用 parallelStream,特別是當操作是純函式(無副作用)且可結合時。

13. 錯誤:忘了呼叫終端運算子

中間操作(mapfilterflatMap 等)在沒有呼叫終端運算子(collectforEachcount 等)之前不會執行。

錯誤範例

Stream<String> stream = Stream.of("a", "b", "c").map(String::toUpperCase);
// 什麼都不會發生!

正確作法

務必呼叫終端運算子:

stream.forEach(System.out::println);
1
問卷/小測驗
合併與投影,等級 32,課堂 4
未開放
合併與投影
Stream API:合併與投影
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION