如何使用流将元素映射到其索引?

nop*_*ens 12 java java-8 java-stream

我得到了一些自定义对象的流,我想创建一个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));
Run Code Online (Sandbox Code Playgroud)

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

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

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

{1=one, 2=two, 3=three}
Run Code Online (Sandbox Code Playgroud)

Sam*_*ipp 11

您可以使用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));
Run Code Online (Sandbox Code Playgroud)

您创建一个IntStream0list.size() - 1IntStream.range()从流中排除最后一个值)并将每个索引映射到列表中的值。该解决方案的优势在于,它也可以与并行流一起使用,而使用不能实现AtomicInteger

因此,这种情况下的结果将是:

{0=one, 1=two, 2=three}
Run Code Online (Sandbox Code Playgroud)

要开始第一个索引,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));
Run Code Online (Sandbox Code Playgroud)

这将导致:

{1=one, 2=two, 3=three}
Run Code Online (Sandbox Code Playgroud)


Hol*_*ger 6

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

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));
        });
Run Code Online (Sandbox Code Playgroud)

这也适用于并行流。

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

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;
        });
}
Run Code Online (Sandbox Code Playgroud)

然后可以像

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

要么

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


mic*_*alk 5

您的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()));
Run Code Online (Sandbox Code Playgroud)

我认为这有点棘手,因为它只能解决有效的最终变量问题。由于它是特殊的ThreadSafe版本,因此可能会带来一些开销。塞缪尔·菲利普(Samuel Philipp)stream答案中的纯解决方案可能会更好地满足您的需求。

  • 具有讽刺意味的是,由于依赖于处理顺序,这承担了线程安全操作的成本,而并非线程安全。[干净的解决方案](/sf/answers/4035860111/)可能没有这些问题…… (4认同)
  • AtomicInteger是线程安全的,可能会带来一些开销。我认为这有点骇人听闻。我认为SamuelPhilipp的解决方案比这更好。还要注意,这仅解决了变量不能有效地最终确定的问题。 (3认同)