根据您的问题,您似乎认为.forEach 比for(:) '更好',现在正在尝试证明这一点。
你无法证明它,因为它是假的。 .forEach 通常是相当更糟。不是更短。代码质量方面,它通常会导致代码不太优雅(“优雅”定义为:更容易理解和遵循,在面对变更请求时更容易修改,更容易测试。未定义为“看起来更漂亮” -没有关于味道的争论!)
有理由使用.forEach,但这些通常是异国情调的。
为什么会更糟?
因为 lambda 在检查异常、控制流和可变局部变量方面不透明。这些都是for(:) 没有遭受的重大缺点。例如,缺少可变局部变量透明性是您无法编写此代码的原因。
如果您不确定这 3 个概念的含义以及 foreach 如何做得好、lambda 做得不好,以及在什么情况下 lambda 真正发挥作用以及这种缺乏透明度的地方变成了好处,请参阅下面的完整论文。
如何让代码示例与 lambda 一起使用?
通常你需要采用函数式思维。这里的问题是“当地人的透明度”问题。您不希望本地人存在,它们本质上意味着并行性不在桌面上(并行性很少相关,但 lambda 和流的架构从根本上设计为它们应该继续有效运行并且没有竞争条件,即使它是相关的),并且一旦您编写了在并行场景中失败的代码,它往往会在尝试将其采用 lambda/stream 样式时产生问题。
没关系,我只想写我的代码!
唯一真正的选择是使用AtomicX 来携带信息:
AtomicInteger totalPrice = new AtomicInteger();
AtomicInteger totalItems = new AtomicInteger();
basketItems.entrySet().forEach(b -> {
totalPrice.add(b.getKey().getPrice() * b.getValue());
totalItems.add(b.getValue());
});
这是丑陋的,低效的,大量的代码,而且不是特别可读。因此,为什么你不应该这样做。这不是对for(:) 的任何改进。
告诉我更多关于“在 lambdas 中思考”的事情。
您希望每个“循环”独立,不与其他循环交互,然后使用并行方法来组合结果。这称为 map/reduce,您应该在网上搜索有关此想法的完整教程。 lambdas/streams 也支持这个想法。但是,因为您要收集两件事,所以要复杂得多,您需要一个对象来表示所有相关信息:对总价格的贡献以及对项目数量的贡献。
假设您只想计算项目,仅此而已。然后不要这样做:
AtomicInteger count_ = new AtomicInteger();
basketItems.values().forEach(x -> count_.add(x));
int count = count_.get();
但是做:
int count = basketItems.values().mapToInt(Integer::intValue).sum();
在您的情况下,您正在做两件事,因此代码变得更加复杂且难以阅读:
int[] totals = basketItems.entrySet()
.map(e -> new int[] {e.getKey().getPrice() * e.getValue(), e.getValue()})
.collect(Collectors.reducing((a, b) -> new int[] {a[0] + b[0], a[1] + b[1]}))
.orElse(new int[2]);
int totalPrice = totals[0];
int totalItems = totals[1];
这将首先将您的商品/金额对映射到一个 int 数组,其中第一个元素包含总价格,第二个包含商品数量。
然后它通过将数组合并到一个数组中来收集您的 2 大小 int 数组流。
然后它会返回这个,使用[0, 0] 作为替身,以防你有一个空篮子。
这就是“在流中思考”。
是不是更短了。
哎呀,不!
- 如果你强迫自己写出中间类型(在这种情况下,
Map.Entry<Item, Integer>),这是不公平的比较,而在 lambda 变体中你没有。使用var。
- 如果您发现在风格上可以接受将整个流操作堆成一条巨大的、无大括号的行,同时还采用一种在其他地方严格执行大括号的样式指南,那么这不是一个公平的比较。然后你只是注意到你自己不一致的风格指南很奇怪,而不是 lambdas 从根本上说更短。
考虑到这一点,下面是 for(:) 形式的代码:
double totalPrice = 0;
int items = 0;
for (var e : basketItems.entrySet()) {
totalPrice += entry.getKey().getPrice() * entry.getValue();
items += entry.getValue();
}
简单。易于阅读。相当少hacky(没有int数组只是为了携带信息)。可能性能要高一个数量级。各方面都好多了。
深入:Lambda 和透明胶片
此答案中得出的结论与互联网上各个角落发现的粗心建议背道而驰。 (即:不要使用 lambdas 或函数式方法,除非替代方案明显更糟;如有疑问,请不要使用 lambdas)。
因此,也许,有人可能会认为证明的责任在于论点的这一方(不确定这是否合乎逻辑,但我想以防万一)。
因此,深入分析 lambdas 所没有的 3 种透明度及其优缺点:
- 它们没有被检查异常透明:如果你写:
try {
Runnable r = () -> { throw new IOException(); }
r.run();
} catch (IOException e) {}
它不会编译,即使你的眼球和大脑正确地告诉你它应该编译 - IOException 被抛出,并被捕获,保证。它不会编译,因为您的 lambda 主体需要“适应”Runnable 接口中的单个抽象方法,该接口未声明允许您抛出 IOException,因此,您不能。
可以通过在未检查的异常中积极地重新包装已检查的异常来解决它;这破坏了检查异常的意义,并且需要添加大量样板代码,使 lambda 变得冗长且笨拙。
基本的 for 循环/不要使用 lambda,根本不会受此影响:
try {
throw new IOException();
} catch (IOException e) {}
编译得很好。
- 它们不是可变的局部变量透明的。
int x = 0;
List.of("a", "b").forEach(elem -> x++);
这不会编译:任何非最终局部变量都不能在所述局部变量范围内的 lambdas 中访问(编译器知道您在谈论哪个变量,您只是无法读取或写入它)。编译器会帮您一个忙,并将任何在其整个范围内从未更改过的非最终局部变量视为“有效最终”,并让您从中读取。但是,从定义上来说,从 lambda 中写入它是不可能的(因为这将使它不是有效的最终结果)。这很烦人。它可以与AtomicInteger / AtomicDouble / AtomicReference 一起工作(这比使用new int[1] 作为车辆更好)。
for 循环不受此影响。这编译得很好:
int x;
for (var elem : List.of("a", b")) x++;
- 它们不是控制流透明的。
outer:
while (true) {
List.of("a", "b").forEach(x -> {
// what goes here?
}
System.out.println("Hello");
}
在“这里发生了什么”部分,假设您不仅要中止这个特定循环,还要中止 整个 forEach 运行。例如,您希望在点击"a" 时停止处理,甚至从不循环"b"。 这是不可能的。想象一下,你想做一些更激烈的事情,打破围绕它的 while 循环。 这是不可能的。
使用基本的 for 循环,两者都非常可能:
outer:
while (true) {
for (var e : List.of("a", "b")) {
if (e.equals("a")) break; // break the for
if (e.equals("b")) break outer; // break the while
}
}
当 lambda “旅行”时,这 3 个不利因素实际上会转变为有利因素。也就是说,当包含 lambda 代码的方法早已完成,或者如果 lambda 在完全不同的线程中执行时,lambda 会被存储并在稍后的某个时间运行:我上面列出的所有 3 个缺点都变得奇怪和令人困惑在这种情况下:
-
那个 catch 块?整个方法已经停止执行,所有状态都消失了,代码不能跳转到那里,即使在词法上看起来应该如此,所以不利因素变成了有利因素。
-
如果在另一个线程中运行的 lambda 中可以看到非最终局部变量并发生变异,则不能再在堆栈上声明局部变量,它需要静默移动到堆中。我们现在应该开始担心将我们的本地人标记为volatile 吗?所有选项,但现在在 java 中,本地定义仅限于您的线程,并且在您的范围结束时不再存在。这使得推理代码变得更容易。这些先决条件将不得不消失。这很讨厌。不利因素变成了有利因素。
-
在这种情况下,没有 while 循环可以继续或中断。代码将毫无意义。不利因素变成了有利因素。
由此得出以下结论:
Lambdas 是fantastic,没有任何警告,当它们用于编写超出您编写范围的代码时,或者是因为它存储在一个字段中并在很久以后执行,或者因为它在另一个线程中运行。
Lambdas 被讨厌的警告拖累,因此,如果它们不旅行,就会出现糟糕的风格和代码味道。即使 lambda 没有“旅行”,基于 lambda 的解决方案仍然很容易是正确的,但它们不仅仅是“默认情况下更好”。默认情况下,它们实际上更糟糕。
由于以上两条规则,我们得到第三条规则:
直接在列表上或在没有中间步骤的流上调用 .forEach(elem -> {...}) 总是很糟糕并且代码有异味!
换句话说,这肯定是愚蠢的:
list.forEach(x -> doStuff);
set.stream().forEach(x -> doStuff);
只需使用基本的 for 循环即可。
因此应该很少使用.forEach 终端。它仅在 3 个场景中是非愚蠢的代码:
-
底层数据结构本质上是并行的,您需要执行的处理也同样是并行的,但您无需微调用于运行它的池。这是很少见的(例如,通常如果并行性与此相关,则需要更多控制,而 fork/join 是答案,或者并行性无关紧要。很少有你在中间),但如果你这样做,那么这会有所帮助,因为它们可以并行运行,而 foreach 则不能。这种情况很少相关。
-
您已经有一个消费者,例如传入:
public void peekEvents(Consumer<Event> consumer) {
eventQueue.forEach(consumer);
}
- 你有中间体:
eventQueue.stream().filter(Event::isPublic)
.map(Event::getWidget)
.forEach(widget -> System.out.println("Widget with queued up events: " + widget);
在这里您可以过滤和映射 - 然后使用 forEach 开始变得更有意义。