1. Stream.concat:合併兩個串流
在程式設計中經常會遇到這種情況:我們有兩個(或更多)資料串流,想把它們合併成一個。例如來自不同分組的兩個學生名單——需要一次性處理全部。在「早期」(Stream API 之前),我們會直接用 addAll 把兩個清單合併。但如果我們正在使用串流(Stream<T>),就會希望用更延遲、表意更清晰的方式來做。
語法與工作原理
最基本的合併兩個串流的方法——使用靜態方法 Stream.concat:
Stream<T> Stream.concat(Stream<? extends T> a, Stream<? extends T> b)
會發生什麼:
- 先處理第一個串流,接著處理第二個。
- 輸出是一個新的串流:會先依序輸出第一個串流的所有元素,然後是第二個串流的所有元素。
- 合併是延遲的:在你對結果串流執行終端操作(例如 forEach 或 collect)之前,不會進行任何處理。
範例:合併兩個姓名清單
import java.util.List;
import java.util.stream.Stream;
List<String> groupA = List.of("Anya", "Boris", "Vika");
List<String> groupB = List.of("Grisha", "Dasha");
Stream<String> allStudents = Stream.concat(groupA.stream(), groupB.stream());
allStudents.forEach(System.out::println);
結果:
Anya
Boris
Vika
Grisha
Dasha
重要:合併後的結果串流是一次性的。和任何 Stream 一樣,不能重複使用。
Stream.concat 的特點
- 一次只能兩個串流。如果有三個以上——要嘛用多次 concat 串接,要嘛用其他做法(見下文)。
- 順序會被保留。先是第一個串流的元素,然後是第二個。
- 延遲合併。如果第一個串流是無限的,就永遠不會走到第二個。
- 常見 bug:試圖把串流與自身合併(Stream.concat(stream, stream))會導致重複使用同一個串流——這是被禁止的。
2. 合併超過兩個串流:flatMap 與 Stream.of
當串流多於兩個時,使用 Stream.of + flatMap 的組合更方便。
使用 Stream.of + flatMap
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;
List<String> groupA = List.of("Anya", "Boris");
List<String> groupB = List.of("Vika");
List<String> groupC = List.of("Grisha", "Dasha");
Stream<String> allStudents = Stream.of(groupA, groupB, groupC)
.flatMap(Collection::stream);
allStudents.forEach(System.out::println);
這裡發生了什麼:
- Stream.of(groupA, groupB, groupC) 建立一個包含三個集合的串流。
- flatMap(Collection::stream) 將每個集合展平成同一個字串串流。
- 結果是——一個包含所有學生的單一串流。
優點:可以合併任意多個串流。
另一種做法:合併串流清單
import java.util.List;
import java.util.stream.Stream;
List<Stream<String>> streams = List.of(
Stream.of("a", "b"),
Stream.of("c"),
Stream.of("d", "e")
);
Stream<String> merged = streams.stream()
.flatMap(s -> s);
merged.forEach(System.out::println);
結果:
a
b
c
d
e
3. Collectors.joining:用分隔符把字串接起來
有時不只是要合併串流,而是要把所有元素黏成一條字串——例如用逗號把名字串起來。這時就可以使用收集器 Collectors.joining。
語法
String result = stream.collect(Collectors.joining(", "));
- 不帶參數:會直接把字串接在一起,不插入分隔符。
- 帶分隔符:會在元素之間插入指定的字串。
- 帶前綴與後綴: Collectors.joining(delimiter, prefix, suffix)
範例:把學生清單輸出為單一字串
import java.util.List;
import java.util.stream.Collectors;
List<String> students = List.of("Anya", "Boris", "Vika");
String line = students.stream()
.collect(Collectors.joining(", "));
System.out.println("學生名單: " + line);
結果:
學生名單: Anya, Boris, Vika
範例:帶前綴與後綴
String line = students.stream()
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(line);
結果:
[Anya, Boris, Vika]
為什麼需要這個?
- 生成 CSV 字串。
- 漂亮地列印清單與報表。
- 將資料組合成單一字串(例如用於 URL 或日誌)。
4. concat 與 flatMap 的比較:何時使用哪一個?
- Stream.concat——當剛好只有兩個串流且需要保留順序時。
- Stream.of + flatMap——當串流很多,或它們存放在集合中時。
- Collectors.joining——當元素型別是 String,且需要一條字串作為結果時。
比較表
| 方法 | 適用情境 | 程式碼範例 |
|---|---|---|
|
兩個串流 | |
|
多個串流、串流集合 | |
|
將元素接成單一字串 | |
5. 教學應用中的實例
範例 1. 合併兩個學院的學生
import java.util.List;
import java.util.stream.Stream;
List<String> itStudents = List.of("Anya", "Boris");
List<String> mathStudents = List.of("Vika", "Grisha");
Stream<String> all = Stream.concat(itStudents.stream(), mathStudents.stream());
all.forEach(System.out::println);
範例 2. 合併所有分組的學生,並輸出為單一字串
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
List<List<String>> allGroups = List.of(
List.of("Anya", "Boris"),
List.of("Vika"),
List.of("Grisha", "Dasha")
);
String allNames = allGroups.stream()
.flatMap(Collection::stream)
.collect(Collectors.joining("; "));
System.out.println("所有學生: " + allNames);
結果:
所有學生: Anya; Boris; Vika; Grisha; Dasha
範例 3. 合併數字串流
import java.util.stream.Stream;
Stream<Integer> s1 = Stream.of(1, 2, 3);
Stream<Integer> s2 = Stream.of(4, 5);
Stream<Integer> merged = Stream.concat(s1, s2);
merged.forEach(System.out::print); // 12345
6. 重要細節與注意事項
concat 只能處理兩個串流
對於三個串流會得到巢狀的寫法——可行,但很冗長。更好的方式是使用 Stream.of + flatMap:
Stream<Integer> s1 = Stream.of(1);
Stream<Integer> s2 = Stream.of(2);
Stream<Integer> s3 = Stream.of(3);
Stream<Integer> merged = Stream.concat(Stream.concat(s1, s2), s3);
串流是一次性的
執行過終端操作之後,串流就不能再使用了。重複使用會導致 IllegalStateException。
concat 產生的串流是延遲的
如果第一個串流是無限的(例如透過 Stream.generate(...) 建立),就永遠不會走到第二個。
元素順序
順序總是被保留:先第一個串流,後第二個串流。
Collectors.joining 只適用於 Stream<String>
對非字串的元素,請先轉成字串,例如透過 map(Object::toString):
import java.util.stream.Collectors;
import java.util.stream.Stream;
Stream<Integer> numbers = Stream.of(1, 2, 3);
String line = numbers
.map(Object::toString)
.collect(Collectors.joining(", "));
System.out.println(line); // 1, 2, 3
與「手動」做法的比較
在 Stream API 之前:
import java.util.ArrayList;
import java.util.List;
List<String> merged = new ArrayList<>(listA);
merged.addAll(listB);
使用 Stream API:
import java.util.stream.Collectors;
List<String> merged = Stream.concat(listA.stream(), listB.stream())
.collect(Collectors.toList());
使用串流的好處:你可以在合併之前或之後串接中間操作(filter、map 等),也能與各種資料來源搭配。
7. 合併串流時的常見錯誤
錯誤 1:重複使用同一個串流。 串流只能使用一次。不可把同一個串流與自身合併。
import java.util.stream.Stream;
Stream<String> s = Stream.of("a", "b");
Stream<String> merged = Stream.concat(s, s); // 錯誤!s 會被重複使用
錯誤 2:試圖將無限串流與有限串流合併。 如果第一個串流是無限的,第二個永遠不會開始。
import java.util.stream.Stream;
Stream<Integer> infinite = Stream.generate(() -> 1);
Stream<Integer> finite = Stream.of(2, 3);
Stream<Integer> merged = Stream.concat(infinite, finite);
// merged.limit(10).forEach(System.out::println); // 有限串流不會出現在結果中
錯誤 3:對非字串使用 joining。 收集器 Collectors.joining 需要字串串流。對數字請先轉成字串:
import java.util.List;
import java.util.stream.Collectors;
List<Integer> numbers = List.of(1, 2, 3);
// numbers.stream().collect(Collectors.joining(", ")); // 編譯錯誤!
String joined = numbers.stream()
.map(Object::toString)
.collect(Collectors.joining(", "));
錯誤 4:順序被破壞。 如果順序很重要,請使用 concat 或維持順序的 flatMap。像 unordered() 這類方法可能會破壞可預期的輸出順序。
GO TO FULL VERSION