Java Collectors

Collectors 類別提供許多預設的收集器,用於將 Stream 結果收集到各種容器中。

收集到集合

import java.util.stream.Collectors;

List<String> names = stream.collect(Collectors.toList());
Set<String> uniqueNames = stream.collect(Collectors.toSet());

// 指定集合類型
ArrayList<String> arrayList = stream.collect(
    Collectors.toCollection(ArrayList::new));
LinkedHashSet<String> linkedHashSet = stream.collect(
    Collectors.toCollection(LinkedHashSet::new));
TreeSet<String> treeSet = stream.collect(
    Collectors.toCollection(TreeSet::new));

// Java 10+
List<String> unmodifiableList = stream.collect(Collectors.toUnmodifiableList());
Set<String> unmodifiableSet = stream.collect(Collectors.toUnmodifiableSet());

收集到 Map

toMap

// 基本用法
Map<Integer, String> map = users.stream()
    .collect(Collectors.toMap(
        User::getId,      // key
        User::getName     // value
    ));

// key 重複時的處理
Map<String, User> map = users.stream()
    .collect(Collectors.toMap(
        User::getName,
        user -> user,
        (existing, replacement) -> existing  // 保留舊值
    ));

// 指定 Map 類型
LinkedHashMap<Integer, String> linkedMap = users.stream()
    .collect(Collectors.toMap(
        User::getId,
        User::getName,
        (a, b) -> a,
        LinkedHashMap::new
    ));

物件轉 Map

// ID -> 物件
Map<Integer, User> userById = users.stream()
    .collect(Collectors.toMap(User::getId, Function.identity()));

// 屬性 -> 物件(使用 Function.identity())
Map<String, User> userByEmail = users.stream()
    .collect(Collectors.toMap(User::getEmail, u -> u));

字串連接

// 基本連接
String result = names.stream()
    .collect(Collectors.joining());  // "AliceBobCharlie"

// 使用分隔符
String result = names.stream()
    .collect(Collectors.joining(", "));  // "Alice, Bob, Charlie"

// 前綴、分隔符、後綴
String result = names.stream()
    .collect(Collectors.joining(", ", "[", "]"));  // "[Alice, Bob, Charlie]"

分組

groupingBy

// 基本分組
Map<String, List<User>> byCity = users.stream()
    .collect(Collectors.groupingBy(User::getCity));

// 多級分組
Map<String, Map<String, List<User>>> byCityAndDept = users.stream()
    .collect(Collectors.groupingBy(
        User::getCity,
        Collectors.groupingBy(User::getDepartment)
    ));

// 分組並計數
Map<String, Long> countByCity = users.stream()
    .collect(Collectors.groupingBy(
        User::getCity,
        Collectors.counting()
    ));

// 分組並求和
Map<String, Integer> ageByCity = users.stream()
    .collect(Collectors.groupingBy(
        User::getCity,
        Collectors.summingInt(User::getAge)
    ));

// 分組並取得其他資訊
Map<String, Optional<User>> oldestByCity = users.stream()
    .collect(Collectors.groupingBy(
        User::getCity,
        Collectors.maxBy(Comparator.comparing(User::getAge))
    ));

// 分組並轉換
Map<String, List<String>> namesByCity = users.stream()
    .collect(Collectors.groupingBy(
        User::getCity,
        Collectors.mapping(User::getName, Collectors.toList())
    ));

// 指定 Map 類型
TreeMap<String, List<User>> sortedByCity = users.stream()
    .collect(Collectors.groupingBy(
        User::getCity,
        TreeMap::new,
        Collectors.toList()
    ));

分區

partitioningBy

分成兩組(true/false):

// 基本分區
Map<Boolean, List<User>> partition = users.stream()
    .collect(Collectors.partitioningBy(u -> u.getAge() >= 18));

List<User> adults = partition.get(true);
List<User> minors = partition.get(false);

// 分區並計數
Map<Boolean, Long> count = users.stream()
    .collect(Collectors.partitioningBy(
        u -> u.getAge() >= 18,
        Collectors.counting()
    ));

數值統計

// 計數
long count = stream.collect(Collectors.counting());

