1. 將 List → Set 與反向轉換
先回顧一下為何需要轉換集合。在實際開發中常會需要:
- 從清單取得唯一元素(例如:電子郵件清單 → 唯一地址的集合)。
- 建立對映(Map),例如從姓名清單得到「姓名 → 姓名長度」的對映。
- 把元素串成一個字串(例如用於美觀輸出)。
過去要為此撰寫許多帶有迴圈、條件與暫存集合的程式碼。使用 Stream API 之後,一切變得更簡單,而且更優雅!
範例:從清單取得唯一姓名的集合
假設我們有一份姓名清單(也許有人在你的程式裡重複輸入了自己——很常見!):
List<String> names = List.of("安娜", "謝爾蓋", "安娜", "瑪麗亞", "伊萬", "謝爾蓋");
我們的目標是取得每個姓名只出現一次的集合,也就是 Set。使用 Stream API 幾乎只要一行:
Set<String> uniqueNames = names.stream()
.collect(Collectors.toSet());
System.out.println(uniqueNames);
輸出:
[瑪麗亞, 伊萬, 安娜, 謝爾蓋]
在 Set 中不保證順序——如果你的輸出順序不同,別意外。
那如果需要反過來: Set → List?
有時候需要把集合轉回清單(例如要排序或用索引存取):
List<String> namesList = uniqueNames.stream()
.collect(Collectors.toList());
System.out.println(namesList);
2. 轉換成 Map: Collectors.toMap()
範例:從姓名清單得到 Map「姓名 → 姓名長度」
有時不僅想當程式設計師,還想當「製圖師」——來建構對映吧!我們試試看:
List<String> names = List.of("安娜", "謝爾蓋", "瑪麗亞", "伊萬");
Map<String, Integer> nameToLength = names.stream()
.collect(Collectors.toMap(
name -> name, // 鍵 — 名字本身
name -> name.length() // 值 — 名字的長度
));
System.out.println(nameToLength);
輸出:
{瑪麗亞=5, 伊萬=4, 安娜=4, 謝爾蓋=6}
重點:鍵重複的情況
如果在原始清單中出現相同的姓名,當你嘗試收集成 Map 時,會拋出 IllegalStateException: Duplicate key。Java 不允許你對同一個鍵放入兩個值。
如何處理重複鍵?
可以指定鍵衝突時要怎麼做——例如保留第一個值或最後一個值:
List<String> names = List.of("安娜", "謝爾蓋", "安娜", "瑪麗亞", "伊萬", "謝爾蓋");
Map<String, Integer> nameToLength = names.stream()
.collect(Collectors.toMap(
name -> name,
name -> name.length(),
(oldValue, newValue) -> oldValue // 保留第一個值
));
System.out.println(nameToLength);
現在程式不會崩潰,並且在 Map 中只會保留每個姓名的第一次出現。
範例:值為物件的 Map
稍微複雜一點:我們有一份使用者清單,想建立 Map「姓名 → 使用者」:
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " (" + age + ")";
}
}
// 使用者清單範例
List<User> users = List.of(
new User("安娜", 25),
new User("謝爾蓋", 30),
new User("瑪麗亞", 22)
);
Map<String, User> nameToUser = users.stream()
.collect(Collectors.toMap(
user -> user.name,
user -> user
));
System.out.println(nameToUser);
輸出:
{瑪麗亞=瑪麗亞 (22), 安娜=安娜 (25), 謝爾蓋=謝爾蓋 (30)}
3. 連接成字串: Collectors.joining()
有時想要的不只是收集成集合,而是產生一個漂亮的輸出字串。例如,將所有姓名用逗號連接:
List<String> names = List.of("安娜", "謝爾蓋", "瑪麗亞", "伊萬");
String result = names.stream()
.collect(Collectors.joining(", "));
System.out.println(result);
輸出:
安娜, 謝爾蓋, 瑪麗亞, 伊萬
也可以加上前綴與後綴
String result = names.stream()
.collect(Collectors.joining(", ", "清單: [", "]"));
System.out.println(result);
輸出:
清單: [安娜, 謝爾蓋, 瑪麗亞, 伊萬]
4. 終端操作: forEach, collect, count, anyMatch, allMatch, noneMatch
方法 forEach
我們對 forEach 已相當熟悉:這個操作會對串流中的每個元素執行動作。
names.stream().forEach(name -> System.out.println("你好," + name + "!"));
方法 collect
用於將元素收集為集合、字串或其他結構。最常見的操作是藉由 Collectors.toList() 與 Collectors.toSet() 收集為 List 或 Set。
方法 count
計算串流中的元素數量。
long count = names.stream()
.filter(name -> name.length() > 4)
.count();
System.out.println("長度大於 4 的姓名數量: " + count);
方法 anyMatch, allMatch, noneMatch
用來檢查條件是否至少對一個元素成立(anyMatch)、對所有元素成立(allMatch),或對任一元素皆不成立(noneMatch)。
boolean hasShortName = names.stream()
.anyMatch(name -> name.length() < 4);
System.out.println("有沒有短姓名? " + hasShortName);
boolean allLong = names.stream()
.allMatch(name -> name.length() > 3);
System.out.println("所有姓名長度都大於 3 嗎? " + allLong);
boolean noneIvan = names.stream()
.noneMatch(name -> name.equals("伊萬"));
System.out.println("沒有伊萬嗎? " + noneIvan);
輸出:
有沒有短姓名? false
所有姓名長度都大於 3 嗎? true
沒有伊萬嗎? false
5. 終端與中間操作:複習一下概念
中間操作(filter、map、distinct、sorted、limit、skip、peek)——會回傳新的 Stream,可串接為管線。
終端操作(forEach、collect、count、anyMatch、allMatch、noneMatch、reduce、findFirst、findAny)——會終結串流,之後就不會再有結果!
範例管線:
List<String> result = users.stream()
.filter(user -> user.age > 20)
.map(user -> user.name.toUpperCase())
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println(result);
輸出:
[安娜, 伊萬, 瑪麗亞, 謝爾蓋]
6. 透過 Stream 轉換集合時的常見錯誤
錯誤 №1:在 toMap 中未處理重複鍵
如果在原始集合中出現重複的鍵,而你使用 Collectors.toMap() 時沒有明確指定處理方式,程式將拋出例外。遇到這種情況請務必提供合併函式:
// 保留最後一個值
.toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)
錯誤 №2:以 forEach 代替 collect
有時新手會嘗試用 forEach 來「收集」集合,例如:
List<String> list = new ArrayList<>();
names.stream().forEach(name -> list.add(name)); // 能運作,但不是 Stream 的作法!
更好的做法是使用 collect(Collectors.toList())——較安全且更乾淨。
錯誤 №3:嘗試重複使用同一個串流
串流只能使用一次。執行過終端操作(例如 collect、forEach)之後,若嘗試繼續使用同一個 Stream,將會導致 IllegalStateException。
錯誤 №4:違反「無副作用」原則
中間操作應該是「純」的(不應修改外部變數)。不要在 map 或 filter 中更動串流之外的狀態。
錯誤 №5:忽略 Set 與 Map 的順序特性
若元素順序很重要,請使用對應的集合——例如 LinkedHashSet、TreeMap——並指定合適的收集器。
GO TO FULL VERSION