【问题标题】:Idiomatically enumerating a Stream of objects in Java 8在 Java 8 中以惯用方式枚举对象流
【发布时间】:2017-02-14 14:20:03
【问题描述】:

如何习惯性地枚举Stream<T>,它将每个T 实例映射到使用Java 8 流方法的唯一整数(例如,对于数组T[] values,创建一个Map<T,Integer>,其中Map.get(values[i]) == i 的计算结果为@987654326 @)?

目前,我正在定义一个匿名类,它增加一个 int 字段以用于 Collectors.toMap(..) 方法:

private static <T> Map<T, Integer> createIdMap(final Stream<T> values) {
    return values.collect(Collectors.toMap(Function.identity(), new Function<T, Integer>() {

        private int nextId = 0;

        @Override
        public Integer apply(final T t) {
            return nextId++;
        }

    }));
}

但是,使用 Java 8 流 API 是否有更简洁/优雅的方式来执行此操作? — 如果可以安全并行化,则加分。

【问题讨论】:

  • values 流中的所有值都是唯一的吗?
  • 在我的具体情况下是的,但是看到一个可以处理对象多次出现的解决方案也会很有趣。
  • @SME_Dev 绝对没有。

标签: java java-8 java-stream enumeration


【解决方案1】:

如果存在重复元素,您的方法将失败。

除此之外,您的任务需要可变状态,因此可以使用Mutable reduction 解决。当我们填充地图时,我们可以简单地使用地图的大小来获取未使用的 id。

比较棘手的部分是合并操作。下面的操作只是重复分配正确的地图,这将处理潜在的重复。

private static <T> Map<T, Integer> createIdMap(Stream<T> values) {
    return values.collect(HashMap::new, (m,t) -> m.putIfAbsent(t,m.size()),
        (m1,m2) -> {
            if(m1.isEmpty()) m1.putAll(m2);
            else m2.keySet().forEach(t -> m1.putIfAbsent(t, m1.size()));
        });
}

如果我们依赖独特的元素,或者插入一个显式的distinct(),我们可以使用

private static <T> Map<T, Integer> createIdMap(Stream<T> values) {
    return values.distinct().collect(HashMap::new, (m,t) -> m.put(t,m.size()),
        (m1,m2) -> { int leftSize=m1.size();
            if(leftSize==0) m1.putAll(m2);
            else m2.forEach((t,id) -> m1.put(t, leftSize+id));
        });

}

【讨论】:

  • 我喜欢关于地图大小的技巧。聪明的。但是为什么你需要检查if(leftSize==0)?这是一个非并发收集器,因此将调用供应商来获取流中的尽可能多的元素,然后累加器将在组合器之前将一个元素放入一个空映射中
  • @Eugene:合并函数将被调用以获得部分结果。这些取决于流源的拆分能力(即是否可以平衡拆分)以及中间是否存在大小更改操作(filterflatMap)。因此,组合器函数可能会以空的部分结果被调用。这仍然没有在此处必需对空地图进行测试,因为普通的合并操作会做正确的事情。这只是一种优化,在这种情况下使用廉价的测试并简化操作。
  • 不过,三参数 collect 方法不允许将其最大化。如果您通过Collector.of 创建自定义收集器,您甚至可以返回第二个地图,如果第一个为空,则省略整个putAll 操作。如果您对有关工作拆分和潜在空部分结果的更多详细信息感兴趣,可以考虑this Q&A
【解决方案2】:

我会这样做:

private static <T> Map<T, Integer> createIdMap2(final Stream<T> values) {
    List<T> list = values.collect(Collectors.toList());
    return IntStream.range(0, list.size()).boxed()
            .collect(Collectors.toMap(list::get, Function.identity()));
}

为了或者并行,可以改成

   return IntStream.range(0, list.size()).parallel().boxed().
                (...)

【讨论】:

  • 你说的很对,我没注意到parallel()可以直接调用,谢谢
  • 如果你能负担得起中间存储,你的解决方案是简单而足够的。如前所述,如果预期有重复,您可以使用List&lt;T&gt; list = values.distinct().collect(Collectors.toList());。不过,好吧,您不必这样做;在任何一种情况下,id 都是唯一的,没有人说它们不允许有间隙……
  • 这个答案比Holger's 更具可读性,但不必使用中间列表会很好。同样可悲的是,Collectors.toList() 似乎不能保证返回随机访问列表,这意味着您的解决方案的复杂性可能会有很大差异; Java 标准库中类似于 Python 的 enumerate(iterable) 的东西会不会很棒?
  • @errantlinguist:原则上,您是对的,因为文档没有说明该列表将具有随机访问权限,但另一方面,随机访问属性不在显式命名的属性中未指定的(类型、可变性、可序列化性、线程安全性)。我认为,可以合理地假设返回的列表永远不会有昂贵的get 操作。而且这个解决方案仍然有效,如果这个假设不成立,它只会更慢(由决定让toList()返回一个没有随机访问的列表的人负责)......
【解决方案3】:

Andremoniy提供的解决方案中比较先将输入流转换为List。我宁愿以不同的方式来做,因为我们不知道“toList()”和“list.get(i)”的成本,而且没有必要创建一个额外的列表,它可以小也可以大

private static <T> Map<T, Integer> createIdMap2(final Stream<T> values) {
    final MutableInt idx = MutableInt.of(0); // Or: final AtomicInteger idx = new AtomicInteger(0);        
    return values.collect(Collectors.toMap(Function.identity(), e -> idx.getAndIncrement()));
}

不管这个问题,我认为在方法中将流作为参数传递是一个糟糕的设计。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-01-03
    • 2014-06-09
    • 2014-03-31
    • 1970-01-01
    • 1970-01-01
    • 2014-06-11
    • 2013-08-03
    相关资源
    最近更新 更多