// 求和
int sum = stream.collect(Collectors.summingInt(User::getAge));
long sum = stream.collect(Collectors.summingLong(User::getScore));
double sum = stream.collect(Collectors.summingDouble(User::getBalance));

// 平均
Double avg = stream.collect(Collectors.averagingInt(User::getAge));
Double avg = stream.collect(Collectors.averagingDouble(User::getBalance));

// 統計摘要
IntSummaryStatistics stats = users.stream()
    .collect(Collectors.summarizingInt(User::getAge));
// stats.getCount(), stats.getSum(), stats.getMin(), stats.getMax(), stats.getAverage()

最大/最小

Optional<User> oldest = users.stream()
    .collect(Collectors.maxBy(Comparator.comparing(User::getAge)));

Optional<User> youngest = users.stream()
    .collect(Collectors.minBy(Comparator.comparing(User::getAge)));

歸約

Optional<String> concat = names.stream()
    .collect(Collectors.reducing((a, b) -> a + b));

String concat = names.stream()
    .collect(Collectors.reducing("", (a, b) -> a + b));

mapping / flatMapping

轉換後再收集:

// mapping
List<String> names = users.stream()
    .collect(Collectors.mapping(
        User::getName,
        Collectors.toList()
    ));

// 分組後 mapping
Map<String, List<String>> namesByCity = users.stream()
    .collect(Collectors.groupingBy(
        User::getCity,
        Collectors.mapping(User::getName, Collectors.toList())
    ));

// flatMapping(Java 9+)
Map<String, Set<String>> skillsByCity = users.stream()
    .collect(Collectors.groupingBy(
        User::getCity,
        Collectors.flatMapping(
            u -> u.getSkills().stream(),
            Collectors.toSet()
        )
    ));

filtering(Java 9+)

過濾後再收集:

Map<String, List<User>> adultsByCity = users.stream()
    .collect(Collectors.groupingBy(
        User::getCity,
        Collectors.filtering(
            u -> u.getAge() >= 18,
            Collectors.toList()
        )
    ));

teeing(Java 12+)

同時使用兩個收集器:

// 同時取得最大和最小
var result = users.stream()
    .collect(Collectors.teeing(
        Collectors.maxBy(Comparator.comparing(User::getAge)),
        Collectors.minBy(Comparator.comparing(User::getAge)),
        (oldest, youngest) -> Map.of("oldest", oldest, "youngest", youngest)
    ));

// 同時計算總和和計數
var result = numbers.stream()
    .collect(Collectors.teeing(
        Collectors.summingInt(Integer::intValue),
        Collectors.counting(),
        (sum, count) -> sum / (double) count  // 手動計算平均
    ));

collectingAndThen

收集後再轉換:

// 收集後轉為不可變
List<String> immutable = names.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toList(),
        Collections::unmodifiableList
    ));

// 收集後取第一個
String first = names.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toList(),
        list -> list.isEmpty() ? null : list.get(0)
    ));

自訂 Collector

// 使用 Collector.of
Collector<String, StringBuilder, String> joiningCollector =
    Collector.of(
        StringBuilder::new,              // supplier
        StringBuilder::append,           // accumulator
        (sb1, sb2) -> sb1.append(sb2),  // combiner
        StringBuilder::toString          // finisher
    );

String result = stream.collect(joiningCollector);

常用 Collectors 一覽

Collector說明
toList()收集到 List
toSet()收集到 Set
toMap()收集到 Map
toCollection()收集到指定集合
joining()連接字串
groupingBy()分組
partitioningBy()分區(二分)
counting()計數
summingInt/Long/Double()求和
averagingInt/Long/Double()平均
maxBy() / minBy()最大/最小
mapping()轉換後收集
filtering()過濾後收集
flatMapping()展開後收集
teeing()同時使用兩個收集器
collectingAndThen()收集後轉換

重點整理

  • toList()toSet()toMap() 是最常用的收集器
  • groupingBy() 用於分組,可以巢狀使用
  • partitioningBy() 將資料分成兩組
  • joining() 用於連接字串
  • mapping()filtering() 可以在收集前轉換/過濾
  • teeing() 可以同時執行兩個收集操作
  • collectingAndThen() 可以對收集結果做後處理