Java Streams 在 Java 8 之后成为了处理集合数据的强大工具。本文将从中级到高级层面,介绍几种常用但不常被提及的技巧,帮助你更高效、更优雅地使用 Streams。
1. 自定义终端操作:多级聚合
在日常编码中,我们经常使用 Collectors.summingInt、Collectors.groupingBy 等标准终端操作。但有时需要一次性完成多重聚合:例如,先按某个字段分组,再按另一个字段聚合。可以通过 Collectors.collectingAndThen 或自定义收集器实现。
Map<String, Map<String, Long>> result =
employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getRole,
Collectors.counting()
)
));
2. 处理异常的 Stream
Stream API 设计之初并未考虑 checked exception,导致在 lambda 中抛异常非常繁琐。可以使用以下两种模式:
- 包装为 RuntimeException
private static <T> Function<T, T> rethrow(Function<T, T> fn) { return t -> { try { return fn.apply(t); } catch (Exception e) { throw new RuntimeException(e); } }; } - 自定义
ThrowingFunction接口@FunctionalInterface interface ThrowingFunction<T, R> { R apply(T t) throws Exception; }
3. 使用 flatMap 生成 Cartesian Product
有时需要生成两个集合的笛卡尔积(Cartesian Product)。传统做法是两层循环,Stream 也能优雅完成:
List <String> a = List.of("X", "Y");
List <Integer> b = List.of(1, 2, 3);
List <String> product = a.stream()
.flatMap(x -> b.stream().map(y -> x + "-" + y))
.collect(Collectors.toList());
4. 通过 limit 及 skip 实现分页
在处理大数据时,分页是一大挑战。Stream 可以通过 skip 与 limit 实现:
List <Emp> page = employees.stream()
.sorted(Comparator.comparing(Emp::getHireDate))
.skip((pageNo - 1) * pageSize)
.limit(pageSize)
.collect(Collectors.toList());
5. 记忆化(Memoization)在 Stream 处理中的应用
某些计算开销较大且结果可重复利用,例如对字符串做复杂正则匹配。可以借助 ConcurrentHashMap 或 Guava 的 Cache 进行记忆化:
private final Map<String, Boolean> regexCache = new ConcurrentHashMap<>();
private boolean isMatch(String s, Pattern pattern) {
return regexCache.computeIfAbsent(s, key -> pattern.matcher(key).matches());
}
在 Stream 中使用:
employees.stream()
.filter(e -> isMatch(e.getName(), namePattern))
.collect(Collectors.toList());
6. peek 的双重用途
peek 常被误解为“debug工具”,但它也可用于中间状态变更。例如,在处理链表结构时,可以利用 peek 给元素注入临时状态:
List <Node> nodes = ...
nodes.stream()
.peek(node -> node.setVisited(true))
.filter(Node::isVisited)
.collect(Collectors.toList());
7. 结合 Optional 与 Streams 的链式操作
Optional 与 Streams 的结合可以让代码更安全、更直观:
Optional <Employee> result = employees.stream()
.filter(e -> e.getAge() > 30)
.max(Comparator.comparingInt(Employee::getSalary));
result.ifPresentOrElse(
e -> System.out.println("高薪员工: " + e.getName()),
() -> System.out.println("无符合条件员工")
);
8. 用 reduce 实现自定义聚合
reduce 可以做任何聚合。举例:求字符串数组中最长字符串长度:
int maxLen = strings.stream()
.reduce(0, (max, s) -> Math.max(max, s.length()), Math::max);
9. 并行 Streams 与自定义 ForkJoinPool
默认并行 Stream 使用 ForkJoinPool.commonPool()。若需要自定义线程数,可以创建自己的 ForkJoinPool:
ForkJoinPool pool = new ForkJoinPool(4);
List <Integer> result = pool.submit(() ->
numbers.parallelStream()
.map(n -> n * n)
.collect(Collectors.toList())
).join();
10. 结合 JDK 17 Pattern Matching 提升可读性
在 JDK 17 之后,instanceof 可直接绑定变量,进一步简化 lambda 表达式:
records.stream()
.filter(r -> r instanceof Employee e && e.getSalary() > 10000)
.map(r -> ((Employee) r).getName())
.forEach(System.out::println);
结语
Java Streams 的力量在于它提供了对集合操作的声明式语法,减少了 boilerplate,同时提升了代码可读性。掌握上述高级技巧后,你可以在项目中写出更简洁、更高效、更易维护的代码。祝编码愉快!

发表回复