CodeGym /课程 /JAVA 25 SELF /forEach、peek 方法:副作用

forEach、peek 方法:副作用

JAVA 25 SELF
第 30 级, 课程 3
可用

1. forEach 方法:对元素的最终操作

在深入细节之前,让我们再回顾一下什么是流的终端操作和中间操作。

  • 中间操作(例如,filter、map、distinct、peek)— 返回新的流,并且通常在调用终端操作之前不会真正执行。
  • 终端操作(例如,forEach、collect、count、anyMatch)— 触发对流中元素的处理,并返回结果(或者不返回任何结果,如 forEach)。

在执行终端操作后,流被视为关闭,不能再对其应用其他操作。这就像想把已经吃掉的冰淇淋再吃一遍——不可能:流已经“用过了”。

现在来认识两个与副作用相关的重要方法:forEach 和 peek。

forEach 做什么?

forEach 是一种终端操作,它会对流中的每个元素执行给定的动作。通常用于打印输出、写日志、统计等副作用(side effects)。

方法签名:

void forEach(Consumer<? super T> action)

Consumer<T> 是一个函数式接口,接收一个参数且不返回结果(例如,System.out::println)。

示例:输出列表中的所有元素

假设我们有一个用户姓名列表:

List<String> users = List.of("Anna", "Boris", "Alex", "Alina", "Dmitry");

用 Stream API 把所有用户打印到屏幕非常简单:

users.stream().forEach(System.out::println);

输出:

Anna
Boris
Alex
Alina
Dmitry

也可以使用 Lambda 表达式:

users.stream().forEach(name -> System.out.println("用户:" + name));

输出:

用户: Anna
用户: Boris
用户: Alex
用户: Alina
用户: Dmitry

重要:forEach 会结束流

调用 forEach 之后,流就“关闭”了。不能继续链式调用:

users.stream()
     .filter(name -> name.startsWith("A"))
     .forEach(System.out::println)
     .map(String::toUpperCase); // 错误!流已关闭。

在 forEach 之后再调用其他方法会导致编译错误:终端操作返回 void,而不是新的流.

2. peek 方法:只“偷看”,不干预

peek 是中间操作。它允许在处理流程的某个阶段对每个元素执行一个动作,但不会改变元素本身,也不会终止流。

方法签名:

Stream<T> peek(Consumer<? super T> action)
  • peek 返回一个新的流,在该流中会对每个元素执行 action。
  • 通常用于调试、日志记录或监控流的状态。

示例:过滤后进行日志记录

List<String> users = List.of("Anna", "Boris", "Alex", "Alina", "Dmitry");

List<Integer> nameLengths = users.stream()
    .filter(name -> name.startsWith("A"))
    .peek(name -> System.out.println("通过过滤:" + name))
    .map(String::length)
    .collect(Collectors.toList());

控制台输出:

通过过滤:Anna
通过过滤:Alex
通过过滤:Alina

nameLengths 的内容:

[4, 4, 5]

哪些场景适合使用 peek?

  • 用于调试操作链:观察每个阶段发生了什么。
  • 用于收集统计信息(例如计数)。
  • 用于在中间阶段记录日志。

重要:peek 不应用于修改流中的元素。需要转换请使用 map。peek 是“偷看”,不是“干预”。

3. forEach vs peek:有什么区别?

方法 操作类型 使用时机 能否继续链式调用? 更适合用于
forEach
终端操作 在流处理的最后阶段 否 最终动作(打印、日志、写入数据库)
peek
中间操作 在操作链的中间 是 调试、阶段性日志、计数

示例:用法差异

// forEach 示例
users.stream()
    .filter(name -> name.startsWith("A"))
    .map(String::toUpperCase)
    .forEach(System.out::println); // 此处流被终止

// peek 示例
users.stream()
    .filter(name -> name.startsWith("A"))
    .peek(name -> System.out.println("通过过滤:" + name))
    .map(String::toUpperCase)
    .collect(Collectors.toList()); // 可以继续链式调用

务必记住

  • forEach 是不可回头的终点:之后无法再对流做任何事情。
  • peek 如果没有终端操作,不保证会执行。如果只写一串中间操作——什么也不会发生。

4. 一些不明显的点:forEach 并不总是最佳选择!

为什么不应该用 forEach 修改集合?

许多新手会尝试用 forEach 修改集合的元素或集合本身(例如删除元素)。但这是不好的实践:流并不是用来修改原始集合的。

错误用法示例:

List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Alex"));
names.stream().forEach(name -> {
    if (name.startsWith("A")) {
        names.remove(name); // 可能导致 ConcurrentModificationException!
    }
});

结果:运行时错误——在遍历流期间不能修改集合(ConcurrentModificationException)。

那么 forEach 该用来做什么?

  • 用于打印到屏幕(例如打印报表)。
  • 用于写入日志。
  • 用于调用外部服务(例如发送 e-mail)。
  • 用于收集统计信息(例如递增计数器)。

5. 再谈 peek:仅用于调试!

很多人会忍不住用 peek 来修改元素,比如给用户年龄加 1:

users.stream()
    .peek(user -> user.setAge(user.getAge() + 1)) // 不好!
    .collect(Collectors.toList());

为什么不推荐?

  • 这破坏了声明式与 Stream API 的纯净性。
  • 这样的代码难以维护与测试。
  • 在中间操作中引入副作用可能导致隐蔽的 bug。

更好的做法是使用 map 来进行数据转换:

List<User> olderUsers = users.stream()
    .map(user -> new User(user.getName(), user.getAge() + 1))
    .collect(Collectors.toList());

示意:forEach 与 peek 的差异

users.stream()
    .filter(...)         // 中间操作
    .peek(...)           // 中间操作,“偷看”
    .map(...)            // 中间操作
    .forEach(...)        // 终端操作,“执行动作”

说明:
— 在 forEach 之前的一切都可以组合、重排、增删。
— 执行 forEach 之后,流就关闭了。

6. 使用 forEach 和 peek 的常见错误

错误 1:使用 peek 修改数据。 peek 仅用于观察,而非修改流中的元素。需要转换请使用 map。

错误 2:期望 peek 一定会执行。 peek 只有在其后有终端操作(collect、forEach、count 等)时才会执行。没有终端操作,什么也不会发生。

错误 3:在 forEach 之后继续调用流操作。 forEach 是终端操作,之后不能再调用其他流方法。

错误 4:在 forEach 中修改集合。 在通过 forEach 遍历时修改原始集合(删除或添加元素)会直接导致 ConcurrentModificationException。

错误 5:用 forEach 代替 collect 来收集结果。 如果你要将元素收集到新集合,请使用 collect(Collectors.toList()),不要用 forEach 手工添加。这会破坏声明式风格,并可能在多线程场景下引发错误。

1
任务
JAVA 25 SELF, 第 30 级, 课程 3
已锁定
叫学生上台 👩‍🏫
叫学生上台 👩‍🏫
1
任务
JAVA 25 SELF, 第 30 级, 课程 3
已锁定
智能家居中的数据流调试 🏠
智能家居中的数据流调试 🏠
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION