← 返回题目列表

Stream 的 Collectors 收集器怎么用?groupingBy、toMap、joining 这些各解决什么?

高频 中等 第 10 / 24 题 更新于 2026/08/03
Collectors收集器StreamgroupingBy

简化版

**Collectors 是 Stream 终端操作 collect() 的「收集策略工厂」——它提供一堆现成的收集器,把流「归拢」成 List、Set、Map、字符串、统计值等。**常用的:toList()/toSet()——收集成 List/Set;toMap(keyFn, valueFn)——收集成 Map(key 重复会抛异常,要传第三个「合并函数」处理冲突);groupingBy(classifier)——按某个字段「分组」成 Map<K, List<T>>(如按部门分组员工),是最强大最常用的;joining(", ")——把流拼成一个字符串(用分隔符连接);counting()/summingInt()/averagingInt()——统计(计数、求和、平均);partitioningBy(predicate)——按条件「二分」成 Map<Boolean, List<T>>关键进阶groupingBy 能配「下游收集器(downstream)」做「分组 + 再统计」——比如 groupingBy(部门, counting()) 就是「每个部门有几个人」、groupingBy(部门, summingInt(工资)) 就是「每个部门的工资总和」。核心:Collectors 把「分组、聚合、转换」这些常见的数据归拢操作标准化了。

详细版

常用 Collectors

收集器作用结果
toList()/toSet()收集成集合List/Set
toMap(k, v)收集成 MapMap(key 冲突要合并函数)
groupingBy(fn)按字段分组Map<K, List>
partitioningBy(pred)按条件二分Map<Boolean, List>
joining(sep)拼接字符串String
counting()计数Long
summingInt/averagingInt求和/平均Integer/Double
mapping(fn, downstream)先转换再收集取决于 downstream
List<Employee> emps = ...;

// 分组:按部门 → Map<部门, List<员工>>
Map<String, List<Employee>> byDept =
    emps.stream().collect(Collectors.groupingBy(Employee::getDept));

// 分组 + 计数:每个部门几个人
Map<String, Long> deptCount =
    emps.stream().collect(Collectors.groupingBy(
        Employee::getDept, Collectors.counting()));

// 分组 + 求和:每个部门工资总和
Map<String, Integer> deptSalary =
    emps.stream().collect(Collectors.groupingBy(
        Employee::getDept, Collectors.summingInt(Employee::getSalary)));

// toMap:id → 员工(key 冲突用后者覆盖)
Map<Long, Employee> byId =
    emps.stream().collect(Collectors.toMap(
        Employee::getId, e -> e, (a, b) -> b));

// joining:名字拼成字符串
String names = emps.stream().map(Employee::getName)
    .collect(Collectors.joining(", ", "[", "]"));  // "[Tom, Jerry]"

// partitioningBy:按是否成年二分
Map<Boolean, List<Employee>> parts =
    emps.stream().collect(Collectors.partitioningBy(e -> e.getAge() >= 18));

⚠️ toMap 有个高频坑:key 重复会直接抛 IllegalStateException——Collectors.toMap(keyFn, valueFn) 只有两个参数时,如果两个元素算出同样的 key,会抛「Duplicate key」异常。所以只要不能 100% 保证 key 唯一,就必须传第三个参数「合并函数」告诉它怎么处理冲突((a, b) -> b 用后者覆盖、(a, b) -> a 保留前者、或合并两者)。另一个进阶点是 groupingBy 的「下游收集器」——它是把「分组」和「对每组再做处理」组合起来的关键:groupingBy(分类, 下游) 会先按分类分组,再对每组的元素用「下游收集器」处理(计数、求和、再映射、甚至再分组做多级分组)。理解「groupingBy 可以嵌套下游收集器」,就能用一行代码完成「分组 + 聚合」这类以前要写好几层循环的操作。

完整版教学

一、collect 与 Collectors 的关系

先理解 collectCollectors 的分工:

Stream 的终端操作 collect(Collector):
  把流"收集/归拢"成一个结果(List、Map、String、数字...)
  collect 需要一个 Collector(收集器)告诉它"怎么归拢"

Collectors 是"收集器工厂":
  提供一堆现成的 Collector:
    Collectors.toList()、Collectors.groupingBy(...)、Collectors.joining(...)
  → 你不用自己实现 Collector,用现成的

关系:
  stream.collect(Collectors.toList())
         ↑终端操作      ↑收集策略(怎么归拢)

所以:
  collect = "执行归拢"
  Collectors = "提供各种归拢策略"
  → 想怎么归拢,就选对应的 Collectors 方法

