JDK JDK 学习手册8 → 27 · JDK 专用学习站
QUICKREF · COLLECTIONS · STREAM

Stream 流水线

JDK 8 · 速查主题 02 / 06 · 预计 35 分钟
核心35 分钟JEP 107
一句话Stream 把集合处理拆成「来源 → 中间操作 → 终止操作」:中间操作只描述、不执行,终止操作才拉动数据。惰性、短路和并行都建立在这个模型上。

学完你要能自己回答

  1. 中间操作和终止操作如何分工?为什么没有终止操作就什么都不发生?
  2. 惰性求值和短路分别依赖什么?sorted 为什么可能破坏短路?
  3. parallel stream 在什么条件下才值得用?它有哪些共享状态风险?

三段式:来源、中间操作、终止操作01

Stream 不是数据结构,不存储元素;它是一条流水线。stream() 得到来源,filter/map 等中间操作返回新的 Stream,collect/forEach/count 等终止操作触发执行。

执行是按元素垂直流动:每个元素依次经过所有阶段,而不是先把整个集合 filter 完再 map。这让 findFirst 这类短路操作可以在拿到结果后立即停止。

List<String> names = Arrays.asList("Ada", "Alan", "Grace", "Barbara", "Alan");

List<String> result = names.stream()
        .filter(n -> n.length() > 3)
        .map(String::toUpperCase)
        .distinct()
        .sorted()
        .collect(Collectors.toList());
System.out.println(result);

Map<Integer, List<String>> byLength = names.stream()
        .distinct()
        .collect(Collectors.groupingBy(String::length, TreeMap::new, Collectors.toList()));
System.out.println(byLength);

String joined = names.stream().collect(Collectors.joining("/", "[", "]"));
System.out.println(joined);

int total = names.stream().mapToInt(String::length).sum();
System.out.println(total);
[ALAN, BARBARA, GRACE]
{3=[Ada], 4=[Alan], 5=[Grace], 7=[Barbara]}
[Ada/Alan/Grace/Barbara/Alan]
23
没有终止操作,中间操作不会执行;Stream 只能消费一次,重复调用终止操作会抛 IllegalStateException: stream has already been operated upon or closed。

操作地图:无状态、有状态与短路02

类别代表方法说明
无状态中间filter / map / flatMap / peek逐元素处理,可并行
有状态中间distinct / sorted / limit / skip需要缓冲或全局信息
短路中间limit提前结束上游
终止forEach / collect / reduce / count触发执行
短路终止findFirst / findAny / anyMatch / allMatch / noneMatch拿到结果即停止

sorted 和 distinct 是有状态操作:它们必须先看到足够多的元素才能产生第一个结果。limit 虽然是短路,但如果上游是 sorted,仍然要先排完序。

String s = Stream.iterate(1, n -> n * 2)
        .limit(5)
        .map(String::valueOf)
        .collect(Collectors.joining(","));
System.out.println(s);
1,2,4,8,16

JDK 8 的 Stream.iterate 只有两参数版本,必须配合 limit 截断;三参数 iterate(seed, hasNext, next) 是 JDK 9 才加入的。Stream.generate 同样需要限制。

Collectors:把流收成结果03

Collectors 是终止操作里最常用的一组策略:把元素累积到集合、字符串、Map,或做分组与聚合。它可以组合:groupingBy 的下游还可以再接 counting、mapping、summingInt。

收集器结果注意
toList / toSetList / SetJDK 8 返回可变集合;Set 不保证顺序
joiningString可指定分隔符、前缀、后缀
groupingByMap<K,List<T>>key 不能为 null;可加下游收集器
partitioningByMap<Boolean,List<T>>只分两组
toMapMap<K,V>重复 key 抛异常;value 不能为 null
counting / summingInt / averagingIntLong / int / double常配合 groupingBy 使用
List<String> names = Arrays.asList("Ada", "Alan", "Grace", "Barbara", "Alan");

Map<Integer, List<String>> byLength = names.stream()
        .distinct()
        .collect(Collectors.groupingBy(String::length, TreeMap::new, Collectors.toList()));
System.out.println(byLength);
{3=[Ada], 4=[Alan], 5=[Grace], 7=[Barbara]}

toMap 的重复 key 默认抛 IllegalStateException,可以传 merge 函数决定保留哪一个;toMap 的 value 为 null 时会抛 NPE,因为内部用了 Map.merge。需要 null 值时改用 groupingBy 或自定义收集器。

