集合操作是日常編程的基礎,但很多人寫得很醜。本篇涵蓋三個常見的集合操作場景:List 差集、JsonArray 分割成 List,以及 Stream 查詢計數。這些小技巧能讓代碼更簡潔。
1. List 差集
需求:從 list1 移除所有在 list2 裡出現的元素。
簡單但爛的做法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| List<String> list1 = Arrays.asList("Apple", "Banana", "Cherry", "Orange"); List<String> list2 = Arrays.asList("Banana", "Orange");
for (String item : list1) { if (list2.contains(item)) { list1.remove(item); } }
List<String> result = new ArrayList<>(); for (String item : list1) { if (!list2.contains(item)) { result.add(item); } }
|
正確的 Stream 做法
1 2 3 4 5
| List<String> result = list1.stream() .filter(s -> !list2.contains(s)) .collect(Collectors.toList());
|
很簡潔,但有個效能問題。
效能陷阱:contains 很慢
list2.contains(s) 對每個元素都要遍歷整個 list2,時間複雜度是 O(n*m)。如果 list1 有 10000 個,list2 有 1000 個,就是 1000 萬次比較。
優化做法:先轉 Set
1 2 3 4 5
| Set<String> set2 = new HashSet<>(list2);
List<String> result = list1.stream() .filter(s -> !set2.contains(s)) .collect(Collectors.toList());
|
現在是 O(n + m),快多了。
完整例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public List<String> getDifference(List<String> list1, List<String> list2) { Set<String> set2 = new HashSet<>(list2); return list1.stream() .filter(item -> !set2.contains(item)) .collect(Collectors.toList()); }
List<String> products = Arrays.asList("Apple", "Banana", "Cherry", "Orange"); List<String> sold = Arrays.asList("Banana", "Orange");
List<String> remaining = getDifference(products, sold); System.out.println(remaining);
|
2. Stream 分割 JsonArray 轉 List
從 Jackson 的 JsonNode 陣列轉成 List。
原始做法(笨重)
1 2 3 4 5 6 7 8 9 10 11 12
| JsonNode jsonArray = mapper.readTree(""" [ {"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Cathy"} ] """);
List<JsonNode> list = new ArrayList<>(); for (int i = 0; i < jsonArray.size(); i++) { list.add(jsonArray.get(i)); }
|
用 Stream 優雅做法
1 2 3 4
| List<JsonNode> list = StreamSupport.stream( jsonArray.spliterator(), false ).collect(Collectors.toList());
|
邏輯:
jsonArray 本身可以 iterable
Spliterator 是 Java 8 用來支援平行 stream 的介面
StreamSupport.stream() 把 spliterator 轉成 stream
- 第二個參數
false 代表不要平行
對象型的 JsonArray
如果陣列裡是物件,可以邊轉邊過濾:
1 2 3 4 5 6 7 8
| List<JsonNode> users = StreamSupport.stream( jsonArray.spliterator(), false ) .filter(node -> node.path("age").asInt() > 25) .collect(Collectors.toList());
|
轉成其他型態
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| List<String> names = StreamSupport.stream( jsonArray.spliterator(), false ) .map(node -> node.path("name").asText()) .collect(Collectors.toList());
Map<Integer, String> map = StreamSupport.stream( jsonArray.spliterator(), false ) .collect(Collectors.toMap( node -> node.path("id").asInt(), node -> node.path("name").asText() ));
|
3. Stream 查詢計數
需求:檢查 List 中有多少元素符合某個條件。
簡單場景
1 2 3 4 5 6 7 8 9
| List<String> types = Arrays.asList("Apple", "Banana", "Apple", "Orange", "Apple"); List<String> PRO_SURE_TYPE = Arrays.asList("Apple", "Orange");
long count = types.stream() .filter(PRO_SURE_TYPE::contains) .count();
System.out.println(count);
|
這邊用 method reference PRO_SURE_TYPE::contains,等於 type -> PRO_SURE_TYPE.contains(type),更簡潔。
複雜條件
如果要查複雜條件,比如「id > 10 且 status = ACTIVE」:
1 2 3 4 5
| List<User> users = ;
long activeCount = users.stream() .filter(user -> user.getId() > 10 && user.getStatus() == Status.ACTIVE) .count();
|
結合 Set 優化(重要)
如果 PRO_SURE_TYPE 是個 List,每次 contains() 都要遍歷,效能差。轉 Set:
1 2 3 4 5
| Set<String> proPriceTypeSet = new HashSet<>(PRO_SURE_TYPE);
long count = types.stream() .filter(proPriceTypeSet::contains) .count();
|
O(n) vs O(n*m),快很多。
不只是計數
如果要拿到符合條件的元素,不只是計數:
1 2 3 4 5 6 7 8 9
| List<String> proPriceItems = types.stream() .filter(PRO_SURE_TYPE::contains) .collect(Collectors.toList());
Set<String> uniqueProPriceItems = types.stream() .filter(PRO_SURE_TYPE::contains) .collect(Collectors.toSet());
|
實戰組合
假設有個 API 回傳商品清單,要計算「高價商品但不在銷售清單裡」的數量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| List<Product> allProducts = getProducts(); List<String> soldProductIds = getSoldList();
Set<String> soldSet = new HashSet<>(soldProductIds);
long expensiveUnsoldCount = allProducts.stream() .filter(p -> p.getPrice() > 1000) .filter(p -> !soldSet.contains(p.getId())) .count();
List<Product> expensiveUnsold = allProducts.stream() .filter(p -> p.getPrice() > 1000) .filter(p -> !soldSet.contains(p.getId())) .collect(Collectors.toList());
|
常見坑
1. List.contains() 效能差
1 2 3 4 5 6
| list1.stream().filter(item -> list2.contains(item))
Set<String> set2 = new HashSet<>(list2); list1.stream().filter(item -> set2.contains(item))
|
2. 在 Stream 中修改原始 list
1 2 3 4 5 6 7
| list.stream().forEach(item -> list.remove(item));
List<Item> removed = list.stream() .filter(condition) .collect(Collectors.toList());
|
3. 忘記 false 參數會變成平行 stream
1 2 3 4 5
| StreamSupport.stream(spliterator, false);
StreamSupport.stream(spliterator, true);
|
平行不一定快,小資料集反而更慢(因為建立執行緒的開銷)。
性能比較
假設 list1 有 10000 個元素,list2 有 1000 個:
1 2
| list2.contains() 方式:10000 * 1000 = 1000 萬次比較(慢) 先轉 Set.contains() 方式:10000 + 1000 = 11000 次操作(快)
|
百倍以上的差別。
總結
三大法則:
- List 差集 - 用 Stream filter,大 list 先轉 Set
- JsonArray 分割 - 用 StreamSupport.stream(spliterator, false)
- 計數和查詢 - 用 Stream filter 和 count,method reference 更簡潔
這些小操作看似簡單,但寫得好和寫得爛,效能能差百倍。特別是處理大資料時,千萬別亂搞。
你的鼓勵將被轉換為我明天繼續加班的動力(真的)。 ❤️