Java 8使用流将列表转换为映射表

50

我有一个List<Item>集合。 我需要将它转换为Map<Integer, Item> 地图的键必须是项目在集合中的索引。 我无法通过流弄清楚如何做到这一点。 类似于:

items.stream().collect(Collectors.toMap(...));

需要帮忙吗?

由于这个问题被识别为可能重复的,所以我需要补充一下我的具体问题是-如何获取列表中项目的位置并将其作为键值放置。


7
也许您需要将Java 8中的List转换为Map,可以参考以下链接:Maybemaybemaybe。这些链接提供了相应的示例代码和解释。 - MadProgrammer
3
使用我免费的 StreamEx 库,对 items 进行 EntryStream.of(items).toMap() 操作。详细文档请参阅这里 - Tagir Valeev
1
经过一些研究,我了解到令人惊讶的是,在Java 8流中没有“zip”函数。 - njzk2
@njzk2,这是因为您无法并行化压缩流。如果有随机访问源(例如两个ArrayList),通过IntStream.range(0,list1.size()).mapToObj(idx -> doSomethingWith(list1.get(idx), list2.get(idx)))将它们压缩在一起并不是很困难,结果将是并行友好的。 - Tagir Valeev
6个回答

50
您可以使用 IntStream 创建一个索引的 Stream,然后将它们转换为 Map
Map<Integer,Item> map = 
    IntStream.range(0,items.size())
             .boxed()
             .collect(Collectors.toMap (i -> i, i -> items.get(i)));

只要“items”不再是“List”,它就会停止工作。如果“items”是LinkedList而不是ArrayList,它的效率非常低下。 - njzk2
如果您没有一个List或者通过索引访问代价很高,请参考我的回答。@njzk2 - Stuart Marks

13

为了完整起见,还有一种解决方案是使用自定义收集器:

public static <T> Collector<T, ?, Map<Integer, T>> toMap() {
    return Collector.of(HashMap::new, (map, t) -> map.put(map.size(), t), 
            (m1, m2) -> {
                int s = m1.size();
                m2.forEach((k, v) -> m1.put(k+s, v));
                return m1;
            });
}

使用方法:

Map<Integer, Item> map = items.stream().collect(toMap());

这个解决方案支持并行处理,不依赖于源代码(可以使用不带随机访问的列表或Files.lines()等)。

只有在以下两个条件都满足时,此方法才有效:a/ 组合器保证以正确的顺序调用 m1 和 m2;b/ 每个累加映射都在连续的项目序列上调用。如果例如在一个映射中累积奇数值,在另一个映射中累积偶数值,则会出现错误。我没有找到任何来源表明这种情况不可能发生。 - njzk2
1
@njzk2,如果您的流是有序的,那么这种情况就不会发生。这样所有现有的收集器(如toList())都可以正常工作。 - Tagir Valeev
我想这很有道理。我将研究一下收集器如何在并行化发生后保证流的顺序。 - njzk2
@njzk2,Collector合约在API文档中有详细描述。我已经履行了这个合约。当正确的映射和新元素传递给累加器时,它会产生一个新的正确映射。当两个正确的映射传递给组合器时,它会产生一个新的正确映射。只要履行合约,你就能得到正确的结果。这就是接口的美妙之处。 - Tagir Valeev
谢谢。看起来流被分成了子字符串,而不是子序列,所以这确实有效! - njzk2

11

不要感觉你必须在/与直播中做 所有事情。我只会这样做:

AtomicInteger index = new AtomicInteger();
items.stream().collect(Collectors.toMap(i -> index.getAndIncrement(), i -> i));
只要不对流进行并行处理,这个方法就可以工作,并且避免了可能昂贵和/或有问题(在重复情况下)的get()indexOf()操作。

(你不能使用一个常规的int变量代替AtomicInteger,因为在lambda表达式外部使用的变量必须是有效的最终变量。请注意,当没有竞争时(如本例),AtomicInteger非常快,不会造成性能问题。但如果你担心,你可以使用非线程安全的计数器。)


