299. Java Stream API - 逐个消费流元素:使用 forEach() 方法

13 阅读2分钟

299. Java Stream API - 逐个消费流元素:使用 forEach() 方法


🎯 forEach() 是什么?

forEach() 是一个终止操作,适用于 将流中的每个元素传递给一个 Consumer(消费者)来进行处理。

👀 它通常用于对每个元素执行副作用操作,比如打印、写日志、发送网络请求等。


✅ 示例:打印所有符合条件的元素

import java.util.stream.Stream;

public class ForEachExample {
    public static void main(String[] args) {
        Stream<String> strings = Stream.of("one", "two", "three", "four");

        strings.filter(s -> s.length() == 3)
               .map(String::toUpperCase)
               .forEach(System.out::println);
    }
}
输出结果:
ONE
TWO

⚠️ 注意!forEach() 用得爽,可能也藏坑!

虽然 forEach() 看起来很简单,但它背后隐藏的问题是 “副作用(side-effect)”

什么是副作用?

当 Lambda 表达式改变了它之外的状态,比如:

  • 修改外部变量;
  • 向集合中添加元素;
  • 改变字段、写文件、打印控制台等等。

⚠️ 副作用例子(容易出问题的写法)

import java.util.*;
import java.util.stream.Stream;

public class SideEffectExample {
    public static void main(String[] args) {
        Stream<String> strings = Stream.of("one", "two", "three", "four");

        List<String> result = new ArrayList<>();

        strings.filter(s -> s.length() == 3)
               .map(String::toUpperCase)
               .forEach(result::add); // ❌ 有副作用!

        System.out.println("result = " + result);
    }
}
输出:
result = [ONE, TWO]

❗ 为什么这种写法有风险?

  1. 副作用 = 捕获外部变量 = 捕获式 Lambda → 性能下降,JVM 生成额外对象。
  2. 并发不安全 → 如果使用 parallelStream() 并发处理,多个线程同时写入 ArrayList 会出错或数据错乱。
  3. 副作用让代码难以测试、难以维护 → 你很难保证副作用带来的状态是否正确,尤其在并发环境中。

✅ 更推荐的做法:无副作用地收集元素

✅ 使用 toList() 方法(Java 16+)

import java.util.List;
import java.util.stream.Stream;

public class SafeCollectExample {
    public static void main(String[] args) {
        Stream<String> strings = Stream.of("one", "two", "three", "four");

        List<String> result = strings
                .filter(s -> s.length() == 3)
                .map(String::toUpperCase)
                .toList(); // ✅ 无副作用

        System.out.println("result = " + result);
    }
}
输出:
result = [ONE, TWO]

✅ 使用 Collectors.toList()(适用于 Java 8+)

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class CollectorsExample {
    public static void main(String[] args) {
        Stream<String> strings = Stream.of("one", "two", "three", "four");

        List<String> result = strings
                .filter(s -> s.length() == 3)
                .map(String::toUpperCase)
                .collect(Collectors.toList()); // ✅ 无副作用

        System.out.println("result = " + result);
    }
}

🔍 对比:三种方式对比总结

方法是否线程安全是否有副作用可读性备注
forEach(result::add)❌ 否✅ 有✅ 简单不推荐;并发风险+性能损耗
.collect(Collectors.toList())✅ 是❌ 无✅ 清晰适用于 Java 8+,推荐使用
.toList()✅ 是❌ 无✅ 简洁Java 16+,结果为不可变列表

🚀 小结与最佳实践建议

forEach() 最适合:

  • 调试(打印、日志)
  • 执行单个操作(写数据库、发请求)
  • 你清楚知道副作用不会引发问题的情况

❌ 不推荐使用 forEach() 来:

  • 将结果“收集”到集合中(请使用 .collect().toList()
  • 执行可能会在并发环境下出错的操作

🎁 结语一句话:

能用 collect() 就别用 forEach() + 副作用收集,能避免副作用就避免副作用!