深入理解 Java Stream:从创建到过滤、归约、分组与聚合(带大量实战代码)

引言

为什么要用Stream

  • 可读性高:把"做什么"与"怎么做"区分开,代码更接近业务描述(声明式)。
  • 可组合:操作可以链式组合,减少样板代码(boilerplate)。
  • 惰性与优化:中间操作惰性求值,可实现短路与合并优化。
  • 并行化支持 :调用 parallel()parallelStream() 可以较容易地并行处理数据(需注意线程安全)。
  • 功能强:内置聚合、分组、统计、收集等能力,非常适合处理集合数据。

但也有代价:不当使用会损失性能、产生线程安全问题或破坏可预测性(例如在并行流中使用有状态的副作用操作)。

Stream 背景与特点(简要)

  • 来源:Stream 不是数据结构,不存储元素;它是对数据源(Collection、数组、I/O channel 等)的一次性操作流水线。
  • 惰性求值 :中间操作(filter, map 等)仅构建操作链;终端操作(collect, forEach 等)触发计算。
  • 可重用性限制:同一个 Stream 只能用一次(一次终端操作后关闭)。要重复操作,需要创建新的 Stream。
  • 分为中间操作(返回 Stream)和终端操作(返回结果/void)
  • 支持顺序/并行执行stream()(顺序)与 parallelStream()(并行)或 stream().parallel()

如何创建 Stream(常见方式)

java 复制代码
List<String> list = List.of("a", "b", "c");

// 1. 从集合
Stream<String> s1 = list.stream();

// 2. 从并行集合
Stream<String> s2 = list.parallelStream();

// 3. 从数组
Stream<Integer> s3 = Arrays.stream(new Integer[]{1,2,3});

// 4. 从静态工厂
Stream<String> s4 = Stream.of("x", "y", "z");

// 5. 空流
Stream<Object> empty = Stream.empty();

// 6. 无界流(生成、迭代)
Stream<Double> randoms = Stream.generate(Math::random); // 无尽流,通常再 limit()
Stream<Integer> iter = Stream.iterate(0, n -> n + 2);

// 7. 从文件/IO(NIO)
try (Stream<String> lines = Files.lines(Paths.get("data.txt"))) {
    lines.forEach(System.out::println);
}

Stream 的中间操作(常见)与代码示例

中间操作返回新的 Stream,通常是惰性的。

1) filter(过滤)

java 复制代码
List<Person> people = ...;
List<Person> adults = people.stream()
    .filter(p -> p.getAge() >= 18)
    .collect(Collectors.toList());

filter 保持元素顺序(对有序流)。

2) map(映射) & flatMap

java 复制代码
List<String> names = people.stream()
    .map(Person::getName)
    .collect(Collectors.toList());

// flatMap 示例:将 List<List<T>> 展平
List<List<String>> nested = List.of(List.of("a","b"), List.of("c"));
List<String> flat = nested.stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList());

3) distinct(去重)

基于 equals/hashCode

java 复制代码
List<Integer> unique = List.of(1,2,2,3).stream().distinct().collect(Collectors.toList()); // [1,2,3]

4) sorted(排序)

有序流或无序流均可使用。默认使用自然顺序或提供 Comparator。

java 复制代码
people.stream().sorted(Comparator.comparing(Person::getAge).reversed()).collect(...);

5) peek(调试/副作用)

用于调试或中间副作用(谨慎使用)。

java 复制代码
people.stream().filter(...).peek(System.out::println).collect(...);

6) limit / skip(截断、跳过)

短路友好,用于分页或流式处理。

java 复制代码
stream.skip(10).limit(20)...

7) parallel(并行化)与 sequential

java 复制代码
stream.parallel()... // 切为并行模式
stream.sequential()... // 切回顺序模式

终端操作(收集、聚合、匹配、查找)

