
1. 项目概述从“会用”到“精通”的蜕变如果你已经能用filter、map、collect这些基础操作处理集合恭喜你你已经踏入了 Java Stream 的大门。但你是否遇到过这样的场景面对一个复杂的集合转换需求写出的 Stream 代码冗长且难以理解性能也不尽如人意或者在处理海量数据时程序莫名抛出OutOfMemoryError让你束手无策。这些痛点恰恰是区分“普通使用者”和“高级玩家”的关键。“深入 Java Stream高级流操作和技巧”这个主题瞄准的就是那些已经不满足于基础用法的开发者。它要解决的是如何让 Stream 代码更优雅、更高效、更健壮。这不仅仅是多学几个 API 的问题而是一种思维模式的升级——从“命令式”的步步为营转向“声明式”的精准表达并深刻理解其背后的运行机制。无论是为了优化现有代码性能应对高并发场景下的资源挑战还是在面试中从容应对那些刁钻的“八股文”深入掌握 Stream 的高级特性都至关重要。本文将带你超越forEach和collect(Collectors.toList())深入剖析并行流的陷阱与调优、自定义收集器的强大威力、无限流的巧妙应用以及如何规避常见的性能坑。我们会结合大量实际案例让你不仅知道“怎么用”更明白“为什么这么用”以及“什么时候不该用”。无论你是正在夯实基础的进阶者还是寻求性能突破的资深工程师这里都有你需要的“干货”。2. 核心设计思路声明式编程与惰性求值要玩转高级 Stream 操作必须从它的两大核心设计思想说起声明式编程和惰性求值。这是理解所有高级技巧的基石。2.1 声明式 vs 命令式思维转换命令式编程关注“如何做”How你需要详细指导计算机每一步操作遍历索引、判断条件、修改集合。而声明式编程关注“做什么”What你只需描述最终想要的结果。举个例子我们要从一个员工列表中找出所有薪资超过 10000 的资深工程师的名字并按薪资降序排列。命令式写法传统 for 循环ListString result new ArrayList(); for (Employee emp : employees) { if (emp.getSalary() 10000 “Engineer”.equals(emp.getRole())) { result.add(emp.getName()); } } result.sort((a, b) - { // 需要重新根据名字找员工对象比较薪资或者维护一个映射非常繁琐 // 通常这里代码会变得复杂 }); // 真正的按薪资排序在命令式下很别扭往往需要额外数据结构声明式写法StreamListString result employees.stream() .filter(emp - emp.getSalary() 10000) .filter(emp - “Engineer”.equals(emp.getRole())) .sorted(Comparator.comparing(Employee::getSalary).reversed()) .map(Employee::getName) .collect(Collectors.toList());Stream 的声明式写法像一串流畅的管道每个操作filter,sorted,map都是一个高阶函数它描述了一个转换意图而非具体步骤。这种写法的优势在于可读性极高代码几乎就是需求描述的直译。易于维护业务逻辑变更时通常只需增删或修改一个“操作节点”。易于并行化由于操作是无状态的理想情况下转换为并行流非常自然。注意声明式并非银弹。在极简单的遍历或需要复杂流程控制如带break、continue的循环时传统的 for 循环可能更直接。高级技巧就在于如何识别并充分利用声明式的优势场景。2.2 惰性求值与短路操作性能优化的关键惰性求值是 Stream 实现高效的核心机制。中间操作如filter,map,sorted是“懒惰”的它们只是被添加到执行计划中并不会立即触发任何计算。只有终端操作如collect,forEach,count被调用时整个流水线才会被激活并且数据会尽可能以“一个接一个”的方式流经整个管道而不是为每个操作创建完整的中间集合。结合短路操作能极大提升性能。例如findFirst()、anyMatch()、limit()都是短路操作。案例在大集合中查找第一个满足条件的元素OptionalEmployee firstRichEngineer employees.stream() .filter(emp - { System.out.println(“Filtering: ” emp.getName()); // 用于观察执行过程 return emp.getSalary() 10000; }) .filter(emp - { System.out.println(“Checking role: ” emp.getName()); return “Engineer”.equals(emp.getRole()); }) .findFirst();假设第一个员工就满足条件那么filter和后续操作都只会执行一次。findFirst()作为短路终端操作一旦找到结果就立即终止整个流的处理。这在处理无限流或超大集合时至关重要。一个常见的误区是误用sorted。sorted是一个有状态的中间操作它通常需要处理整个流的数据后才能进行排序这破坏了流的“逐元素”流动特性可能成为性能瓶颈。在findFirst前使用sorted会导致必须对所有元素排序后才能取第一个完全丧失了短路优化的可能。// 错误示例低效 employees.stream() .sorted(Comparator.comparing(Employee::getSalary).reversed()) // 必须先全排序 .filter(emp - “Engineer”.equals(emp.getRole())) .findFirst(); // 正确示例高效 employees.stream() .filter(emp - “Engineer”.equals(emp.getRole())) .max(Comparator.comparing(Employee::getSalary)); // 使用 max 避免全局排序理解惰性求值和短路是编写高效 Stream 代码的第一步。它要求我们在组合操作时时刻考虑操作的顺序和特性将能过滤掉大量数据的操作如filter前置将有状态且耗时的操作如sorted、distinct后置或考虑替代方案。3. 并行流深度解析威力与陷阱并存parallelStream()一念天堂一念地狱。它利用 Fork/Join 框架试图将工作负载自动拆分到多个 CPU 核心上执行这对于计算密集型任务很有吸引力。但并行不是免费的午餐用不好反而会导致性能下降、结果错误甚至引发OutOfMemoryError。3.1 何时该用并行流并行流生效的理想条件数据量足够大通常数据量在万级以上才能开始显现并行优势。对于几百条数据线程调度和合并结果的开销可能远超计算本身。任务计算密集每个元素的处理比较耗时如复杂的数学计算、模拟、IO等待这样并行带来的收益才能掩盖线程管理的开销。数据源易于拆分ArrayList、数组这类支持随机访问、时间复杂度为 O(1) 的数据源拆分效率极高。而LinkedList、Stream.iterate这类数据源拆分本身可能成为瓶颈。操作本身是并行友好的中间操作如filter、map通常是并行友好的无状态。而sorted、distinct、limit这类有状态操作在并行时需要进行昂贵的协调与合并可能抵消并行收益。一个简单的性能测试对比ListInteger numbers IntStream.rangeClosed(1, 10_000_000) .boxed() .collect(Collectors.toList()); // 顺序流 long start System.currentTimeMillis(); long sumSeq numbers.stream().reduce(0, Integer::sum); long seqTime System.currentTimeMillis() - start; // 并行流 start System.currentTimeMillis(); long sumPar numbers.parallelStream().reduce(0, Integer::sum); long parTime System.currentTimeMillis() - start; System.out.println(“顺序和: ” sumSeq “, 耗时: ” seqTime “ms”); System.out.println(“并行和: ” sumPar “, 耗时: ” parTime “ms”);在多核机器上对于简单的求和操作并行流可能有数倍的提升。但对于limit(5).parallel()这样的操作并行几乎毫无益处。3.2 并行流的致命陷阱与规避陷阱一共享状态与线程安全这是并行流最经典的错误。流操作中的 lambda 表达式必须是无状态的且不干扰外部变量。// 错误示例线程不安全的累加 ListInteger unsafeList new ArrayList(); IntStream.range(0, 10000).parallel().forEach(unsafeList::add); // 运行多次结果列表大小很可能小于10000且可能抛出 ArrayIndexOutOfBoundsException // 正确做法使用线程安全的收集器 ListInteger safeList IntStream.range(0, 10000) .parallel() .boxed() .collect(Collectors.toList()); // toList() 是线程安全的forEach中的操作应避免修改外部共享状态。归约操作reduce的累加器和组合器必须满足结合律且初始值identity对于组合器必须是“恒等值”。陷阱二forEach的顺序性forEach在并行流中不保证顺序。如果需要按顺序处理应使用forEachOrdered但这会牺牲部分并行性能。// 并行流输出顺序随机 IntStream.range(0, 10).parallel().forEach(System.out::print); // 可能输出6 2 1 8 7 4 0 9 3 5 // 保证顺序但可能退化为类似顺序执行 IntStream.range(0, 10).parallel().forEachOrdered(System.out::print); // 保证输出0 1 2 3 4 5 6 7 8 9陷阱三性能不升反降与资源耗尽不当使用并行流可能导致线程竞争如果任务很简单如i线程竞争锁的开销可能主导执行时间。拆分流成本过高数据源拆分困难如Stream.iterate。内存压力并行处理会同时创建多个中间结果集合如果每个元素都很大或操作本身生成大量数据可能迅速耗尽内存触发OutOfMemoryError: Java heap space。这在处理大型对象或进行字符串拼接时尤其需要注意。实战建议先测量后优化使用 JMH 等微基准测试工具在真实数据规模和硬件环境下测试并行流的性能。谨慎对待有状态操作尽量避免在并行流中使用sorted、distinct考虑能否在数据源阶段预处理。关注收集器使用Collectors.toConcurrentMap、Collectors.groupingByConcurrent等并发收集器来提升并行收集阶段的效率。设置自定义 ForkJoinPool默认情况下并行流使用公共的 ForkJoinPool。如果有一个耗时很长的并行流任务它可能会阻塞池中所有其他任务。对于此类任务可以考虑在自定义的 ForkJoinPool 中提交。ForkJoinPool customPool new ForkJoinPool(4); // 自定义线程数 try { customPool.submit(() - hugeList.parallelStream() .filter(...) .collect(Collectors.toList()) ).get(); } finally { customPool.shutdown(); }4. 自定义收集器释放 Stream 的终极力量Collectors工具类提供了丰富的内置收集器但当你需要将流元素规约到一个自定义的复杂结果如构建一个特定的 JSON 结构、计算多个统计指标、增量更新一个数据库时自定义收集器就是你的终极武器。它实现了CollectorT, A, R接口其中T是流元素类型A是中间累加容器类型R是最终结果类型。4.1 手把手实现一个自定义收集器假设我们需要从一个Transaction交易流中收集以下信息总交易金额最大单笔交易金额按货币分类的交易金额总和交易ID列表内置收集器很难一步到位完成这个复合收集。我们来定义一个TransactionSummary类作为结果并实现收集器。第一步定义结果容器和最终结果类// 最终结果类 Data // 使用 Lombok 简化代码 public class TransactionSummary { private double totalAmount; private double maxAmount; private MapString, Double amountByCurrency; private ListString transactionIds; } // 中间累加容器可变容器用于高效累加 public class TransactionAccumulator { private double total 0.0; private double max Double.MIN_VALUE; private MapString, Double currencyMap new HashMap(); private ListString ids new ArrayList(); // 方法将单个交易累加到容器中 public void accumulate(Transaction t) { double amount t.getAmount(); total amount; if (amount max) { max amount; } currencyMap.merge(t.getCurrency(), amount, Double::sum); ids.add(t.getId()); } // 方法合并两个累加器并行流时使用 public void combine(TransactionAccumulator other) { total other.total; if (other.max max) { max other.max; } other.currencyMap.forEach((k, v) - currencyMap.merge(k, v, Double::sum)); ids.addAll(other.ids); } // 方法将累加器转换为最终结果 public TransactionSummary toSummary() { TransactionSummary summary new TransactionSummary(); summary.setTotalAmount(total); summary.setMaxAmount(max Double.MIN_VALUE ? 0.0 : max); // 处理空流 summary.setAmountByCurrency(new HashMap(currencyMap)); // 防御性拷贝 summary.setTransactionIds(new ArrayList(ids)); return summary; } }第二步实现 Collector 接口我们可以使用Collector.of()这个工厂方法来简化实现。public class TransactionCollectors { public static CollectorTransaction, TransactionAccumulator, TransactionSummary toSummary() { return Collector.of( TransactionAccumulator::new, // Supplier: 创建新的累加器 TransactionAccumulator::accumulate, // Accumulator: 累加单个元素 TransactionAccumulator::combine, // Combiner: 合并两个累加器用于并行 TransactionAccumulator::toSummary, // Finisher: 转换为最终结果 Collector.Characteristics.CONCURRENT, // 特性累加器支持并发如果currencyMap是ConcurrentHashMap Collector.Characteristics.UNORDERED // 特性流元素顺序不影响结果 ); } }第三步使用自定义收集器ListTransaction transactions ... // 获取交易列表 TransactionSummary summary transactions.stream() .collect(TransactionCollectors.toSummary()); System.out.println(“总金额: ” summary.getTotalAmount()); System.out.println(“最大交易: ” summary.getMaxAmount()); summary.getAmountByCurrency().forEach((k, v) - System.out.println(k “: ” v));4.2 特性Characteristics详解与性能考量Characteristics是一个枚举集合用于向 Stream 框架提示收集器的属性以进行优化CONCURRENT表示累加容器A是线程安全的多个线程可以同时调用accumulator函数操作同一个容器。这可以显著提升并行流的性能因为它避免了为每个线程创建子容器再合并的开销。但前提是容器本身线程安全如ConcurrentHashMap。如果设置了CONCURRENT但容器不安全会导致数据竞争。UNORDERED表示规约结果不受流中元素相遇顺序的影响。对于toSet()或groupingBy这类操作设置此标志可以让框架在并行执行时更自由地优化。IDENTITY_FINISH表示finisher函数是恒等函数即累加容器A本身就是最终结果R。这允许框架省略调用finisher的步骤进行一些优化。在我们的例子中如果我们将TransactionAccumulator中的Map和List都换成并发版本如ConcurrentHashMap和CopyOnWriteArrayList并确信业务逻辑允许无序就可以声明CONCURRENT和UNORDERED特性来提升并行性能。实操心得实现自定义收集器时先从顺序流场景开始确保accumulator和combiner逻辑正确。然后务必用并行流进行测试验证combiner的正确性和线程安全性。combiner必须满足结合律并且与accumulator兼容。一个简单的测试方法是stream.collect(collector)的结果应该与stream.parallel().collect(collector)的结果完全一致对于确定性操作。5. 无限流与生成器应对流式数据源StreamAPI 不仅用于处理已有的有限集合还能优雅地处理无限或未知长度的序列。这是通过Stream.generate()和Stream.iterate()方法实现的。5.1 使用generate与iterate创建流Stream.generate(Supplier)接受一个Supplier无参返回值的函数用于无限地生成流中的下一个元素。它生成的流是无序的。// 生成随机数流 StreamDouble infiniteRandoms Stream.generate(Math::random); // 生成常量流 StreamString constantStream Stream.generate(() - “echo”); // 配合 limit 截取有限个 ListDouble tenRandoms Stream.generate(Math::random) .limit(10) .collect(Collectors.toList());generate常用于模拟数据源、生成测试数据或连接到一个不断产生数据的生产者如消息队列的消费者回调。但要注意Supplier应该是无状态的否则在并行流中会产生不可预知的行为。Stream.iterate(seed, UnaryOperator)接受一个初始种子seed和一个一元操作符UnaryOperatorT - T通过反复对前一个元素应用该操作符来生成无限序列。它生成的流是有序的。// 生成从1开始的奇数序列1, 3, 5, 7... StreamInteger oddNumbers Stream.iterate(1, n - n 2); // 生成斐波那契数列 StreamLong fibonacci Stream.iterate(new long[]{0L, 1L}, t - new long[]{t[1], t[0] t[1]}) .map(t - t[0]); // 取第一个数从 Java 9 开始iterate有了一个重载版本iterate(seed, Predicate, UnaryOperator)可以生成一个有限流当谓词条件不满足时停止。// 生成小于1000的2的幂次方序列 StreamInteger powersOfTwo Stream.iterate(1, n - n 1000, n - n * 2);5.2 连接外部数据源与资源管理无限流的强大之处在于可以连接真实的外部流式数据源例如读取一个大文件、监听网络套接字、或消费 Kafka 主题。关键在于如何将这种“拉取”或“事件驱动”的模型适配到 Stream 的“拉取”模型。案例使用 Stream 懒读取超大文件直接Files.lines会将所有行读入内存对于超大文件不合适。我们可以利用Stream.generate和BufferedReader实现懒加载。public StreamString lazyFileLines(Path filePath) throws IOException { BufferedReader reader Files.newBufferedReader(filePath); // 生成器每次调用 readLine结束时返回 null return Stream.generate(() - { try { return reader.readLine(); } catch (IOException e) { throw new UncheckedIOException(e); } }) .takeWhile(line - line ! null); // Java 9遇到 null 停止流 // 注意需要关闭 reader可以在 onClose 钩子中处理 // .onClose(() - { try { reader.close(); } catch (IOException e) { ... } }); }这里有一个至关重要的陷阱资源泄漏。Stream 本身不会自动管理像BufferedReader这样的资源。你必须确保流被正确关闭通常通过try-with-resources语句来包装 Stream。try (StreamString lines lazyFileLines(Paths.get(“huge.log”))) { ListString errorLines lines.filter(l - l.contains(“ERROR”)) .limit(100) .collect(Collectors.toList()); } // 流关闭时会触发 onClose 钩子从而关闭 BufferedReader如果使用Files.lines它返回的流已经封装了关闭逻辑推荐使用它。另一个高级技巧使用Spliterator自定义源对于更复杂的数据源如数据库游标、分页 API可以实现Spliterator接口。Spliterator可拆分迭代器是 Stream 的底层抽象它定义了如何遍历和拆分元素序列。public class PagingApiSpliteratorT implements SpliteratorT { private final PageFetcherT pageFetcher; private ListT currentPage; private int currentIndex; Override public boolean tryAdvance(Consumer? super T action) { if (currentPage null || currentIndex currentPage.size()) { currentPage pageFetcher.fetchNextPage(); // 获取下一页 currentIndex 0; if (currentPage.isEmpty()) { return false; // 没有更多数据 } } action.accept(currentPage.get(currentIndex)); return true; } Override public SpliteratorT trySplit() { return null; // 不支持并行拆分返回 null } // 省略其他方法estimateSize, characteristics } // 使用 SpliteratorData spliterator new PagingApiSpliterator(fetcher); StreamData dataStream StreamSupport.stream(spliterator, false); // false 表示顺序流通过自定义Spliterator你可以将任何按需获取数据的能力接入到 Stream API 中享受声明式操作的所有便利。6. 性能调优与常见陷阱排查即使理解了原理在实际编码中仍会踩坑。下面是一些高频的实战问题与调优技巧。6.1 警惕装箱/拆箱与原始类型流Stream 操作对象流StreamT。对于int、long、double等原始类型频繁的自动装箱int - Integer和拆箱会带来不小的性能开销并产生大量短期对象增加 GC 压力。错误示例ListInteger list ...; int sum list.stream() .map(i - i * 2) // Integer - Integer 中间有装箱 .reduce(0, Integer::sum); // 拆箱求和优化方案使用原始类型特化流IntStream、LongStream、DoubleStream。IntStream intStream list.stream().mapToInt(Integer::intValue); // 转为 IntStream int sum intStream.map(i - i * 2) // 在原始类型上操作无装箱 .sum(); // 专用终端操作效率高如果源头就是数组或IntStream.range直接使用原始类型流。mapToInt、mapToLong、mapToDouble是常用的转换方法。6.2 规约操作reduce的细节reduce是一个非常重要的终端操作用于将流元素组合成一个单一结果。它有三个重载形式OptionalT reduce(BinaryOperatorT accumulator)T reduce(T identity, BinaryOperatorT accumulator)U U reduce(U identity, BiFunctionU,? super T,U accumulator, BinaryOperatorU combiner)关键点恒等值identity对于形式2和3identity必须是累加器函数的“恒等元”。即对于所有taccumulator.apply(identity, t)必须等于t。例如求和的恒等值是0乘法的恒等值是1。如果identity不满足这个条件在并行规约时会导致错误结果。结合律累加器函数必须满足结合律(a op b) op c a op (b op c)。这是并行规约能够正确工作的数学基础。减法就不满足结合律。组合器combiner在形式3中combiner用于合并并行计算产生的部分结果。在顺序流中它不会被调用。combiner必须与accumulator兼容即combiner.apply(u, accumulator.apply(identity, t))应等于accumulator.apply(u, t)。通常combiner和accumulator是同一个函数。6.3 调试与日志记录Stream 的流水线操作使得调试变得困难因为你不能像在循环里那样轻松地设置断点查看中间状态。一个实用的技巧是使用peek操作。peek(Consumer)是一个中间操作它接收一个元素执行一些操作如打印日志然后原样将元素传递下去。它主要用于调试不应在生产代码中用于修改状态。ListString result transactions.stream() .peek(t - System.out.println(“原始: ” t)) // 调试点1 .filter(t - t.getAmount() 1000) .peek(t - System.out.println(“大于1000: ” t)) // 调试点2 .map(Transaction::getCurrency) .distinct() .collect(Collectors.toList());由于惰性求值peek中的输出会与流的实际执行顺序一致帮助你理解数据是如何流过每个阶段的。但要注意在并行流中peek的执行线程是不确定的。6.4 常见异常与问题排查表问题现象可能原因解决方案java.lang.OutOfMemoryError: Java heap space1. 流处理的数据集本身巨大。2. 中间操作产生了巨大的中间集合如sorted全排序。3. 在并行流中多个线程同时创建大型中间结果。1. 使用limit尽早限制数据量。2. 避免在流中间使用sorted、distinct考虑在数据源处预处理。3. 考虑使用原始类型流减少内存占用。4. 调整 JVM 堆大小-Xmx作为临时措施但需优化代码。并行流结果不正确或非确定1. 在forEach、peek或accumulator中修改了共享的可变状态。2. 使用了非线程安全的收集器或容器。3.reduce的identity不是恒等元或操作不满足结合律。1. 确保 lambda 是无状态且不干涉的。2. 使用线程安全的容器或并发收集器如toConcurrentMap。3. 检查reduce操作的数学属性。流操作抛出NullPointerException1. 流元素本身为null。2. 在map、flatMap等操作中函数返回了null。1. 使用filter(Objects::nonNull)过滤掉空元素。2. 在映射函数中做好空值检查或使用Optional进行包装。IllegalStateException: stream has already been operated upon or closed流是“一次性”的终端操作只能调用一次。尝试重复使用已消费的流。每次需要新的流水线时都从数据源如集合重新创建流。性能未达预期甚至比循环慢1. 数据量太小流框架开销占比高。2. 操作过于简单如i。3. 使用了不合适的并行流。4. 频繁的装箱/拆箱。1. 对小数据集10000优先使用循环。2. 对简单操作循环可能更优。3. 对并行流进行性能剖析。4. 使用原始类型流。7. 高级模式与组合技巧掌握了基础组件后我们可以将它们组合起来解决更复杂的实际问题。7.1 使用flatMap进行多维操作flatMap可能是最强大也最容易用错的中间操作。它接收一个返回Stream的函数然后将所有生成的子流“扁平化”连接成一个流。经典场景一展开嵌套集合ListListString nestedList Arrays.asList( Arrays.asList(“a”, “b”, “c”), Arrays.asList(“d”, “e”), Arrays.asList(“f”, “g”, “h”) ); ListString flatList nestedList.stream() .flatMap(Collection::stream) // 将每个 ListString 转为 StreamString .collect(Collectors.toList()); // 结果: [a, b, c, d, e, f, g, h]经典场景二一对多映射并过滤假设有订单列表每个订单有多个订单项。我们要找出所有订单中价格超过100的订单项。ListOrderItem expensiveItems orders.stream() .flatMap(order - order.getItems().stream()) // 将订单流展开为订单项流 .filter(item - item.getPrice() 100) .collect(Collectors.toList());高级技巧用flatMap实现过滤和映射的融合有时我们需要先映射但映射结果可能为空Optional.empty()我们希望自动过滤掉这些空值。// 传统方式先 map再 filter ListString names users.stream() .map(User::getNickname) // 可能返回 null .filter(Objects::nonNull) .collect(Collectors.toList()); // 使用 flatMap 与 Optional 的流 ListString names users.stream() .map(User::getNickname) .flatMap(Optional::stream) // Java 9将 OptionalString 转为 StreamString空则转为空流 .collect(Collectors.toList());如果getNickname返回OptionalString这种方法非常优雅。对于返回null的情况可以.map(u - Optional.ofNullable(u.getNickname())).flatMap(Optional::stream)。7.2 分区与分组的高级用法Collectors.partitioningBy和Collectors.groupingBy非常强大可以进行多级分组和下游收集。两级分组按部门分组再按薪资级别分组。MapString, MapSalaryLevel, ListEmployee employeesByDeptAndLevel employees.stream().collect( Collectors.groupingBy(Employee::getDepartment, Collectors.groupingBy(emp - { if (emp.getSalary() 5000) return SalaryLevel.JUNIOR; else if (emp.getSalary() 15000) return SalaryLevel.MID; else return SalaryLevel.SENIOR; }) ) );分组后进行计算计算每个部门的平均薪资。MapString, Double avgSalaryByDept employees.stream().collect( Collectors.groupingBy(Employee::getDepartment, Collectors.averagingDouble(Employee::getSalary) ) );Collectors还提供了summingInt、counting、maxBy、minBy、summarizingDouble获取统计摘要等强大的下游收集器。将分组结果转换为其他类型获取每个部门薪资最高的员工。MapString, OptionalEmployee topEarnerByDept employees.stream().collect( Collectors.groupingBy(Employee::getDepartment, Collectors.maxBy(Comparator.comparing(Employee::getSalary)) ) ); // 注意值是 Optional因为部门可能为空 // 如果想直接获取员工假设部门非空可以使用 collectingAndThen MapString, Employee topEarnerByDept2 employees.stream().collect( Collectors.groupingBy(Employee::getDepartment, Collectors.collectingAndThen( Collectors.maxBy(Comparator.comparing(Employee::getSalary)), Optional::get // 如果确定部门非空可以这样用。否则有风险。 ) ) );collectingAndThen是一个适配器在主要收集完成后再施加一个 finishing 转换函数。7.3 短路操作与无限流的交互limit和takeWhileJava 9是处理无限流的关键。它们允许你在满足条件时提前终止流的处理这是惰性求值和短路操作的完美结合。生成斐波那契数列直到值超过1000Stream.iterate(new long[]{0L, 1L}, t - new long[]{t[1], t[0] t[1]}) .map(t - t[0]) .takeWhile(n - n 1000) // Java 9当条件为 false 时停止 .forEach(System.out::println);在 Java 8 中可以用limit配合一个很大的数来模拟或者自己实现一个Spliterator。查找第一个满足复杂条件的元素我们想找到第一个其平方大于1000的奇数。OptionalInteger first Stream.iterate(1, n - n 2) // 奇数流 .filter(n - n * n 1000) .findFirst();由于findFirst是短路操作流不会无限计算下去一旦找到符合条件的元素这里是33因为33*331089计算就会停止。将这些模式组合起来你可以用非常简洁的代码表达复杂的逻辑。核心在于始终从“我想得到什么”的声明式角度思考然后选择合适的操作符去组合。当代码变得复杂时适时地将其拆分成多个流步骤或者考虑自定义收集器或Spliterator来保持清晰性。Stream 不是要完全取代循环而是在适合的场景下提供一种更富表达力、更易于并行化、也更易于维护的抽象。