用流替换通常的 for 循环

Replace the usual for loop with a stream

我有简单的 class CountBarrier 方法 count() 我想替换类似的东西:

for (int i = 0; i < 5; i++) {
    countBarrier.count()
}

与:

 IntStream.range(0, 5).forEach(countBarrier::count);

但为了正常工作,我只能这样写:

IntStream.range(0, 5).forEach((i) -> countBarrier.count());

如何处理这个问题(不想看到 var i)?

没有更好的选择。您可以编写自己的方法将 countBarrier::count 转换为 Consumer<Integer>,但通常没有一种方法可以比现有方法做得更好。

您传递给 forEach() 的方法引用被分类为 reference to an instance method of a particular object

containingObject::instanceMethodName    

默认情况下,编译器会假定需要将流的整数元素用作方法参数。

因为您的方法 count() 没有参数,所以您遇到编译错误。如果你有一个像 count(int) 这样的重载版本,那么 forEach(countBarrier::count) 将编译。

需要注意的重要一点是,您在这里通过更改流外部对象的状态来滥用 Stream API。

方法forEach()只能通过副作用来操作。根据 documentation,它必须仅在没有其他实现方式的情况下使用。

例如,当您使用 return 流 的方法拨号时,您需要以某种方式处理其元素,然后需要调用方法每个流元素。在这种情况下,使用 forEach() 是合理的,因为别无选择。循环不是一个选项,如果你创建一个中间集合,它会增加内存消耗并可能损害性能。

但对于您的简单示例,forEahc() 并非必不可少。此操作不需要流。

“医生,按这里很痛!” “好吧,那就别按了。”

听起来你希望尽可能简洁地写出'call this method 5 times'的概念。

Java 可以做到这一点 - 并且已经有能力做到这一点 30 年了:

for (int i = 0; i < 5; i++) countBarrier.count();

正是这样。没有牙套。没有换行符。

想象一下您使用 foreach 的尝试确实有效(它没有,也不能成功,但让我们前往 hypothetical-land 并找到位于独角兽和彩虹之间的这段代码):

IntStream.range(0, 5).forEach(countBarrier::count);

更长。如果我们按照高尔夫标准,显然第一个更好。

但是,最后一个片段(已经很差)甚至不起作用 - 正如您发现的那样。问题是,forEach 的目的是为流中的每个事物执行一次给定的消费者(这是一个接受 1 个参数且 returns 什么都没有的代码块),并且 流元素被传递给每个调用。这里有一个整数流(从 0 到 4,包括在内),所以 .forEach 的目的是 运行 一些代码用于 0、1、2、3,最后是 4 , 并且索引 被传递给代码 。因此,您的 count 方法必须将 int 作为参数。它没有,因此 java 拒绝编译它。没有语言功能可以告诉 java:是的,无论如何,我不关心 i 只是 运行 它 - 因为 java 是那样的固执者。没有(简单的)修复方法。

这让我们回到:

for (int i = 0; i < 5; i++) countBarrier.count();

是完成这项工作的最佳方式。所以,写那个。

但是大括号是必须的!

不,他们不是。风格指南中要求您使用它们的情况有些常见,但是有一个非常非常简单的解决方案可以解决该问题:不要使用这种不必要的限制性风格指南,并且总是问 为什么 。风格指南不是来自山区;他们传达的规则有一个潜在的意图。 'braces for ALL THE THINGS' 风格指南规则的目的主要是您可以轻松添加额外的语句(除了调用 count(),每个循环之外,您可能还想发出一条日志消息),部分以避免排长队。

链接 lambdas/stream API 如果你只是把它全部堆起来,就会有完全相同的问题。给定你喜欢的 (non-working) 语法,如果你想除了调用计数之外还发出一条日志消息,你需要打开这个:

IntStream.range(0, 5).forEach(countBarrier::count);

进入这个:

IntStream.range(0, 5).forEach(() -> {
  log.info("Counting!");
  countBarrier.count();
});

与转动此相比, 重写更多:

for (int i = 0; i < 5; i++) countBarrier.count();

进入这个:

for (int i = 0; i < 5; i++) {
  log.info("Counting!");
  countBarrier.count();
}