collect(Collector) 是 Stream 的终端操作(把流归拢成结果),需要一个 Collector 告诉它怎么归拢Collectors 是收集器工厂(提供现成的 Collector:toList/groupingBy/joining 等)。所以 stream.collect(Collectors.toList()) 里 collect 执行归拢、Collectors 提供策略。理解「collect 是终端操作执行归拢、Collectors 是收集器工厂提供各种归拢策略、想怎么归拢选对应的 Collectors 方法」,就理解了两者的分工。

二、基础收集:toList/toSet/toMap

最基础的收集是「归拢成集合」:

toList() / toSet():
  stream.collect(Collectors.toList())  → List
  stream.collect(Collectors.toSet())   → Set(去重)
  (Java 16+ 有更简洁的 stream.toList())

toMap(keyFn, valueFn):
  收集成 Map,指定 key 和 value 怎么来
  Map<Long, String> idToName = stream.collect(
      Collectors.toMap(Emp::getId, Emp::getName));

★ toMap 的 key 冲突坑:
  如果两个元素算出同样的 key → 抛 IllegalStateException(Duplicate key)
  必须传第三个"合并函数"处理冲突:
    toMap(key, value, (a, b) -> b)   // 后者覆盖前者
    toMap(key, value, (a, b) -> a)   // 保留前者
    toMap(key, value, (a, b) -> a + b) // 合并
  → 只要 key 不保证唯一,就要传合并函数

  还能指定 Map 类型(第四参数):
    toMap(k, v, merge, TreeMap::new)  // 用 TreeMap

基础收集:toList()/toSet()(归拢成 List/Set,Java 16+ 有 stream.toList())、toMap(keyFn, valueFn)(归拢成 Map)。toMap 的 key 冲突坑:两个元素算出同 key 会抛 IllegalStateException,必须传第三个「合并函数」((a,b)->b 后者覆盖、(a,b)->a 保留前者、(a,b)->a+b 合并)——只要 key 不保证唯一就要传(还可第四参数指定 Map 类型)。理解「toList/toSet 归拢集合、toMap 归拢 Map、★toMap key 冲突抛异常必须传合并函数((a,b)->b 覆盖等)、key 不保证唯一就要传」,就掌握了基础收集和 toMap 的坑。

三、分组:groupingBy

groupingBy 是最强大最常用的收集器——「按字段分组」:

groupingBy(classifier):
  按分类函数的结果分组,得到 Map<分类, List<元素>>
  Map<String, List<Emp>> byDept = stream.collect(
      groupingBy(Emp::getDept));
  → {"研发": [员工1, 员工2], "销售": [员工3], ...}

相当于 SQL 的 GROUP BY:
  把元素按某个字段"归堆",同一堆的放一个 List

groupingBy 的三种重载:
  ① groupingBy(分类):Map<K, List<T>>
  ② groupingBy(分类, 下游收集器):Map<K, 下游结果>
     → 分组后对每组再处理(见下节,最强大)
  ③ groupingBy(分类, Map工厂, 下游):指定 Map 类型 + 下游

为什么 groupingBy 强大:
  数据分析常需要"按某维度分组,再对每组聚合"
  groupingBy + 下游收集器 一行搞定(替代多层循环)

groupingBy(classifier) 是最常用的收集器——按分类函数的结果分组得到 Map<分类, List<元素>>(相当于 SQL 的 GROUP BY,把元素按字段归堆)。三种重载:groupingBy(分类)Map<K,List<T>>)、groupingBy(分类, 下游收集器)(分组后对每组再处理,最强大)、groupingBy(分类, Map工厂, 下游)。它强大在「按维度分组 + 对每组聚合一行搞定」(替代多层循环)。理解「groupingBy 按字段分组成 Map<K,List>(相当于 GROUP BY)、三种重载、配下游收集器可分组后再处理最强大」,就掌握了分组的核心。

四、下游收集器:分组 + 聚合

groupingBy 的精髓是「下游收集器」——分组后对每组再处理:

groupingBy(分类, 下游收集器):
  先按分类分组,再对每组的元素用"下游收集器"处理

常见组合:
  ① 分组 + 计数(每组几个):
     groupingBy(Emp::getDept, counting())
     → Map<部门, Long>  {"研发": 2, "销售": 1}

  ② 分组 + 求和(每组工资总和):
     groupingBy(Emp::getDept, summingInt(Emp::getSalary))
     → Map<部门, Integer>

  ③ 分组 + 平均:
     groupingBy(Emp::getDept, averagingInt(Emp::getSalary))

  ④ 分组 + 映射(每组只要名字):
     groupingBy(Emp::getDept, mapping(Emp::getName, toList()))
     → Map<部门, List<名字>>

  ⑤ 多级分组(分组再分组):
     groupingBy(Emp::getDept, groupingBy(Emp::getLevel))
     → Map<部门, Map<级别, List<Emp>>>

  ⑥ 分组 + 求最大(每组工资最高的):
     groupingBy(Emp::getDept, maxBy(comparingInt(Emp::getSalary)))

