是时候对 Java Stream API 祛魅了

8 阅读5分钟

作为 java8 引入的核心特性之一,Stream API 确实极大简化了集合操作的编码体验,但它并非“银弹”:在某些场景下是利器,在另一些场景反而成为负担。本篇文章就从优势、误区、局限和正确使用四个维度展开论述。

优势

  • 声明式编程

关注做什么,而不是怎么做

// for 循环实现
List<String> productNames = new ArrayList<>();
for (Product product : products) {
  String name = product.getName();  // 相当于第一个 map
  String upperCaseName = name.toUpperCase();  // 相当于第二个 map
  productNames.add(upperCaseName);  // 相当于 collect
}


// Stream 方式实现
List<String> productNames = products.stream()
                              .map(Product::getName)
                              .map(String::toUpperCase)
                              .collect(Collectors.toList());

此种方式更接近自然语言,减少了代码,提升了可读性。

  • 函数式编程范式

Stream API 内置了大量的高阶函数,如filter,map,reduce,flatmap等,支持lambda表达式和方法引用,天然适配函数式编程思想,通过函数组合的方式提升了代码抽象能力。

// 多步骤数据丰富管道

List<EnrichedData> enrichedData = rawDataList.stream()
    .map(this::validateAndClean)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .peek(data -> log.debug("Processing: {}", data.getId()))
    .parallel()  // 并行处理
    .map(data -> CompletableFuture.supplyAsync(() -> 
        enrichWithExternalData(data, externalService)
    ))
    .map(CompletableFuture::join)
    .map(this::applyBusinessRules)
    .flatMap(data -> Stream.of(
        data,
        createDerivedData(data)
    ))
    .sorted(Comparator
        .comparing(EnrichedData::getPriority, Comparator.reverseOrder())
        .thenComparing(EnrichedData::getTimestamp)
    )
    .distinct()
    .collect(Collectors.collectingAndThen(
        Collectors.toList(),
        list -> {
            // 后处理
            Map<String, List<EnrichedData>> grouped = list.stream()
                .collect(Collectors.groupingBy(EnrichedData::getGroupKey))
                
            return grouped.values().stream()
                .flatMap(group -> group.stream()
                    .limit(maxPerGroup))
                .collect(Collectors.toList());
        }
    ));
  • 并行处理透明化

通过高阶函数 parallelStream() 或 stream().parallel() 将串行流转为并行流,轻松利用多核显著提升性能。使用并行流时需注意线程安全与数据规模阈值。但这依然比 for 实现并行计算使用简单且不易出错。

  // 并行计算复杂指标
  ProductStats stats = products.parallelStream()
            .collect(Collectors.teeing(
                Collectors.summarizingDouble(Product::getPrice),
                Collectors.summarizingInt(Product::getQuantity),
                (priceStats, quantityStats) -> new ProductStats(
                    priceStats.getAverage(),
                    priceStats.getMax(),
                    quantityStats.getSum(),
                    products.size()
                )
            ));

误区

  • 一定比传统循环更高效

  • Stream的操作性能取决于具体的操作和数据规模。

  • 有些中间操作无法利用容器的具体特性。

  • 并行流并非万能加速器,在数据量较小的情况下,线程调度的开销可能超过并行计算的收益。

  • 代码一定简洁