遍历(forEach / forEachOrdered

java 复制代码
stream.forEach(System.out::println); // 并行流可能无顺序
stream.forEachOrdered(System.out::println); // 保证顺序(代价更高)

匹配操作 anyMatch / allMatch / noneMatch

短路操作(遇到条件就停止)。

java 复制代码
boolean hasMinor = people.stream().anyMatch(p -> p.getAge() < 18);

查找 findFirst / findAny

返回 Optional<T>

java 复制代码
Optional<Person> first = people.stream().filter(...).findFirst();

规约(reduce

通用的归约操作。

java 复制代码
// 求和
int sum = List.of(1,2,3).stream().reduce(0, Integer::sum);

// 返回 Optional
Optional<Integer> maybe = List.of(1,2,3).stream().reduce(Integer::sum);

聚合(count, max, min

java 复制代码
long cnt = people.stream().filter(...).count();
Optional<Person> oldest = people.stream().max(Comparator.comparing(Person::getAge));

收集(collect + Collectors

collect 是最强大的终端操作:

常见 Collector 用法

java 复制代码
// toList / toSet / toMap
List<String> names = people.stream().map(Person::getName).collect(Collectors.toList());

Map<Integer, List<Person>> byAge = people.stream().collect(Collectors.groupingBy(Person::getAge));

Map<Integer, String> map = people.stream()
    .collect(Collectors.toMap(Person::getId, Person::getName)); // 若 key 重复需提供 merge 函数

// joining(字符串连接)
String csv = people.stream().map(Person::getName).collect(Collectors.joining(", "));

// summarizing(统计)
IntSummaryStatistics stats = people.stream().collect(Collectors.summarizingInt(Person::getAge));
// stats.getCount(), getSum(), getAverage(), getMax(), getMin()

// averaging / summing
double avg = people.stream().collect(Collectors.averagingInt(Person::getAge));
int total = people.stream().collect(Collectors.summingInt(Person::getAge));

分组与分区(核心示例与进阶)

假设有 Order 类:

java 复制代码
record Order(long id, long userId, int amount, String status) {}
List<Order> orders = List.of(
    new Order(1, 100, 50, "PAID"),
    new Order(2, 101, 20, "CREATED"),
    new Order(3, 100, 70, "PAID"),
    new Order(4, 102, 30, "REFUND")
);
  1. 按 userId 分组
java 复制代码
Map<Long, List<Order>> byUser = orders.stream()
    .collect(Collectors.groupingBy(Order::userId));
  1. 分组后求和/统计
java 复制代码
Map<Long, Integer> amountByUser = orders.stream()
    .collect(Collectors.groupingBy(Order::userId,
             Collectors.summingInt(Order::amount)));
  1. 多级分组(先 status 再 user)
java 复制代码
Map<String, Map<Long, List<Order>>> multi =
    orders.stream().collect(Collectors.groupingBy(Order::status,
        Collectors.groupingBy(Order::userId)));
  1. 分区(partitioningBy) ------ 返回两个桶(true/false)
java 复制代码
Map<Boolean, List<Order>> bigOrSmall = orders.stream()
    .collect(Collectors.partitioningBy(o -> o.amount > 50));

5) groupingByConcurrent(并发分组)

用于并行流时减少锁竞争:

java 复制代码
ConcurrentMap<Long, Integer> concurrentSum = orders.parallelStream()
    .collect(Collectors.groupingByConcurrent(Order::userId, Collectors.summingInt(Order::amount)));

进阶:Collectors 的常用组合模式

TopN(分组后取 top k)

java 复制代码
Map<Long, List<Order>> top2ByUser = orders.stream()
    .collect(Collectors.groupingBy(Order::userId,
        Collectors.collectingAndThen(
            Collectors.toList(),
            list -> list.stream()
                        .sorted(Comparator.comparingInt(Order::amount).reversed())
                        .limit(2)
                        .collect(Collectors.toList())
        )));

toMap 带合并函数(防止 key 冲突)

java 复制代码
Map<Long, Integer> totalByUser = orders.stream()
    .collect(Collectors.toMap(Order::userId, Order::amount, Integer::sum));

原始类型流(IntStream / LongStream / DoubleStream)

避免自动拆装箱,提升性能/内存效率。

java 复制代码
int total = orders.stream().mapToInt(Order::amount).sum();
IntSummaryStatistics st = orders.stream().mapToInt(Order::amount).summaryStatistics();

从数组快速创建:

java 复制代码
IntStream.range(0, 10).forEach(System.out::println);
IntStream.rangeClosed(1, 5).toArray(); // 包含端点

并行流(parallelStream)与性能/线程安全注意事项

并行流能利用多核,但要注意:

  • 要确保操作是无副作用、线程安全的(例如避免对共享集合做非线程安全的 add)。
  • 慎用 forEach 在并行流中对外部 mutable 状态产生副作用
  • 并行不是总是快:小集合、短链式操作、存在顺序依赖(ordered)或排序/limit/skip 等操作,可能导致开销更大。
  • 使用并发收集器 (如 groupingByConcurrent)以提高并行性能。
  • Collectors.toList() 在并行流下不是线程安全的聚合器,会由框架内部合并片段,但外部直接对同一 list 操作会出事

示例并发安全写法:

java 复制代码
Map<Long, Integer> concurrent = orders.parallelStream()
    .collect(Collectors.groupingByConcurrent(Order::userId, Collectors.summingInt(Order::amount)));

常见陷阱与调优建议

  1. 不要在流中做阻塞 IO (如在 map 中调用网络请求),会阻塞线程池。
  2. 注意短路与惰性 :把易短路的操作放在前面(例如高选择性 filter 放在早期,减少后续工作量)。
  3. 避免在并行流中使用非线程安全的集合做副作用收集 。使用 collect 而不是外部 add
  4. 对大量数据做排序/聚合可能耗内存,考虑流式分批或外部排序/外部聚合。
  5. 使用 mapToInt/mapToLong 以避免装箱成本
  6. 使用合适的 CollectorgroupingByConcurrent 对并行流友好,toMap 要处理 key 冲突。
  7. 限制并行度 :可以设置 ForkJoinPool.commonPool() 的并行度或用自定义 ForkJoinPool 执行并行流任务,谨慎使用全局修改。

实战完整示例 ------ 一个中等复杂度的任务

需求:给定订单列表,统计每个 userId 的订单总额、最近订单时间,并取 top3 用户按总额排序。

java 复制代码
import java.time.Instant;
import java.util.*;
import java.util.stream.*;

record Order(long id, long userId, int amount, Instant ts){}

public class StreamExample {
    public static void main(String[] args) {
        List<Order> orders = List.of(
            new Order(1, 100, 50, Instant.parse("2025-01-01T10:00:00Z")),
            new Order(2, 101, 20, Instant.parse("2025-01-02T10:00:00Z")),
            new Order(3, 100, 70, Instant.parse("2025-01-03T10:00:00Z")),
            new Order(4, 102, 30, Instant.parse("2025-01-01T09:00:00Z")),
            new Order(5, 101, 200, Instant.parse("2025-01-04T10:00:00Z"))
        );

        // 分组并计算总额与最近时间
        Map<Long, UserStat> stats = orders.stream()
            .collect(Collectors.groupingBy(Order::userId, Collectors.collectingAndThen(
                Collectors.toList(),
                list -> {
                    int total = list.stream().mapToInt(Order::amount).sum();
                    Instant last = list.stream().map(Order::ts).max(Comparator.naturalOrder()).orElse(Instant.EPOCH);
                    return new UserStat(total, last);
                }
            )));

        // top3 用户
        List<Map.Entry<Long, UserStat>> top3 = stats.entrySet().stream()
            .sorted(Map.Entry.<Long, UserStat>comparingByValue(Comparator.comparingInt(UserStat::total).reversed()))
            .limit(3)
            .collect(Collectors.toList());

        top3.forEach(e -> System.out.println(e.getKey() + " -> " + e.getValue()));
    }

    static class UserStat {
        int total;
        Instant last;
        UserStat(int total, Instant last) { this.total = total; this.last = last; }
        int total() { return total; }
        Instant last() { return last; }
        public String toString() { return "total=" + total + ", last=" + last; }
    }
}

这段代码展示了:分组(groupingBy)→ 聚合(summing / max)→ 排序 → topN 的完整流水线。

总结

  • Stream 提供强大、声明式的数据处理能力,能显著简化集合操作的代码量与可读性。
  • 常用操作:过滤(filter)、映射(map)、归约(reduce)、收集(collect)、分组(groupingBy)等。
  • 并行流是便捷的加速工具,但需要理解线程安全、合并成本与适用场景。
  • 熟练运用 Collectors(复合收集器、分组/并发收集、下游收集器)是写好 Stream 代码的关键。
  • 在生产环境,注意避免在流中做阻塞 IO、不安全的副作用和不恰当的排序/深度聚合导致内存问题。
相关推荐
一只叫煤球的猫1 小时前
从 JDK1.2 到 JDK21:ThreadLocal的进化解决了什么问题
java·后端·面试
天马行空-2 小时前
ES 精准匹配 和 模糊查询的实现方式
java·开发语言
Z***25802 小时前
Java计算机视觉
java·开发语言·计算机视觉
一点事2 小时前
ruoyi:集成mybatisplus实现mybatis增强
java·开发语言·mybatis
e***87702 小时前
Tomcat Request Cookie 丢失问题
java·tomcat·firefox
linksinke2 小时前
Mapstruct引发的 Caused by: java.lang.NumberFormatException: For input string: ““
java·开发语言·exception·mapstruct·numberformat·不能为空
likuolei2 小时前
Eclipse 代码模板
java·ide·eclipse
好好研究2 小时前
SpringMVC框架 - 异常处理
java·开发语言·spring·mvc
只会写代码2 小时前
JDK8 Lambda 加持:打造优雅通用的对象构建器
java