4
你认为List.get()操作很耗费资源,但建议使用AtomicInteger - Holger
4
不行。 - Pepijn Schmitz
3
只有当你使用LinkedList时,它才真正有用。另一方面,AtomicInteger的复杂度为O(1)并不重要,因为在您自己承认的不支持并行操作的情况下,线程安全性的隐藏成本是一个问题。如果您以“不必在流中完成所有操作”开始回答,为什么不提供一个没有流的替代方案,比如一个直接的循环?这将比呈现一个被鼓励避免的流用法更好。 - Holger
2
@Holger,OP没有指定List的实现方式。你似乎对LinkedList有偏见,但实际上它并没有什么问题,而且List很容易就可以是它,或者甚至是另一种更昂贵的实现方式。为什么要猜测呢?这种方式总是最快的。 - Pepijn Schmitz
2
我并不是对 LinkedList 有偏见,因为它已经存在了十五年以上的时间,这足以证明它在实际生活中并不实用。理论上的优势只有一个操作,即在任意索引处插入,但由于它必须为此分配内存并更新半打节点引用,因此这种优势并没有真正体现出来。只有在非常大的列表中,才需要使用 LinkedList 来超越 ArrayList,然而对于大型列表,LinkedList 的疯狂内存开销将抵消它的优势。LinkedList 只在忽略内存效应的 O(…) 比较中获胜。 - Holger
显示剩余6条评论

7

这是更新的答案,没有评论中提到的任何问题。

Map<Integer,Item> outputMap = IntStream.range(0,inputList.size()).boxed().collect(Collectors.toMap(Function.identity(), i->inputList.get(i)));

5
如果列表中有重复的Item,这将失败。 - Misha
11
不要对大型列表这样操作,除非你想通过实例了解什么是 O(n²) - Holger
list.indexOf(i) 很_慢_。我不建议使用这种方法。 - Boris the Spider
这种方法存在太多的限制和低效性,因此它并不是一个有用的解决方案。 - njzk2

1
使用第三方库(例如protonpack,但也有其他库可用),您可以将值与其索引一起进行压缩,然后就完成了:
StreamUtils.zipWithIndex(items.stream())
    .collect(Collectors.toMap(Indexed::getIndex, Indexed::getValue));

尽管getIndex返回一个long,因此您可能需要使用类似以下内容进行转换:
i -> Integer.valueOf((int) i.getIndex())

1
使用@TagirValeev的库,仅需一行代码即可实现:EntryStream.of(items).toMap(); - Jean-François Savard
1
@Jean-FrançoisSavard,更不用说zipWithIndex创建的流根本无法并行化。 - Tagir Valeev
@TagirValeev,你有信心使用EntryStream吗? - njzk2
1
@njzk2,确实是这样。但是,如文档所说,它依赖于快速随机访问。内部类似于 Eran 的解决方案(也可以并行化,只适用于随机访问源的合理速度)。相比之下,protonpack 解决方案不需要随机访问(粗略地说,它更接近 Pepijn Schmitz 的答案)。 - Tagir Valeev
@TagirValeev 同样的评论适用于此:它仅适用于 List,其复杂度取决于列表实现中 get(i) 的访问时间。 - njzk2

1

Eran's answer通常是处理随机访问列表的最佳方法。

如果你的List不是随机访问的,或者你有一个Stream而不是List,你可以使用forEachOrdered

Stream<Item> stream = ... ;
Map<Integer, Item> map = new HashMap<>();
AtomicInteger index = new AtomicInteger();
stream.forEachOrdered(item -> map.put(index.getAndIncrement(), item));

如果流是并行的,则这是安全的,即使目标映射是线程不安全的并且作为副作用被操作。 forEachOrdered 保证按顺序逐个处理项目。因此,运行并行化很少会产生任何加速效果。 (如果在 forEachOrdered 之前有昂贵的操作,可能会有一些加速。)


1
为什么这么复杂?使用 forEachOrdered,您不需要 AtomicInteger,只需使用 stream.forEachOrdered(item -> map.put(map.size(), item))。读取非易失性字段 HashMap.size,该字段无论如何都会更新,与在 AtomicInteger 中使用 CAS 没有什么区别。 - Tagir Valeev
@TagirValeev 我想是的。我的主要观点是使用 forEachOrdered,这一点还没有被提到。 - Stuart Marks
当然,这是一个相当简短的解决方案,但使用我版本中提出的“Collector”在概念上更加正确。实际上,最好的解决方案是使用toList()并编写一个特殊的适配器(基于AbstractMap<Integer, T>),将List<T>适配到Map<Integer, T>。将它们存储到HashMap中只是浪费时间和内存。 - Tagir Valeev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接