只有三个选项:

  • 你是风格指南的无可救药的奴隶。真奇怪。
  • 您认为在 single-statement 循环体上扩展所需的一些轻微的额外重写工作是可以接受的(在这种情况下 one-linering 传统的 for 循环很好)。
  • 您认为一些轻微的额外重写会导致错误或太费力,因此应该始终编写循环,以便能够仅通过在中间注入它们来接收额外的语句。在这种情况下,您的意图 new-style .forEach 是一种风格违规。

排长队?好吧,很明显,流 API 的堆积会导致令人难以置信的长行,所以这显然是 non-starter:你不能使用 'but lambdas let me put stuff on one line!' 同时也支持所有传统 for 循环 必须使用大括号,否则行会太长。

Lambda 更好..只是..因为!

他们绝对不是。 java 中的 Lambda 不知道它们是 运行 'in context' 还是 'out of context'。例如,.forEach() 运行s 'in context':调用 foo.forEach(x -> codeGoesHere) 将在整个语句、lambda 和所有语句之前完成 所有循环, 'resolves' 并完成。但是,对于 lambda 表达式则不必如此。例如,这个:

new TreeSet<>(Comparator.comparing(Student::getName))

从不调用任何学生对象的 .getName() 方法。 'code' 而是保存在 TreeSet 中的一个字段中,并且会在您与该树集交互时随时调用。可能是 5 天后,在不同的线程中。

而java不知道。

正因为如此,java 有意 使这些东西 non-transparent 在任何条带的 lambda 中:

  • 您不能从 lambda 中抛出已检查的异常,即使包含的代码捕获了它们。即:
try {
  listOfPaths.forEach(path -> books.append(Files.readString(path)));
} catch (IOException e) { .. }

是一个编译器错误,因为 Files.readString 被声明为 throws IOException - 即使我们捕获了它,javac 也不知道那个 lambda 是否是 运行 'in context' 或否,因此不会编译以上内容。用普通的 jane for 循环替换它,效果很好。

因此,lambda 不能替代 dowhiletry 等循环/控制流语句。它们是不同的工具,擅长不同的事情。使用正确的工具来完成工作。对于这项工作,不是 lambda。

  • Non-final 局部变量

你不能与它们交互,甚至不能从 lambda 中读取它们。毕竟,如果该代码在 运行ning 5 天后在另一个线程中结束,那么,该上下文早已不复存在。是的,我们 知道它会 运行 在上下文中,但 javac 不会,因此不会让你。传统的for循环没有这样的问题。

  • 控制流程

您可以 breakcontinuereturn 从普通 for 循环内部到外部。但是 lambda 不能那样做。同样,同样的原因。如果 lambda 运行 脱离上下文,那将毫无意义。你试图 break/continue/return 的东西已经消失了。

因此,虽然使用 .forEach 样式循环的任何和所有好处都完全基于 'style'(并且美丽在旁观者的眼中。你不能做出有利于发现的理性论据一件比一件更漂亮)——使用传统 for 循环的好处是 objective。他们可以做得更多。

但是forEach允许列表结构控制循环行为

它专门指定按列表顺序依次进行。 运行现在和永远不会有什么区别,因为那将是向后不兼容的。

但是.forEach让我省略了变量类型,所以它更短

不是这样,因为 var 存在。这是合法的 java:

for (var map : listOfMapsOfStringsToLists) { .... }

但是,嘿,如果你真的想要它...

IntStream 是 'abuse' - IntStream 的目的是生成连续的 int 值序列。您对它们不感兴趣(您只是 'abusing' 使某个东西被调用 5 次,您对序列 '0, 1, 2, 3, 4' 没有特别的兴趣 - 您只对任何长度恰好为 5) 的序列,但因为您拥有它们,所以您只能使用 :: 语法来调用使用 int 的方法。你的 count() 方法没有(你不希望它),所以你不应该使用 IntStream。您想要的是:

Loops.forEach(5, countBarrier::count);

很遗憾,java没有这个方法。但是,您当然可以自己编写!很简单:

class Loops {
  public static void forEach(int count, Runnable r) {
    if (i < 0) throw new IllegalArgumentException("i is negative");
    for (int i = 0; i < count; i++) r.run();
  }
}

一个普通的 2 班轮。