Stream 的简洁性依赖于操作的线程流程,一旦遇上分支逻辑,嵌套操作或异常处理时,代码可能变得晦涩难懂

 // 感受一下如下代码
 Optional.ofNullable(inputs)
            .orElse(Collections.emptyList())
            .parallelStream()
            .filter(Objects::nonNull)
            .map(input -> {
                try {
                    return Optional.ofNullable((Map<String, Object>)input.get("data"))
                        .map(data -> ((List<Map<String, Object>>)data.get("items")).stream()
                            .flatMap(item -> {
                                Object val = item.get("value");
                                return val instanceof List ? 
                                    ((List<?>)val).stream()
                                        .filter(v -> v != null)
                                        .map(v -> Map.<String, Object>of(
                                            "id", item.get("id"),
                                            "processedValue", 
                                            Optional.ofNullable(v)
                                                .map(Object::toString)
                                                .map(s -> {
                                                    try { 
                                                        return Double.parseDouble(s) *  Optional.ofNullable(item.get("factor"))
                                                                .map(f -> Double.parseDouble(f.toString()))
                                                                .orElse(1.0);
                                                    } catch (Exception e) { 
                                                        return 0.0; 
                                                    }
                                                })
                                                .orElse(0.0),
                                            "timestamp", 
                                            Optional.ofNullable(item.get("time"))
                                                .map(t -> Long.parseLong(t.toString()))
                                                .orElse(System.currentTimeMillis())
                                        )) :
                                    Stream.empty();
                            })
                            .collect(Collectors.groupingBy(
                                m -> (String)m.get("id"),
                                Collectors.collectingAndThen(
                                    Collectors.toList(),
                                    lst -> lst.stream()
                                        .collect(Collectors.teeing(
                                            Collectors.summarizingDouble(m -> (Double)m.get("processedValue")),
                                            Collectors.mapping(m -> (Long)m.get("timestamp"), Collectors.toList())
                                            (stats, timestamps) -> Map.<String, Object>of(
                                                "count", stats.getCount(),
                                                "sum", stats.getSum(),
                                                "avg", stats.getAverage(),
                                                "latest", timestamps.stream()
                                                    .mapToLong(Long::longValue)
                                                    .max()
                                                    .orElse(0L),
                                                "histogram", lst.stream()
                                                    .collect(Collectors.groupingBy(
                                                        m -> (int)((Double)m.get("processedValue") / 10.0) * 10,
                                                        TreeMap::new,
                                                        Collectors.counting()
                                                    ))
                                            )
                                        ))
                                )
                            )))
                        .orElse(Collections.emptyMap());
                } catch (Exception e) {
                    return Collections.<String, Map<String, Object>>emptyMap();
                }
            })
            .flatMap(map -> map.entrySet().stream())
            .collect(Collectors.groupingBy(
                Map.Entry::getKey,
                Collectors.mapping(
                    Map.Entry::getValue,
                    Collectors.collectingAndThen(
                        Collectors.toList(),
                        lst -> lst.stream()
                            .reduce((m1, m2) -> m1.entrySet().stream()
                                .collect(Collectors.toMap(
                                    Map.Entry::getKey,
                                    e -> {
                                        Object v1 = e.getValue();
                                        Object v2 = m2.get(e.getKey());
                                        if (v1 instanceof Double && v2 instanceof Double) {
                                            return (Double)v1 + (Double)v2;
                                        } else if (v1 instanceof Long && v2 instanceof Long) {
                                            return (Long)v1 + (Long)v2;
                                        } else if (v1 instanceof Map && v2 instanceof Map) {
                                            return ((Map<?, ?>)v1).entrySet().stream()
                                                .collect(Collectors.toMap(
                                                    Map.Entry::getKey,
                                                    e2 -> {
                                                        Object subV1 = e2.getValue();
                                                        Object subV2 = ((Map<?, ?>)v2).get(e2.getKey());
                                                        return subV1 instanceof Long && subV2 instanceof Long ?
                                                            (Long)subV1 + (Long)subV2 : subV1;
                                                    }
                                                ));
                                        }
                                        return v1;
                                    }
                                )
                            )
                            .orElse(Collections.emptyMap())
                    )
                )
            ))
            .entrySet()
            .stream()
            .collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> (Map<String, Object>)e.getValue()
            ));
  • 必须用stream才能显得“高级”
List<Person> persons = new ArrayList<>();

// 不推荐:Stream 不适合(需要新创建列表,并且有副作用,不符合函数式理念)
persons = persons.stream()
    .map(p -> {
        p.setAge(p.getAge() + 1);  // 副作用
        return p;
    })
    .collect(Collectors.toList());  // 创建了新列表


// 推荐: for 循环更适合
for (Person p : persons) {
    p.setAge(p.getAge() + 1);  // 直接修改原列表
}

局限

  • 不适合精细控制的迭代逻辑
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

int target = 5;
boolean found = false;
int result = -1;

// 推荐:for 循环可以提前退出
for (int n : numbers) {
    if (n > target) {
        result = n;
        found = true;
        break;  // 找到就立即退出
    }
}


