首页 > 解决方案 > Java lambda 用 entrySet 编写 foreach

问题描述

我正在尝试使用 Maps 和 lambdas。首先,我决定编写普通的 foreach 循环,然后将它与 lambda 进行比较,以及它的长度。所以首先我有显示键和值的方法,之后我想得到这些值的总和。键是包含名称、价格等的对象,值是项目的数量。

下面是正常的 foreach 方法。

    double totalPrice = 0;
    int items = 0;
    for (Map.Entry<Item, Integer> entry : basketItems.entrySet()) {
        System.out.println("Key: " + entry.getKey() +
                "Value: " + entry.getValue());

        totalPrice += entry.getKey().getPrice() * entry.getValue();
        items += entry.getValue();
    }

    System.out.println("Wartość zamówienia: " + String.format("%.2f", totalPrice));
    System.out.println("Ilość przedmiotów: " + items);

我可以像这样用 lambda 做同样的事情。

    basketItems.entrySet().forEach(
            b -> System.out.println("Key: " + b.getKey() +
                        " Value: " + b.getValue()));

但是,我怎样才能通过这种方式在 lambda 中获得我的总价格?有可能在一个 lambda 中做到这一点吗?我不知道。

标签: javalambda

解决方案


从您的问题来看,您似乎认为.forEach比“更好” for(:),现在正在尝试证明这一点。

你无法证明它,因为它是假的。.forEach往往会很多。不是更短。代码质量方面,它通常会导致代码不太优雅(“优雅”定义为:更容易理解和遵循,在面对变更请求时更容易修改,更容易测试。未定义为“看起来更漂亮” -没有关于味道的争论!)

有理由使用.forEach,但这些通常是异国情调的。

为什么更糟?

因为 lambda 在检查异常、控制流和可变局部变量方面不透明。这些都是for(:)不会受到影响的重大缺点。例如,缺少可变局部变量透明性是您无法编写此代码的原因。

如果您不确定这 3 个概念的含义以及 foreach 如何做得好、lambda 做得不好,以及在什么情况下 lambda 真正发挥作用以及这种缺乏透明度的地方变成了好处,请参阅下面的完整论文。

无论如何,我如何使代码示例与 lambdas 一起使用?

一般来说,你需要采用函数式思维方式。这里的问题是“当地人的透明度”问题。您不希望本地人存在,它们本质上意味着并行性不在桌面上(并行性很少相关,但 lambda 和流的架构从根本上设计为它们应该继续有效运行并且没有竞争条件,即使它是相关的),一旦你编写了在并行场景中失败的代码,当你试图将它采用到 lambda/stream 风格时,它往往会令人讨厌并导致问题。

没关系,我只想写我的代码!

唯一真正的选择是使用 anAtomicX来携带信息:

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)。

因此,也许,人们可能认为举证责任在于论证的这一方面(不确定这是否合乎逻辑,但以防万一,我猜)。

因此,深入分析 lambda 所没有的 3 种透明胶片及其优缺点:

  1. 它们不是检查异常透明的:如果你写:
try {
    Runnable r = () -> { throw new IOException(); }
    r.run();
} catch (IOException e) {}

它不会编译,即使你的眼球和大脑正确地告诉你它应该编译 - IOException 被抛出,并被捕获,保证。它不会编译,因为您的 lambda 主体需要“适合”Runnable接口中未声明为允许您 throw的单个抽象方法IOException,因此您不能。

可以通过在未检查的异常中积极地重新包装已检查的异常来解决它;这破坏了检查异常的意义,并且需要添加大量样板代码,使 lambda 变得冗长且笨拙。

一个基本的 for 循环/不使用 lambda,根本不会受此影响:

try {
    throw new IOException();
} catch (IOException e) {}

编译得很好。

  1. 它们不是可变的局部变量透明的。
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++;
  1. 它们不是控制流透明的。
outer:
while (true) {
    List.of("a", "b").forEach(x -> {
      // what goes here?
    }
    System.out.println("Hello");
}

在“这里发生了什么”部分,假设您不仅要中止这个特定的循环,还要中止整个forEach 运行。例如,您希望在点击时停止处理"a",甚至从不循环 for "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 “旅行”时,这三个不利因素有效地转变为有利因素。也就是说,当包含 lambda 代码的方法早已完成,或者如果 lambda 在完全不同的线程中执行时,lambda 会被存储并在稍后的某个时间运行:我上面列出的所有 3 个缺点都变得奇怪和令人困惑在这种情况下:

  1. 那个捕捉块?整个方法已经停止执行,所有状态都消失了,代码不能跳转到那里,即使在词法上它看起来应该如此,所以缺点变成了优点。

  2. 如果在另一个线程中运行的 lambda 中可以看到非最终局部变量并发生变异,则不能再在堆栈上声明局部变量,它需要静默移动到堆中。我们应该开始担心像现在一样标记我们的当地人volatile吗?所有选项,但现在在 java 中,本地定义仅限于您的线程,并且在您的范围结束时不再存在。这使得推理代码变得更容易。这些先决条件将不得不消失。这很讨厌。不利因素变成了有利因素。

  3. 在这种情况下,没有 while 循环可以继续或中断。代码将毫无意义。不利因素变成了有利因素。

这导致以下结论:

Lambda非常棒,没有任何警告,当它们用于编写超出您编写范围的代码时,要么因为它存储在一个字段中并稍后执行,要么因为它在另一个线程中运行。

Lambdas 被讨厌的警告拖累了,因此,如果它们不旅行,就会出现糟糕的风格和代码气味。即使 lambda 没有“旅行”,基于 lambda 的解决方案仍然很容易是正确的,但它们不仅仅是“默认情况下更好”。默认情况下,它们实际上更糟糕。

由于上述两条规则,我们得到第三条规则:

直接在列表上调用.forEach(elem -> {...}),或者在没有中间步骤的流上调用,总是很糟糕并且代码有异味!

换句话说,这保证是愚蠢的:

list.forEach(x -> doStuff);

set.stream().forEach(x -> doStuff);

只需使用基本的 for 循环即可。

因此.forEach应该很少使用终端。它仅在 3 个场景中是非愚蠢的代码:

  1. 底层数据结构本质上是并行的,您需要执行的处理同样是并行的,但您无需微调用于运行它的池。这是很少见的(例如,通常如果并行性与此相关,则需要更多控制,而 fork/join 是答案,或者并行性无关紧要。很少有你在中间),但如果你这样做,那么这会有所帮助,因为它们可以并行运行,而 foreach 则不能。这种情况很少相关。

  2. 您已经有一个消费者,例如传入:

public void peekEvents(Consumer<Event> consumer) {
    eventQueue.forEach(consumer);
}
  1. 你有中间体:
eventQueue.stream().filter(Event::isPublic)
  .map(Event::getWidget)
  .forEach(widget -> System.out.println("Widget with queued up events: " + widget);

在这里你过滤和映射 - 然后它开始更有意义使用forEach


推荐阅读