JDK 8 的 Collectors.toList() 返回 ArrayList,是可变的;不要把它当作不可变集合直接暴露给调用方。不可变收集器(如 toUnmodifiableList)是 JDK 10 才有的。

基本类型流:避免装箱04

IntStream、LongStream、DoubleStream 用基本类型处理元素,避免 Stream<Integer> 的装箱开销。mapToInt、boxed、summaryStatistics 是常用的桥接方法。

IntSummaryStatistics stats = IntStream.of(3, 1, 4, 1, 5).summaryStatistics();
System.out.println(stats.getCount() + "/" + stats.getSum() + "/"
        + stats.getMax() + "/" + stats.getAverage());
5/14/5/2.8
  • range 是左闭右开,rangeClosed 是左闭右闭;差一错误在两者之间很常见。
  • Stream<Integer> 做数值计算时,每个元素都可能装箱;热点路径优先用基本类型流。
  • boxed() 会重新装箱,只在需要对象流时使用。
  • summaryStatistics 一次遍历同时得到 count/sum/min/max/average,比多次流式统计更高效。
DoubleStream.sum() 使用浮点累加,结果可能受顺序影响;对精度敏感时用 Kahan 或 BigDecimal,不要假设并行与顺序结果逐位相同。

并行流:边界比写法重要05

并行流把 Spliterator 切成多段,交给 ForkJoinPool.commonPool() 执行,最后合并结果。它适合大数据量、CPU 密集、无状态且满足结合律的操作。

long sum = IntStream.rangeClosed(1, 100)
        .parallel()
        .filter(i -> i % 2 == 0)
        .sum();
System.out.println("parallelSum=" + sum);
parallelSum=2550
  • 数据量小、单核、I/O 密集时,并行只会增加切分与合并成本。
  • forEach 不保证顺序;需要顺序用 forEachOrdered,但它会限制并行收益。
  • 不要在并行流里修改共享的 ArrayList 或普通计数器;用 collect、reduce 或线程安全结构。
  • commonPool 默认并行度是 CPU 核数减一;阻塞任务会拖慢同一 JVM 里的其他并行流。
  • findFirst 在并行流上要保留遇到顺序,通常比 findAny 慢;不需要顺序时用 findAny。
并行流不是「加个 parallel 就变快」。先用顺序流确认瓶颈在 CPU 计算,再测量并行收益;否则只是把问题从业务代码转移到线程池。

版本边界BOUNDARY

这张表比 API 本身更值得记。写代码时先确认你的运行时版本。

特性 / API状态说明
Stream API8 正式JEP 107;集合默认方法 stream()/parallelStream() 同时加入。
`Stream.iterate` 三参数9 起JDK 8 只有 iterate(seed, UnaryOperator)。
`takeWhile` / `dropWhile`9 起JDK 8 用 filter + limit 模拟。
`Stream.toList`16 起JDK 8 用 collect(Collectors.toList())。
`Collectors.toUnmodifiableList`10 起JDK 8 只有可变的 toList/toSet。
`Optional.isEmpty`11 起与 Stream 配合时常用 !isPresent() 代替。
`ForkJoinPool.commonPool()`8 起并行流的默认执行器;并行度默认 CPU-1。

费曼自检SELF CHECK

先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。

01. 为什么说 Stream 是惰性的?什么时候真正执行?
中间操作只构建流水线,不读取元素;只有终止操作开始拉取数据时,元素才逐个经过各阶段。没有终止操作,流水线不会执行。
02. filter 和 sorted 在并行流里的代价有什么不同?
filter 是无状态操作,可以逐元素并行处理;sorted 是有状态操作,必须收集全部元素后统一排序,无法真正流式处理,遇到无限流还可能永远不结束。
03. parallel stream 适合什么任务?至少说出三个不适合的场景。
适合数据量大、CPU 密集、无状态、满足结合律的操作。不适合小数据量、I/O 密集、共享可变状态、强顺序依赖,以及会阻塞 commonPool 的任务。
04. orElse 和 Stream 的 findFirst 有什么关系?
findFirst 返回 Optional,常用 orElse/orElseGet 提供默认值。注意 orElse 的参数会立即求值,默认值昂贵时应使用 orElseGet。
学习进度 记录保存在本机浏览器 · 不上传

JDK 学习手册 · JDK / Java 专用学习站 · 纯静态 · 双击 index.html 即可使用

本站专注 JDK 版本演进与 Java 教学,非 Oracle / OpenJDK 官方站点。版本数据来自 OpenJDK 官方 Features 表;课程示例已在对应本机 JDK 实测。构建日期 2026-11-12。