在Java中如何使用Collectors收集流结果_Java流聚合工具说明

Collectors.toList() 返回有序可变ArrayList(JDK15前)或不可修改视图(JDK16+),toSet() 返回无序HashSet且不支持null;groupingBy分类函数返回null会直接抛NPE;averagingDouble等对空流返回默认值易掩盖数据缺失;自定义Collector用于并行流时combiner须满足结合律且无副作用。

Collectors.toList() 和 toSet() 的行为差异

调用 Collectors.toList() 会返回一个可变的 ArrayList,元素顺序与流中出现顺序一致;而 Collectors.toSet() 返回的是无序、去重的 HashSet(JDK 11+ 默认),不保证插入顺序,且无法处理 null 元素(会抛 NullPointerException)。

  • 若需有序且去重,改用 Collectors.toCollection(LinkedHashSet::new)
  • 若流中可能含 null,先用 filter(Objects::nonNull) 过滤,否则 toSet() 在终端操作时才报错,不易定位
  • toList() 在 JDK 16+ 是不可修改视图(UnsupportedOperationException),但 JDK 15 及之前返回的是真实 ArrayList,注意版本兼容性

用 Collectors.groupingBy 分组时的空指针风险

groupingBy 的分类函数(classifier)若返回 null,会直接触发 NullPointerException,不是静默跳过。这是最常被忽略的崩溃点。

  • 安全写法:用 groupingBy(e -> e.getType() != null ? e.getType() : "UNKNOWN")
  • 如需保留 null 键,改用 groupingBy(Objects::toString, HashMap::new, toList()) 并配合自定义 map 工厂
  • 避免嵌套使用 groupingBy + mapping 时传入 null 值——mapping 的映射函数同样不接受 null 输入

Collectors.averagingDouble 等统计收集器的类型陷阱

averagingDoublesummingInt 等统计收集器要求输入流元素能被明确转换为对应数值类型,且对空流返回默认值(如 averagingDouble 返回 0.0),但不会报错,容易掩盖数据缺失问题。

  • 若字段本身是 Double 包装类,用 averagingDouble(Double::doubleValue);若字段为 String 数字,必须先 map(Double::parseDouble),否则运行时报 NumberFormatException
  • 空流时 averagingDouble 返回 0.0,但业务上可能期望 null 或抛异常——此时应改用 collectingAndThen(averagingDouble(...), d -> d == 0.0 && stream.isEmpty() ? null : d)(需提前缓存流为 Iterable 或用 Stream.peek() 计数辅助判断)
  • 整数溢出风险:summingIntint 求和不检查溢出,大数量级建议用 summingLongreducing 配合 Math.addExact

自定义 Collector 的性能与线程安全边界

Collector.of() 手写 collector 时,combiner 函数仅在并行流中被调用,且必须满足结合律;若忽略这一点,parallelStream().collect(customCollector) 可能返回错误结果,而非抛异常。

  • supplier 必须每次返回新容器(如 () -> new ArrayList()),不能复用静态实例
  • combiner 必须是无副作用的纯函数,例如 (left, ri

    ght) -> { left.addAll(right); return left; }
    是错的(修改了 left),应写成 (left, right) -> Stream.concat(left.stream(), right.stream()).collect(toList())
  • 如果 collector 用于 parallelStream(),确保 accumulatorcombiner 不依赖外部状态或共享变量,否则结果不可预测
Collector> wordLengthCount = Collector.of(
    () -> new HashMap<>(),
    (map, word) -> map.merge(word.length(), 1L, Long::sum),
    (m1, m2) -> {
        m2.forEach((len, count) -> m1.merge(len, count, Long::sum));
        return m1;
    }
);

上面这个 collector 在并行下是安全的,因为 merge 是原子操作,且 combiner 中没有修改传入的 m2

真正难调试的,是 combiner 里悄悄修改了 supplier 创建的原始容器,或者 accumulator 用了非线程安全的集合——这种 bug 在单测里几乎不暴露,只在压测或生产流量高峰时浮现。