下游收集器让"分组 + 任意聚合"成为可能
  → 数据分析的利器(一行完成复杂统计)

groupingBy 的精髓是「下游收集器」——分组后对每组元素用下游收集器处理:分组+计数(counting())、分组+求和(summingInt)、分组+平均(averagingInt)、分组+映射(mapping(fn, toList()) 每组只要某字段)、多级分组(groupingBygroupingBy)、分组+求最大(maxBy。下游收集器让「分组 + 任意聚合」一行搞定,是数据分析利器。理解「下游收集器让分组后再处理:分组+计数/求和/平均/映射/多级分组/求最大、一行完成复杂统计」,就掌握了 groupingBy 最强大的能力。

五、其他常用收集器

补充其他常用收集器:

joining(拼字符串):
  joining()            直接拼
  joining(", ")        用分隔符
  joining(", ", "[", "]")  分隔符 + 前后缀
  → stream.map(...).collect(joining(", "))  拼成 "a, b, c"

partitioningBy(二分):
  按 predicate 把流分成两组(true 组、false 组)
  Map<Boolean, List<T>> = partitioningBy(e -> e.getAge() >= 18)
  → {true: [成年人], false: [未成年人]}
  比 groupingBy 更专门(只分两组,key 固定是 true/false)

统计收集器:
  counting()            计数 → Long
  summingInt/Long/Double(fn)      求和
  averagingInt/Long/Double(fn)    平均
  summarizingInt(fn)    一次性拿 count/sum/min/max/average(IntSummaryStatistics)
  minBy/maxBy(comparator)  最小/最大 → Optional

reducing:
  更通用的归约(自己定义怎么合并)
  → 大多数场景有专门的收集器,reducing 用得少

collectingAndThen(收集后再变换):
  collectingAndThen(toList(), Collections::unmodifiableList)
  → 收集成 List 后再转成不可变 List

其他常用收集器:joining(分隔符, 前缀, 后缀)(拼字符串)、partitioningBy(predicate)(按条件二分成 Map<Boolean, List<T>>,比 groupingBy 专门、只两组)、统计收集器counting/summingInt/averagingInt/summarizingInt 一次拿 count/sum/min/max/avg、minBy/maxBy)、collectingAndThen(收集后再变换,如转不可变)。理解「joining 拼字符串、partitioningBy 按条件二分(只两组 key 是 true/false)、统计收集器(counting/summing/summarizing/minBy/maxBy)、collectingAndThen 收集后变换」,就掌握了其他常用收集器。

六、实践与选择

总结 Collectors 的实践和选择:

选择速查:
  归拢成 List/Set        → toList()/toSet()
  归拢成 Map             → toMap(注意 key 冲突传合并函数)
  按字段分组             → groupingBy
  分组 + 聚合            → groupingBy + 下游收集器
  按条件二分             → partitioningBy
  拼字符串               → joining
  计数/求和/平均/统计     → counting/summingInt/summarizingInt

实践建议:
  ① toMap 只要 key 不绝对唯一,一律传合并函数(防 Duplicate key)
  ② 复杂统计优先想 groupingBy + 下游收集器(别写多层循环)
  ③ 需要 count+sum+min+max+avg 一起 → summarizingInt(一次算完)
  ④ Java 16+ 简单收集用 stream.toList()(更简洁,返回不可变)

常见误区:
  ✗ 用 groupingBy 但只分两组 → 用 partitioningBy 更合适
  ✗ toMap 忘了合并函数 → 遇到重复 key 崩溃
  ✗ 手写多层循环做分组聚合 → groupingBy 一行搞定

一句话:数据的分组、聚合、转换,先想 Collectors 有没有现成的

Collectors 选择:List/Set 用 toList/toSet、Map 用 toMap(注意合并函数)、分组用 groupingBy、分组+聚合用 groupingBy+下游、二分用 partitioningBy、拼字符串用 joining、统计用 counting/summing/summarizing。实践:toMap 不绝对唯一就传合并函数、复杂统计优先 groupingBy+下游(别写多层循环)、要多个统计值用 summarizingInt。理解「选择:List/Set→toList、Map→toMap(合并函数)、分组→groupingBy、分组聚合→groupingBy+下游、二分→partitioningBy、拼接→joining、统计→summarizing;数据分组聚合先想 Collectors 现成的」,就掌握了 Collectors 的实践选择。

