【问题标题】:Converting to lambda expression with ForEach使用 ForEach 转换为 lambda 表达式
【发布时间】:2017-09-10 19:31:42
【问题描述】:

有如下示例代码(此示例代码只是为了说明具体问题而写的):

package test;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Test {

    private static List<Integer> integerList = Arrays.asList(1,3,5,7,9,11,13);

    private static Map<Integer, Integer> integerMap = new HashMap<>();

    public static void main(String[] args) {
        Integer number = 0;
        for (Integer integer : integerList) {
            integerMap.put(number, integer);
            number++;
        }
        System.out.println(integerMap.get(3));
    }
}

我想使用 stream()forEach() 将其转换为 Lambda 表达式,但由于我有一个变量 number 不是 final(根据 lambda 表达式中的要求),那么这个特定的 for 循环是不适合转成lambda表达式?

【问题讨论】:

  • 上面的代码有什么问题?为什么一定要stream/forEach?
  • 没什么,只是想利用Java 8中引入的stream()forEach()的内部迭代器特性。

标签: for-loop lambda foreach java-8 java-stream


【解决方案1】:

问题是在你的循环中你正在修改一个变量,流并不真正适合那个(你可以使用AtomicInteger - 但它太多了)。因此,您可以迭代键的索引:

IntStream.range(0, integerList.size())
        .boxed()
        .collect(Collectors.toMap(Function.identity(), integerList::get));

【讨论】:

  • 是的,这解决了我的具体问题,其中有一个确定性模式,number 在每次迭代中递增 1。如果我们想让它成为通用的(for 循环内的变量变化,可能不会增加 1,而是一些其他随机动作),将其转换为 lambda 表达式的通用方法是什么?
  • @user892960 没有通用的方法;要么这个随机动作可以安全地表达为流的文档,要么你假装 - 使其安全(如AtomicInteger)。
  • 据我了解,这一切都归结为 Lambda 表达式的行为,在这种情况下,我想利用内部迭代器。当涉及到内部迭代器时,编码器不控制它何时/如何执行(与 for 循环相反),因此编码器不能控制表达式本身(用于后续迭代)中的不同状态,因为它必须是静态的/最终的。在这种情况下,lambda 表达式为所有流/foreach 元素定义一个单一的行为。
【解决方案2】:

如果你的目标只是内部迭代,你可以使用

Map<Integer, Integer> integerMap = new HashMap<>();
integerList.forEach(i -> integerMap.put(integerMap.size(), i));

要支持目标映射本身无法表达的任意临时可变状态,您需要一个持有该状态的可变对象,或者作为辅助对象,或者将Consumer 实现为内部类。

相当于有状态循环的流是Collector。要继续使用升序键号的示例,您可以使用

Map<Integer, Integer> integerMap = integerList.stream()
    .collect(HashMap::new,
             (m,i) -> m.put(m.size(),i),
             (m1,m2) -> {
                 if(m1.isEmpty()) m1.putAll(m2);
                 else if(!m2.isEmpty()) {
                     int offset = m1.size();
                     m2.forEach((k,v) -> m1.put(k+offset, v));
                 }
             });

其中最复杂的是合并部分结果的组合函数,它甚至不用于顺序执行,但对于收集器来说是必需的。

Collector API 已经支持使用临时对象来保存中间可变状态,以便在最后一步转换为最终结果。

例如使用随机增量而不是一个:

Map<Integer, Integer> integerMap = integerList.stream()
    .collect(Collector.of(() -> new Object() {
                 HashMap<Integer,Integer> map = new HashMap<>();
                 int lastKey;
             },
             (tmp,i) -> tmp.map.put(
                 tmp.lastKey += ThreadLocalRandom.current().nextInt(10)+1,
                 i),
             (tmp1,tmp2) -> {
                 if(tmp1.map.isEmpty()) return tmp2;
                 if(tmp2.map.isEmpty()) return tmp1;
                 int offset = tmp1.lastKey;
                 tmp2.map.forEach((k,v) -> tmp1.map.put(k + offset, v));
                 tmp1.lastKey += tmp2.lastKey;
                 return tmp1;
             },
             tmp -> tmp.map));

现在收集器有第四个函数,它将中间状态对象转换为最终结果。

【讨论】:

  • 我认为这个integerList.forEach(i -&gt; integerMap.put(integerMap.size(), i));IntStream 解决方案更直观。进一步的解释只是告诉我们,循环中的随机变量修改不适合用 Lambda 表示,因为解决方案比问题本身复杂得多:)。
  • @user892960:确实,除非您拥有非常大的数据集并希望从并行处理中受益,否则将有状态循环转换为流没有任何优势。但另一个信息是,虽然它很复杂,但并非不可能。如果这是一项重复性任务,则将复杂性隐藏在收集器中(例如 builtin collectors)可以简化使用站点。
猜你喜欢
  • 1970-01-01
  • 2018-02-20
  • 1970-01-01
  • 2013-09-07
  • 1970-01-01
  • 2016-02-10
  • 2012-01-15
  • 1970-01-01
相关资源
最近更新 更多