// 不推荐:Stream 必须处理所有元素
Optional<Integer> streamResult = numbers.stream()
    .filter(n -> n > target)
    .findFirst();  // 虽然用findFirst,但filter会检查所有元素
  • 不适合高频调用的性能敏感场景
// 处理大文件

// 推荐:for 循环:流式处理,内存友好
try (BufferedReader br = new BufferedReader(new FileReader("huge_file.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        processLine(line);  // 一次处理一行
    }
}

// 不推荐:Stream:可能加载全部到内存
List<String> allLines = Files.readAllLines(Paths.get("huge_file.txt"));
allLines.stream()  // 已经加载了全部内容
    .forEach(this::processLine);

正确使用

避免盲目崇拜,根据具体场景选择合适的方式。

优先使用 stream 的场景

  • 简单的数据转换
// 推荐:Stream:声明式,更接近自然语言
List<String> result = orders.stream()
    .filter(order -> order.getStatus() == OrderStatus.COMPLETED)
    .filter(order -> order.getAmount() > 1000)
    .map(Order::getCustomer)
    .filter(customer -> customer.getAge() >= 18)
    .map(Customer::getEmail)
    .distinct()
    .sorted()
    .collect(Collectors.toList());


// 不推荐: for 循环:命令式,逻辑分散
List<String> result = new ArrayList<>();
Set<String> emailSet = new HashSet<>();
for (Order order : orders) {
    if (order.getStatus() == OrderStatus.COMPLETED) {
        if (order.getAmount() > 1000) {
            Customer customer = order.getCustomer();
            if (customer.getAge() >= 18) {
                String email = customer.getEmail();
                if (emailSet.add(email)) {  // 实现 distinct
                    result.add(email);
                }
            }
        }
    }
}

Collections.sort(result);  // 额外排序
  • 并行计算
// 推荐 Stream:一行代码开启并行
double avgSalary = employees.parallelStream()
    .filter(e -> e.getDepartment().equals("Engineering"))
    .mapToDouble(Employee::getSalary)
    .average()
    .orElse(0.0);


// 不推荐:手动并行计算
int threads = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(threads);
List<Future<Double>> futures = new ArrayList<>();
intchunkSize = engineers.size() / threads;

for (int i = 0; i < threads; i++) {
    int start = i * chunkSize;
    int end = (i == threads - 1) ? engineers.size() : (i + 1) * chunkSize;
    List<Employee> chunk = engineers.subList(start, end);
  

    futures.add(executor.submit(() -> {
        double sum = 0;
        for (Employee e : chunk) {
            sum += e.getSalary();
        }
        return sum;
    }));



double total = 0;
for (Future<Double> future : futures) {
    total += future.get();
}
double avgSalary = total / engineers.size();
executor.shutdown();

慎用 stream 的场景

  • 复杂分支逻辑
// 推荐 for 循环:清晰的条件控制
for (int i = 0; i < array.length; i++) {
    if (i > 0 && array[i] > array[i-1]) {
        // 处理递增序列
        if (array[i] > 100) {
            break;
        }
    } else if (i > 0) {
        // 处理递减序列
        continue;
    }
    // 其他处理...
}



// 不推荐 Stream:难以表达复杂控制流
Arrays.stream(array)
    .???  // 很难表达上述复杂逻辑
  • 需要精细控制的迭代
int[] array = {1, 2, 3, 4, 5};


// 推荐 for 循环:天然支持索引
for (int i = 0; i < array.length; i++) {
    if (i > 0) {
        System.out.println("当前: " + array[i] + ", 前一个: " + array[i-1]);
    }
}


// 不推荐 Stream:需要额外处理
IntStream.range(0, array.length)
    .mapToObj(i -> i > 0 ? 
        "当前: " + array[i] + ", 前一个: " + array[i-1] : 
        "第一个: " + array[i])
    .forEach(System.out::println);

总结

无论使用 Stream 还是传统循环,代码的可读性和可维护性都是第一优先级的。技术是工具,不是信仰。好的代码从来不是“用了多少新特性”,而是能否高效解决问题。