Stream 拿來過濾、轉換、聚合很方便,但也容易踩三個坑:在 pipeline 裡改外面的狀態、同一個 Stream 用兩次,還有隨便加 parallel()。

Stream 適合寫「資料怎麼轉」,不適合一邊跑一邊改外面的變數或集合。

正確:轉換寫在 stream 裡,最後再收結果

把轉換留在 stream 裡,最後用 collect 或 toList 收起來:

List<String> activeEmails = users.stream()
        .filter(User::active)
        .map(User::email)
        .filter(email -> email != null && !email.isBlank())
        .distinct()
        .toList();

若是「對每個元素做事」(寫檔、打 API、改資料庫),用傳統 for 通常比較好讀,例外跟中斷也好處理。

陷阱:副作用、重複消費、盲目 parallel

// ❌ 在 forEach 裡改外面的 list——不好讀,開 parallel 時還可能出問題
List<String> emails = new ArrayList<>();
users.stream()
        .filter(User::active)
        .forEach(u -> emails.add(u.email()));

// ❌ 同一個 Stream 只能消費一次
Stream<User> stream = users.stream().filter(User::active);
long count = stream.count();
List<User> list = stream.toList(); // IllegalStateException

// ❌ 資料量小,或有共享可變狀態時,別亂加 parallel
users.parallelStream()
        .forEach(u -> sharedCounter.incrementAndGet());

建議這樣寫:

// ✅ 用 collect / toList 產出新的 list
List<String> emails = users.stream()
        .filter(User::active)
        .map(User::email)
        .toList();

// ✅ 要兩次結果就開兩條 stream,或先收成中間結果
List<User> active = users.stream().filter(User::active).toList();
long count = active.size();

// ✅ parallel 要資料量夠大,而且 pipeline 裡沒有共享可變狀態;先測再加
long sum = values.parallelStream()
        .mapToLong(Long::longValue)
        .sum();

parallel() 會用到共同的 ForkJoinPool。list 很短、做的是 I/O,或 pipeline 裡有鎖/共享變數時,常常更慢,也更難除錯。

小結

先寫對,再談效能。 Stream 拿來轉換跟聚合;有副作用就用迴圈;同一個 Stream 不要用兩次;要不要 parallel,先測過真的有比較快、而且資料量夠大再考慮。

寫完 pipeline 後問自己一句:「拿掉 parallel(),結果還對不對?加上去之後,測起來真的比較快嗎?」