Stream 操作是 Java 8+ 的大招。三個最常用的場景:合併多個 Map、把 List 轉成 Map、排序物件。一次整理出來,以後就 copy-paste 吧。

1. Stream 合併多個 Map

經常要把好幾個 Map 合起來,最直白的做法是逐個 put:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Map<String, Integer> map1 = new HashMap<>();
map1.put("Apple", 10);
map1.put("Banana", 20);

Map<String, Integer> map2 = new HashMap<>();
map2.put("Orange", 15);
map2.put("Pear", 25);

Map<String, Integer> map3 = new HashMap<>();
map3.put("Cherry", 30);

// 爛做法
Map<String, Integer> merged = new HashMap<>(map1);
merged.putAll(map2);
merged.putAll(map3);

用 Stream 的做法更優雅:

1
2
3
4
5
6
Map<String, Integer> merged = Stream.of(map1, map2, map3)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue
));

邏輯:

  • Stream.of(map1, map2, map3) - 產生一個包含 3 個 Map 的 stream
  • .flatMap(map -> map.entrySet().stream()) - 把每個 Map 拆成 entry stream,再 flatten
    • 結果是一堆 Map.Entry<String, Integer>
  • .collect(Collectors.toMap(...)) - 把 entries 轉回 Map
    • Key extractor: Map.Entry::getKey
    • Value extractor: Map.Entry::getValue

如果有重複的 key 怎麼辦

預設會爆 IllegalStateException。可以指定 merge 函數:

1
2
3
4
5
6
7
Map<String, Integer> merged = Stream.of(map1, map2, map3)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(oldValue, newValue) -> oldValue + newValue // 重複 key 時,把值相加
));

或者只保留新的:

1
2
3
4
5
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(oldValue, newValue) -> newValue // 新的值覆蓋舊的
))

2. Stream 將 List 轉 Map

很常見的場景:有個物件列表,要轉成 Map,key 是 id,value 是物件本身(或某個欄位)。

基本用法

假設有個 User 物件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class User {
private int id;
private String name;
private String email;

public int getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
}

List<User> users = Arrays.asList(
new User(1, "Alice", "alice@example.com"),
new User(2, "Bob", "bob@example.com"),
new User(3, "Cathy", "cathy@example.com")
);

轉 Map(key=id, value=User):

1
2
3
4
5
6
7
8
Map<Integer, User> userMap = users.stream()
.collect(Collectors.toMap(
User::getId, // key extractor
Function.identity() // value extractor,直接用物件本身
));

// 查詢
User user = userMap.get(1); // 拿到 Alice

轉成 key=id, value=name

1
2
3
4
5
6
7
Map<Integer, String> idToName = users.stream()
.collect(Collectors.toMap(
User::getId,
User::getName
));

// 結果:{1="Alice", 2="Bob", 3="Cathy"}

如果 List 裡有重複的 id

爆 exception。加上 merge function:

1
2
3
4
5
6
Map<Integer, User> userMap = users.stream()
.collect(Collectors.toMap(
User::getId,
Function.identity(),
(existing, replacement) -> existing // 保留第一個
));

或者只保留最後一個:

1
2
3
4
5
.collect(Collectors.toMap(
User::getId,
Function.identity(),
(existing, replacement) -> replacement
))

3. Stream 物件排序

單欄位排序

假設要按 name 排序:

1
2
3
4
5
List<User> sorted = users.stream()
.sorted(Comparator.comparing(User::getName))
.collect(Collectors.toList());

// 結果:[Alice, Bob, Cathy]

反序排列:

1
2
3
4
5
List<User> sorted = users.stream()
.sorted(Comparator.comparing(User::getName).reversed())
.collect(Collectors.toList());

// 結果:[Cathy, Bob, Alice]

多欄位排序

常見場景:先按部門排,再按名字排。用 thenComparing()

1
2
3
4
List<User> sorted = users.stream()
.sorted(Comparator.comparing(User::getDepartment)
.thenComparing(User::getName))
.collect(Collectors.toList());

複雜點的例子:先按 department 升序,再按 salary 降序:

1
2
3
4
List<User> sorted = users.stream()
.sorted(Comparator.comparing(User::getDepartment)
.thenComparing(Comparator.comparingInt(User::getSalary).reversed()))
.collect(Collectors.toList());

特殊型態排序

Integer/Long:

1
2
3
4
5
6
// 升序
.sorted(Comparator.comparingInt(User::getAge))
.sorted(Comparator.comparingLong(User::getId))

// 降序
.sorted(Comparator.comparingInt(User::getAge).reversed())

Double/Float:

1
.sorted(Comparator.comparingDouble(User::getScore))

根據 Null 處理排序

如果有些欄位可能是 null,要特別處理:

1
2
3
4
5
// null 優先
.sorted(Comparator.nullsFirst(Comparator.comparing(User::getDepartment)))

// null 最後
.sorted(Comparator.nullsLast(Comparator.comparing(User::getDepartment)))

實戰例子

把這三個操作組合起來:

例子 1:合併多個 user list,去掉重複,按 id 排序

1
2
3
4
5
6
7
8
9
List<User> mergedUsers = Stream.of(users1, users2, users3)
.flatMap(List::stream)
.collect(Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparingInt(User::getId))),
ArrayList::new
))
.stream()
.sorted(Comparator.comparingInt(User::getId))
.collect(Collectors.toList());

例子 2:List 轉 Map,然後排序值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Map<String, Integer> scoreMap = users.stream()
.collect(Collectors.toMap(
User::getName,
User::getScore,
(s1, s2) -> Math.max(s1, s2) // 重複 name 時取最高分
));

// 按分數排序(Map 無法直接排序,要轉成 List)
List<Map.Entry<String, Integer>> sorted = scoreMap.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.collect(Collectors.toList());

// 最高分的名單
sorted.forEach(entry -> System.out.println(entry.getKey() + ": " + entry.getValue()));

例子 3:按多欄位排序,然後轉成 Map

1
2
3
4
5
6
7
8
9
Map<Integer, User> sortedUsers = users.stream()
.sorted(Comparator.comparing(User::getDepartment)
.thenComparing(User::getName))
.collect(Collectors.toMap(
User::getId,
Function.identity(),
(a, b) -> a,
LinkedHashMap::new // 用 LinkedHashMap 保持排序順序
));

注意最後一個參數 LinkedHashMap::new,這樣 Map 會保持 stream 的排序順序(HashMap 不會)。

效能提示

  • 合併 Map - 如果 Map 超級大,考慮直接 putAll 而不是 stream
  • List 轉 Map - 如果 List 有上百萬元素,stream 會有開銷,直接 for loop 可能更快
  • 排序 - Stream sorted() 是 O(n log n),大資料集要小心

但大多數時候,stream 的可讀性遠勝效能損失。除非是效能熱點,否則就用 stream 吧。

總結

三大絕招:

1
2
3
4
5
6
7
8
9
10
// 合併 Map
Stream.of(map1, map2).flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(E::getKey, E::getValue))

// List 轉 Map
list.stream().collect(Collectors.toMap(User::getId, Function.identity()))

// 排序
list.stream().sorted(Comparator.comparing(User::getName).thenComparing(User::getAge))
.collect(Collectors.toList())

組合起來用,就沒有搞不定的集合操作。