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

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() + 副作用收集,能避免副作用就避免副作用!

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容