Stream 流水线
学完你要能自己回答
- 中间操作和终止操作如何分工?为什么没有终止操作就什么都不发生?
- 惰性求值和短路分别依赖什么?
sorted为什么可能破坏短路? - 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
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 / toSet | List / Set | JDK 8 返回可变集合;Set 不保证顺序 |
joining | String | 可指定分隔符、前缀、后缀 |
groupingBy | Map<K,List<T>> | key 不能为 null;可加下游收集器 |
partitioningBy | Map<Boolean,List<T>> | 只分两组 |
toMap | Map<K,V> | 重复 key 抛异常;value 不能为 null |
counting / summingInt / averagingInt | Long / 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 或自定义收集器。
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。
版本边界BOUNDARY
这张表比 API 本身更值得记。写代码时先确认你的运行时版本。
| 特性 / API | 状态 | 说明 |
|---|---|---|
| Stream API | 8 正式 | 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 在并行流里的代价有什么不同?
03. parallel stream 适合什么任务?至少说出三个不适合的场景。
04. orElse 和 Stream 的 findFirst 有什么关系?
findFirst 返回 Optional,常用 orElse/orElseGet 提供默认值。注意 orElse 的参数会立即求值,默认值昂贵时应使用 orElseGet。