CodeGym /課程 /JAVA 25 SELF /透過 Stream 轉換集合

透過 Stream 轉換集合

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

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() 收集為 ListSet

方法 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. 終端與中間操作:複習一下概念

中間操作filtermapdistinctsortedlimitskippeek)——會回傳新的 Stream,可串接為管線。

終端操作forEachcollectcountanyMatchallMatchnoneMatchreducefindFirstfindAny)——會終結串流,之後就不會再有結果!

範例管線:

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:嘗試重複使用同一個串流
串流只能使用一次。執行過終端操作(例如 collectforEach)之後,若嘗試繼續使用同一個 Stream,將會導致 IllegalStateException

錯誤 №4:違反「無副作用」原則
中間操作應該是「純」的(不應修改外部變數)。不要在 mapfilter 中更動串流之外的狀態。

錯誤 №5:忽略 SetMap 的順序特性
若元素順序很重要,請使用對應的集合——例如 LinkedHashSetTreeMap——並指定合適的收集器。

1
任務
JAVA 25 SELF, 等級 30, 課堂 4
上鎖
統計會議的唯一參與者 🧑‍🤝‍🧑
統計會議的唯一參與者 🧑‍🤝‍🧑
1
任務
JAVA 25 SELF, 等級 30, 課堂 4
上鎖
建立使用者個人檔案清單(優先保留首次出現的記錄) 👤
建立使用者個人檔案清單(優先保留首次出現的記錄) 👤
1
問卷/小測驗
Stream API 基礎,等級 30,課堂 4
未開放
Stream API 基礎
Stream API 基礎
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION