当使用流(Stream)中的Collections.toMap()方法时,如何保留List的迭代顺序?

106

我正在按以下方式从 List 创建一个 Map

List<String> strings = Arrays.asList("a", "bb", "ccc");

Map<String, Integer> map = strings.stream()
    .collect(Collectors.toMap(Function.identity(), String::length));

我想保留与List中相同的迭代顺序。如何使用Collectors.toMap()方法创建LinkedHashMap


1
请检查我的答案,它只是使用自定义的“Supplier”、“Accumulator”和“Combiner”来为您的“stream”的“collect”方法编写的4行代码 :) - hzitoun
1
问题已经得到回答,我只想描述一下找到答案的路径。(1)如果您想在地图中进行排序,必须使用LinkedHashMap (2) Collectors.toMap() 有许多实现,其中一个要求使用Map。因此,在期望Map的地方使用LinkedHashMap。 - Satyendra Kumar
6个回答

154

Collectors.toMap() 的 2 参数版本使用了 HashMap:

public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(
    Function<? super T, ? extends K> keyMapper, 
    Function<? super T, ? extends U> valueMapper) 
{
    return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}

为了使用四个参数版本,你可以替换掉原先的代码:
Collectors.toMap(Function.identity(), String::length)

使用:

Collectors.toMap(
    Function.identity(), 
    String::length, 
    (u, v) -> {
        throw new IllegalStateException(String.format("Duplicate key %s", u));
    }, 
    LinkedHashMap::new
)

或者为了让代码更加简洁,编写一个新的toLinkedMap()方法并使用它:
public class MoreCollectors
{
    public static <T, K, U> Collector<T, ?, Map<K,U>> toLinkedMap(
        Function<? super T, ? extends K> keyMapper,
        Function<? super T, ? extends U> valueMapper)
    {
        return Collectors.toMap(
            keyMapper,
            valueMapper, 
            (u, v) -> {
                throw new IllegalStateException(String.format("Duplicate key %s", u));
            },
            LinkedHashMap::new
        );
    }
}

3
为什么这么复杂?你可以轻松做到,查看我下面的答案。 (翻译已按要求进行,如需任何更改请告知) - hzitoun
3
在具有4个参数版本的Collectors.toMap()中,mergeFunction不知道正在合并哪个键,uv都是值。因此,关于IllegalStateException的消息并不完全正确。 - MoonFruit
1
@hzitoun因为如果你只是使用Map::put,你最终可能会得到不同的值,但是对于相同的键。通过使用Map::put,你间接地选择了第一个值是不正确的。这是最终用户想要的吗?你确定吗?如果是的话,那么当然可以使用Map::put。否则,你不确定并且无法决定:让用户知道他们的流映射到两个具有不同值的相同键。我本来想在你自己的答案上发表评论,但它目前被锁定了。 - Olivier Grégoire

95

制作您自己的SupplierAccumulatorCombiner

List<String> myList = Arrays.asList("a", "bb", "ccc"); 
// or since java 9 List.of("a", "bb", "ccc");
    
LinkedHashMap<String, Integer> mapInOrder = myList
                        .stream()
                        .collect(
                          LinkedHashMap::new,                           // Supplier LinkedHashMap to keep the order
                          (map, item) -> map.put(item, item.length()),  // Accumulator
                          Map::putAll);                                 // Combiner

System.out.println(mapInOrder);  // prints {a=1, bb=2, ccc=3}

1
@Sushil 接受的答案允许重用逻辑。 - dzenisiy
你知道哪个更快吗?使用你的版本还是采纳的答案? - nimo23
@Nikolas,你能解释一下你所说的“副作用”是什么意思吗?我实际上有问题要决定选择哪一个:https://stackoverflow.com/questions/61479650/stream-map-with-collectors-tomap-vs-accumulator?noredirect=1#comment108755306_61479650 - nimo23
@NikolasCharalambidis,什么副作用?这是一个累加器。java.util.stream.Collectors#uniqKeysMapAccumulator也是做同样的事情。 - Vsevolod Golovanov

2
这个问题的正确解决方案是:
当前方式 ----> 2个参数版本 Map<Integer, String> mapping = list.stream().collect(Collectors.toMap(Entity::getId, Entity::getName)); 正确方式 ----> 使用4个参数版本的Collectors.toMap,告诉供应商提供一个新的LinkedHashMap: Map<Integer, String> mapping = list.stream().collect(Collectors.toMap(Entity::getId, Entity::getName, (u, v) -> u, LinkedHashMap::new)); 这样做会有所帮助。

热爱Java。在大多数其他编程语言中都是小菜一碟。 - garryp

1
自从Java 9开始,您可以收集一个与原始列表顺序相同的映射条目列表map entries
List<String> strings = Arrays.asList("a", "bb", "ccc");

List<Map.Entry<String, Integer>> entries = strings.stream()
        .map(e -> Map.entry(e, e.length()))
        .collect(Collectors.toList());

System.out.println(entries); // [a=1, bb=2, ccc=3]

或者您可以以同样的方式收集一个地图列表,其中只有一个条目:

List<String> strings = Arrays.asList("a", "bb", "ccc");

List<Map<String, Integer>> maps = strings.stream()
        .map(e -> Map.of(e, e.length()))
        .collect(Collectors.toList());

System.out.println(maps); // [{a=1}, {bb=2}, {ccc=3}]

1

通过某个字段映射对象数组的简单函数:

public static <T, E> Map<E, T> toLinkedHashMap(List<T> list, Function<T, E> someFunction) {
    return list.stream()
               .collect(Collectors.toMap(
                   someFunction, 
                   myObject -> myObject, 
                   (key1, key2) -> key1, 
                   LinkedHashMap::new)
               );
}


Map<String, MyObject> myObjectsByIdMap1 = toLinkedHashMap(
                listOfMyObjects, 
                MyObject::getSomeStringField()
);

Map<Integer, MyObject> myObjectsByIdMap2 = toLinkedHashMap(
                listOfMyObjects, 
                MyObject::getSomeIntegerField()
);

1
Kotlin中,toMap()是有序的。
fun <K, V> Iterable<Pair<K, V>>.toMap(): Map<K, V>

Returns a new map containing all key-value pairs from the given collection of pairs.

The returned map preserves the entry iteration order of the original collection. If any of two pairs would have the same key the last one gets added to the map.

这是它的实现:

public fun <K, V> Iterable<Pair<K, V>>.toMap(): Map<K, V> {
    if (this is Collection) {
        return when (size) {
            0 -> emptyMap()
            1 -> mapOf(if (this is List) this[0] else iterator().next())
            else -> toMap(LinkedHashMap<K, V>(mapCapacity(size)))
        }
    }
    return toMap(LinkedHashMap<K, V>()).optimizeReadOnlyMap()
}

使用方法很简单:
val strings = listOf("a", "bb", "ccc")
val map = strings.map { it to it.length }.toMap()
< p > map 的底层集合是一个 LinkedHashMap(按照插入顺序排序)。


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