【问题标题】:Java 8 lambda: convert Collection to Map of element, iteration positionJava 8 lambda:将集合转换为元素的映射,迭代位置
【发布时间】:2014-07-30 15:44:03
【问题描述】:

如何将 ["a", "b", "c"] 之类的集合转换为 {"a": 0, "b":1, "c":2} 之类的映射,其值为迭代的顺序。 JDK8中是否有一个带有流和收集器的衬垫? 老式的方式是这样的:

    Collection<String> col = apiCall();
    Map<String, Integer> map = new HashMap<>();
    int pos = 0;
    for (String s : collection) {
        map.put(s, pos++);
    }

【问题讨论】:

  • 请注意,您的代码将创建地图{"a": 0, "b": 1, "c": 2},到目前为止的答案也随之而来。
  • @ajb 谢谢,更新了问题

标签: java lambda java-8 java-stream


【解决方案1】:

如果您不需要并行流,您可以使用地图的长度作为索引计数器:

collection.stream().forEach(i -> map.put(i, map.size() + 1));

【讨论】:

  • 恐怕这是一个糟糕的解决方案:它不是线程安全的,因为不同的元素可能由不同的线程处理,即使在顺序流上也是如此。此外(不太重要)forEach 不能保证维持秩序。
  • @Maurice Naftalin:“顺序”是“平行”的相反。可能您的意思是“有序”,它仍然可以并行。
  • @Holger 抱歉,我通过删除您回复的评论混淆了这个线程,因为我想修改它。对于阅读本文的任何人,我所说的是:“不,我的意思是我所说的:“即使在顺序流上”。java.util.stream 的文档说(在“副作用”下):“即使当管道受限于产生与流源的遭遇顺序一致的结果,不保证......对于给定元素执行任何行为参数的线程。”
  • @Holger 我删除的评论的问题是它是正确的但不适用;我提到的短语记录了中间操作的行为。我需要的文档是forEach:“对于任何给定的元素,可以在库选择的任何时间和任何线程中执行操作。如果操作访问共享状态,它负责提供所需的同步。”所以这个解决方案不是线程安全的。 (forEachOrdered 不会遇到同样的问题,所以下面的解决方案有效)。
  • @Maurice Naftalin:然后我们可以删除 cmets。关于forEach,我不确定引用的句子是否仍然属于前面以“For parallel stream pipelines”开头的句子。
【解决方案2】:

这是一种方法:

List<String> list = Arrays.asList("a", "b", "c");

Map<String, Integer> map =
    IntStream.range(0, list.size())
        .boxed()
        .collect(toMap(i -> list.get(i), i -> i));

不一定是单行或比直接循环更短,但如果将toMap 更改为toConcurrentMap,它确实可以使用并行流。

另外请注意,这假设您有一个随机访问列表,而不是一般的Collection。如果您有一个 Collection ,否则您无法做出任何假设,那么除了按顺序迭代它并增加一个计数器之外,您无能为力。

更新

OP 已澄清输入是Collection 而不是List,因此上述内容不适用。似乎我们可以对输入Collection 做出很少的假设。 OP 已指定迭代顺序。使用顺序迭代器,元素将以 some 顺序出现,尽管不能保证它。它可能会从一个运行到另一个运行,甚至从一个迭代到下一个迭代(尽管这在实践中是不寻常的——除非底层集合被修改)。

如果需要保留确切的迭代顺序,我认为没有办法将其保留到结果 Map 中而不依次迭代输入 Collection

但是,如果确切的迭代顺序并不重要,并且要求输出 Map 对每个输入元素具有唯一值,则可以并行执行类似的操作:

Collection<String> col = apiCall();
Iterator<String> iter = col.iterator();

Map<String, Integer> map =
    IntStream.range(0, col.size())
        .parallel()
        .boxed()
        .collect(toConcurrentMap(i -> { synchronized (iter) { return iter.next(); }},
                                 i -> i));

现在这单线。我也不清楚它有多大用处。 :-) 但它确实表明可以并行执行类似的操作。请注意,我们必须同步对输入集合的迭代器的访问,因为它将从多个线程中调用。另请注意,这是迭代器的不寻常用法,因为我们从不调用hasNext,并且我们假设调用next 的次数与输入集合的size() 返回的次数完全相同。

【讨论】:

  • 我有一个收藏。我知道 List 的这个解决方案。
  • @danial 对不起,我假设了List。感谢您的澄清。我已经更新了只处理Collection 的答案。您可能会或可能不会发现它有用。 :-)
  • i -&gt; i 可以替换为 Function.identity()
【解决方案3】:

基于maba’s answer,一般解决方案是:

collection.stream().forEachOrdered(i -> map.put(i, map.size()));

来自documentation of void forEachOrdered(Consumer&lt;? super T&gt; action)

此操作一次处理一个元素,如果存在则按遇到的顺序。

这里的重要方面是如果有订单,它会保留订单,例如如果CollectionSortedSetList。这样的流称为有序流(不要与排序流混淆)。它可能由不同的线程调用消费者方法,但始终确保“一次一个”和线程安全保证。

当然,如果流是并行的,它不会从并行执行中受益。


为了完整起见,如果它们仍然是有序,那么即使在使用并行处理的并行流上也可以使用以下解决方案:

stream.collect(HashMap::new, (m, i) -> m.put(i, m.size()),
  (a, b) -> {int offset = a.size(); b.forEach((k, v) -> a.put(k, v + offset));});

【讨论】:

    【解决方案4】:

    如果您不介意使用 3rd 方库,我的 cyclops-react 库具有所有 JDK Collection 类型的扩展,并附有大量强大的操作,因此您可以像这样实现:-

        CollectionX<String> col = CollectionX.fromCollection(orgCol);
        col.zipWithIndex()
           .toMap(k->k.v1, v->v.v2);
    

    cyclops-react Collection 扩展是急切的,所以你可以使用我们的 Stream 扩展获得更好的性能,ReactiveSeq(它扩展了 jOOλ 的 Seq,它又是 JDK 的 java.util.stream.Stream 的扩展,它还实现了反应流 api)。

        ReactiveSeq.fromCollection(col)
                   .zipWithIndex()
                   .toMap(k->k.v1, v->v.v2);
    

    【讨论】:

      【解决方案5】:

      您可以使用 AtomicInteger 作为流中的索引:

      
          Collection col = Arrays.asList("a", "b", "c");
          AtomicInteger index = new AtomicInteger(0);
          Map collectedMap =  col.stream().collect(Collectors.toMap(Function.identity(), el -> index.getAndIncrement()));
          System.out.println("collectedMap = " + collectedMap);
      

      【讨论】:

        【解决方案6】:

        试试

            int[] pos = { 0 };
            list.forEach( a -> map.put(a, pos[0]++));
        

        【讨论】:

        • 你怎么知道这个合集不使用并行蒸汽?
        • @fabian:我没看懂问题?
        • @fabian A Stream 似乎没有参与这里。
        • 流不需要在单线程上运行。因此,在流上使用 forEach 可能会导致 lambda 表达式的并行执行。
        • @SyamS:Iterator.forEach 也是如此,正如 javadoc 中的这句话所阐明的那样:“除非实现类另有说明,否则按顺序执行操作的迭代”。这句话告诉你执行的顺序可能和迭代器中的不一样。而且这个问题并没有说明使用了什么样的集合。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-03-06
        • 2023-03-15
        相关资源
        最近更新 更多