如何在Java Stream中增加一个值?

10

我希望每次循环都将index的值递增1。在for-loop中可以轻松实现。变量image是一个包含多个ImageView的数组。

下面是我的for-loop

for (Map.Entry<String, Item> entry : map.entrySet()) {      
    image[index].setImage(entry.getValue().getImage());
    index++;
}

为了练习Stream,我尝试将其重写为Stream
map.entrySet().stream()
    .forEach(e -> item[index++].setImage(e.getValue().getImage()));

导致错误的原因:

错误: lambda表达式引用的本地变量必须是final或有效的final

如何重写使用增加变量indexStream


1
选项1:将lambda表达式扩展为匿名类,该类具有实例字段“private int index”。选项2:声明“int[] index = {0}”,然后使用“index [0] ++”。 - Marko Topolnik
第二种方法可行。您能解释一下为什么使用“int数组”而不是“int”?还有没有其他使用不同流的方式? - Nikolas Charalambidis
1
它能够正常工作是因为它满足只引用有效最终变量的要求。您不再在 lambda 中重新分配“ index”变量。 - Marko Topolnik
谢谢你的帮助。请给我写一个答案 :)) - Nikolas Charalambidis
@FedericoPiazza 我之前查看了那个问题,但是没有得到答案。这就是为什么我提出了自己的问题。 - Nikolas Charalambidis
2个回答

11
你不应该这样做。虽然它们看起来相似,但概念上是不同的。循环只是一个循环,而forEach指示库对每个元素执行操作,不指定动作顺序(用于并行流)或执行它们的线程。如果使用forEachOrdered,则仍然没有关于线程的保证,但至少您有在后续元素上的操作之间 happens-before 关系的保证。
请特别注意文档中所说:

对于任何给定的元素,操作可以在库选择的任何时间和任何线程中执行。如果操作访问共享状态,则负责提供所需的同步。

正如@Marko在下面的评论中所指出的那样,尽管措辞有点令人困惑,但它仅适用于并行流。无论如何,使用循环意味着您甚至不必担心所有这些复杂的事情!
因此,要在函数中使用循环逻辑,而如果您只想告诉Java对流的元素“这样做”或“那样做”,则使用forEach。这是关于forEach与循环的区别。现在讨论为什么变量需要首先是final的,以及为什么可以对类字段和数组元素进行操作。这是因为Java有一个限制,即匿名类和lambda无法访问本地变量,除非它从未更改过。这意味着它们不仅不能自己更改它,而且您也不能在它们外部更改它。但是这仅适用于局部变量,这就是为什么它适用于其他所有内容(如类字段或数组元素)的原因。
我认为这种限制的原因是生命周期问题。局部变量仅在包含它的块执行时存在。其他所有内容都存在于有引用的情况下,感谢垃圾回收。而这一切也包括lambda和匿名类,因此,如果它们可以修改具有不同生命周期的局部变量,那么可能会导致类似于C ++中的悬挂引用的问题。所以Java采取了简单的方法:它只是在创建lambda /匿名类时复制局部变量。但是,如果您可以更改该变量,这将导致混淆(因为副本不会更改,并且由于副本是不可见的,这将非常令人困惑)。因此,Java禁止对这些变量进行任何更改,就是这样。
已经讨论了有关final变量和匿名类的许多问题,例如this one

2
@Marko Topolnik:数组元素和映射元素之间必须存在预先的关系,否则谁能保证数组的大小正确?我猜这整个问题都是一个xy问题,问题中省略了数组创建,而让流在第一次创建数组会更好。否则,使用从entry set创建的流没有任何好处,特别是没有消除任何顺序与并行问题(以及过时的e.getValue()调用)的map.values().forEach(…)... - Holger
1
@Marko,针对你的第一条评论:你是对的,我已经添加了一个注释,指出未定义的顺序仅适用于并行流。我不太明白你所说的“不管在哪个线程上都无所谓”的意思。当然,如果你在消费者内部执行各种操作,那么它确实很重要。文档明确表示:“如果该操作访问共享状态,则应负责提供所需的同步。”我将编辑以添加该引用。 - Sergei Tachenov
1
@Marko,有没有相关的证明链接?我有点困惑于你的“绝对不重要”和文档中的“动作可以在库选择的任何时间和任何线程中执行”的区别。是否有明确的保证所有操作甚至会在forEach调用返回时完成? - Sergei Tachenov
1
@Marko,问题是在哪里说了呢?听起来很合理,但是在Stream文档中搜索“happens”并没有找到相关内容。如果你所说的是真的,那么“如果操作访问共享状态,则负责提供所需的同步。”具体意思是什么呢?(我并不是说你说的不对。) - Sergei Tachenov
1
@Marko,哦,我明白了。我一直在阅读Stream文档,而不是java.util.stream文档。是的,现在有点更清楚了,尽管如果他们明确提到happens-before关系,那就更清楚了,这既可以避免混淆,又可以实现高效搜索。 - Sergei Tachenov
显示剩余12条评论

2

这里需要一种“压缩”操作,但标准的Stream API并不支持。一些扩展Stream API的第三方库提供了此功能,其中包括我的免费StreamEx库:

IntStreamEx.ints() // get stream of numbers 0, 1, 2, ...
           .boxed() // box them
           .zipWith(StreamEx.ofValues(map)) // zip with map values
           .forKeyValue((index, item) -> image[index].setImage(item.getImage()));

请查看zipWith文档获取更多详细信息。请注意,您的映射应具有有意义的顺序(例如LinkedHashMap),否则这将是相当无用的...

1
点击了+1,因为使用扩展库的方式很好。不过我更喜欢使用默认的库:)) - Nikolas Charalambidis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接