掌握Java Streams的高级技巧

Java Streams 在 Java 8 之后成为了处理集合数据的强大工具。本文将从中级到高级层面,介绍几种常用但不常被提及的技巧,帮助你更高效、更优雅地使用 Streams。

1. 自定义终端操作:多级聚合

在日常编码中,我们经常使用 Collectors.summingIntCollectors.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. 通过 limitskip 实现分页

在处理大数据时,分页是一大挑战。Stream 可以通过 skiplimit 实现:

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,同时提升了代码可读性。掌握上述高级技巧后,你可以在项目中写出更简洁、更高效、更易维护的代码。祝编码愉快!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注