【问题标题】:How to map elements to their index using streams?如何使用流将元素映射到它们的索引?
【发布时间】:2019-08-24 12:59:00
【问题描述】:

我得到一些自定义对象的流,我想创建一个映射Map<Integer, MyObject>,其中每个对象的索引作为键。举个简单的例子:

Stream<String> myStream = Arrays.asList("one","two","three").stream();
Integer i = 0;
Map<Integer, String> result3 = myStream.collect(Collectors.toMap(x -> i++, x -> x));

显然,这不会编译,因为:

从 lambda 表达式引用的局部变量必须是 final 或 有效地最终

是否有一种简单的方法可以将流的元素映射到它们的索引,以便上述示例的预期输出类似于:

{1=one, 2=two, 3=three}

【问题讨论】:

    标签: java java-8 java-stream


    【解决方案1】:

    您可以使用IntStream 来解决这个问题:

    List<String> list = Arrays.asList("one","two","three");
    Map<Integer, String> map = IntStream.range(0, list.size()).boxed()
            .collect(Collectors.toMap(Function.identity(), list::get));
    

    您创建一个从0list.size() - 1IntStreamIntStream.range() 不包括流中的最后一个值)并将每个索引映射到列表中的值。此解决方案的优点是,它还可以与并行流一起使用,而使用 AtomicInteger 则无法做到这一点。

    所以这种情况下的结果是:

    {0=one, 1=two, 2=three}
    

    要在1 开始第一个索引,您只需在收集期间添加1

    List<String> list = Arrays.asList("one", "two", "three");
    Map<Integer, String> map = IntStream.range(0, list.size()).boxed()
            .collect(Collectors.toMap(i -> i + 1, list::get));
    

    这将导致:

    {1=one, 2=two, 3=three}
    

    【讨论】:

    • 我曾认为这是一种解决方法,但如果可能的话,我想避免先将流转换为列表。无论如何,非常感谢您的回答。
    • 这不适用于未知(可能无限)大小的流...
    【解决方案2】:

    你的 i 变量实际上不是最终的。

    您可以将AtomicInteger 用作Integer 包装器:

    Stream<String> myStream = Arrays.asList("one","two","three").stream();
    AtomicInteger atomicInteger = new AtomicInteger(0);
    Map<Integer, String> result3 = myStream.collect(Collectors.toMap(x -> atomicInteger.getAndIncrement(), Function.identity()));
    

    我认为它有点 hacky,因为它只解决了有效的 final 变量的问题。由于它是一个特殊的 ThreadSafe 版本,它可能会引入一些开销。 answer by Samuel Philipp 中的纯 stream 解决方案可能更适合您的需求。

    【讨论】:

    • 直到现在对 AtomicInteger 一无所知。谢谢,解决了我的问题。
    • AtomicInteger 是线程安全的,它可能会引入一些开销。我认为它有点hacky。我认为 SamuelPhilipp 的解决方案比这更好。另请注意,它仅解决了您的变量不是有效最终的问题。
    • 具有讽刺意味的是,由于依赖于处理顺序,它承担了线程安全操作的成本,而不是线程安全的。 A clean solution 没有这些问题,是可能的……
    • 您不必使用 AtomicInteger。一个简单的单元素 int 数组就可以了。没有更多的假线程安全开销。并且收集流然后重新流,这听起来也是不必要的开销。
    【解决方案3】:

    不需要随机访问源数据的干净解决方案是

    Map<Integer,String> result = Stream.of("one", "two", "three")
        .collect(HashMap::new, (m,s) -> m.put(m.size() + 1, s),
            (m1,m2) -> {
                int offset = m1.size();
                m2.forEach((i,s) -> m1.put(i + offset, s));
            });
    

    这也适用于并行流。

    在不太可能的情况下,这是一项重复性任务,值得将逻辑放入可重用的收集器中,包括一些优化:

    public static <T> Collector<T,?,Map<Integer,T>> toIndexMap() {
        return Collector.of(
            HashMap::new,
            (m,s) -> m.put(m.size() + 1, s),
            (m1,m2) -> {
                if(m1.isEmpty()) return m2;
                if(!m2.isEmpty()) {
                    int offset = m1.size();
                    m2.forEach((i,s) -> m1.put(i + offset, s));
                }
                return m1;
            });
    }
    

    然后可以像这样使用

    Map<Integer,String> result = Stream.of("one", "two", "three")
        .collect(MyCollectors.toIndexMap());
    

    Map<Integer,Integer> result = IntStream.rangeClosed(1, 1000)
        .boxed().parallel()
        .collect(MyCollectors.toIndexMap());
    

    【讨论】:

      【解决方案4】:

      试试这个:

      让我们说String[] array = { "V","I","N","A","Y" };

      Arrays.stream(array) 
              .map(ele-> index.getAndIncrement() + " -> " + ele) 
              .forEach(System.out::println); 
      

      输出:

      0 -> V
      1 -> I
      2 -> N
      3 -> A
      4 -> Y
      

      【讨论】:

      • 我猜索引也是 AtomicInteger 类型。谢谢你的例子。
      【解决方案5】:

      Guava 有一个静态方法Streams#mapWithIndex

      Stream<String> myStream = Stream.of("one","two","three");
      Map<Long, String> result3 = Streams.mapWithIndex(myStream, (s, i) -> Maps.immutableEntry(i + 1, s))
          .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
      
      // {1=one, 2=two, 3=three}
      System.out.println(result3);
      

      【讨论】:

        【解决方案6】:

        我们可以使用List.indexOf(Object o)方法获取列表中元素的索引,同时构造Map

         List<String> list = Arrays.asList("one","two","three");
         Map<Integer, String> result = list.stream()
                                           .collect(Collectors.toMap(
                                            k -> list.indexOf(k) + 1, 
                                            Function.identity(),
                                            (v1, v2) -> v2));
        
         System.out.println(result);
        

        如果列表中有重复项,则将元素第一次出现的索引添加到最终映射中。此外,为了解决键冲突期间地图中的合并错误,我们需要确保有一个合并功能提供给toMap(keyMapper, valueMapper, mergeFunction)

        输出:

        {1=one, 2=two, 3=three}
        

        【讨论】:

        • 使用indexOf 不是一个好主意,原因有二: 它对于大型列表的性能较差,因为它的时间复杂度为 O(n) (对于ArrayList)并且如果列表包含一个值多次你得到错误的结果,因为indexOf 只返回第一个索引,所以你将覆盖第一个值。
        • @SamuelPhilipp 你是对的,即使我已经想到了,所以这个解决方案适用于一个小列表。但是 OP 特别询问“是否有一种简单的方法将流的元素映射到它们的索引”,所以 OP 想要列表中的实际索引。是的,如果有重复元素的索引的最后一次出现将被添加到地图中。
        • 感谢您添加一种 mor 方法。但是如果我添加一个重复的字符串'one',这会产生一个java.lang.IllegalStateException: Duplicate key one,但如果列表包含唯一元素,它将起作用。
        • @nopens 是的,我忘了在toMap 的最后一个参数中添加mergeFunction,它解决了重复键的合并错误
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2018-03-06
        • 1970-01-01
        • 1970-01-01
        • 2018-01-17
        • 1970-01-01
        • 2021-12-24
        • 1970-01-01
        相关资源
        最近更新 更多