记忆钩子:「Collectors=collect 的收集策略工厂;基础:toList/toSet、toMap(keyFn,valueFn)★key 冲突抛 IllegalStateException 必须传合并函数((a,b)->b 覆盖);groupingBy(分类)按字段分组成 Map<K,List>(相当于 GROUP BY),精髓是下游收集器:groupingBy(分类,下游)分组后再处理(counting 计数/summingInt 求和/averagingInt 平均/mapping 映射/groupingBy 多级分组/maxBy 求最大)一行完成复杂统计;partitioningBy 按条件二分(只 true/false 两组);joining 拼字符串(分隔符+前后缀);summarizingInt 一次拿 count/sum/min/max/avg;数据分组聚合先想 Collectors 现成的」

七、常见误区与追问

  • 误区:Collectors.toMap 遇到重复 key 会自动覆盖。 不会——只传两个参数(keyFn、valueFn)时遇到重复 key 抛 IllegalStateException(Duplicate key);必须传第三个合并函数((a,b)->b 后者覆盖、(a,b)->a 前者保留、(a,b)->a+b 合并);只要 key 不绝对唯一就要传。
  • 误区:分组聚合要写多层嵌套循环。 用 groupingBy + 下游收集器一行搞定——groupingBy(分类, counting()) 是每组计数、groupingBy(分类, summingInt(fn)) 是每组求和、groupingBy(分类, groupingBy(…)) 是多级分组;比手写循环简洁且不易错。
  • 误区:groupingBy 和 partitioningBy 一样。 groupingBy 按分类函数的任意结果分组(可能很多组);partitioningBy 按 predicate 只分两组(key 固定是 true/false);只需二分(成年/未成年、及格/不及格)时用 partitioningBy 更合适、更明确。
  • 误区:collect 和 Collectors 是一回事。 collect 是 Stream 的终端操作(执行归拢),Collectors 是收集器工厂(提供归拢策略);stream.collect(Collectors.toList()) 里 collect 干活、Collectors.toList() 提供「归拢成 List」的策略。
  • 追问:groupingBy 的下游收集器有什么用? 分组之后对每组的元素再做处理——分组+计数(counting)、分组+求和(summingInt)、分组+平均(averagingInt)、分组+只取某字段(mapping(fn, toList()))、分组再分组(多级分组)、分组+求组内最大(maxBy);它让「按维度分组 + 对每组聚合」一行代码完成,是数据分析的利器。
  • 追问:想同时得到每组的 count、sum、min、max、average 怎么办? 用 summarizingInt/Long/Double 收集器——groupingBy(分类, summarizingInt(Emp::getSalary)) 返回 Map<分类, IntSummaryStatistics>,IntSummaryStatistics 一个对象里就有 getCount/getSum/getMin/getMax/getAverage,一次遍历算完所有统计值,不用分别求。
  • 追问:toMap 怎么指定返回的 Map 类型(如 TreeMap)? 用四参数的 toMap——toMap(keyFn, valueFn, mergeFn, mapSupplier),第四个参数传 Map 的构造器如 TreeMap::new,就能收集成 TreeMap(有序)或 LinkedHashMap(保持顺序)等;groupingBy 也有类似的带 Map 工厂的重载。

八、加强记忆

Collectors 是 Stream 终端操作 collect() 的「收集策略工厂」——提供现成的收集器把流归拢成 List/Set/Map/字符串/统计值。基础toList()/toSet()(Java 16+ 有 stream.toList())、toMap(keyFn, valueFn)key 冲突抛 IllegalStateException,只要 key 不绝对唯一就必须传第三个「合并函数」 (a,b)->b 覆盖/(a,b)->a 保留/(a,b)->a+b 合并)。groupingBy(classifier) 是最常用的——按字段分组成 Map<K, List<T>>(相当于 SQL 的 GROUP BY,精髓是下游收集器groupingBy(分类, 下游) 分组后对每组再处理——counting() 计数、summingInt(fn) 求和、averagingInt 平均、mapping(fn, toList()) 每组只取某字段、groupingBygroupingBy 多级分组、maxBy 求组内最大——一行完成复杂统计(替代多层循环)partitioningBy(predicate) 按条件二分(只 true/false 两组)。joining(分隔符, 前缀, 后缀) 拼字符串。summarizingInt(fn) 一次拿 count/sum/min/max/average(IntSummaryStatistics)。一句话「Collectors 是 collect 的收集策略工厂:toList/toSet/toMap(key 冲突必须传合并函数)、groupingBy 按字段分组(GROUP BY)配下游收集器做分组+聚合(counting/summingInt/mapping/多级/maxBy 一行完成复杂统计)、partitioningBy 二分、joining 拼串、summarizing 一次拿全部统计」。