在流上使用 Collections.toMap()时,如何保持 List 的迭代顺序?

我正在从 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

60422 次浏览

The 2-parameter version of Collectors.toMap() uses a 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);
}

To use the 4-parameter version, you can replace:

Collectors.toMap(Function.identity(), String::length)

with:

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

Or to make it a bit cleaner, write a new toLinkedMap() method and use that:

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
);
}
}

Make your own Supplier, Accumulator and Combiner:

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}

In Kotlin, toMap() is order-preserving.

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.

Here's its implementation:

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()
}

The usage is simply:

val strings = listOf("a", "bb", "ccc")
val map = strings.map { it to it.length }.toMap()

The underlying collection for map is a LinkedHashMap (which is insertion-ordered).

Simple function to map array of objects by some field:

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()
);

Since Java 9 you can collect a list of map entries with the same order as in the original list:

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]

Or you can collect a list of maps with a single entry in the same way:

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}]

The right solution for this problem is

Current ----> 2 parameter version

Map<Integer, String> mapping = list.stream().collect(Collectors.toMap(Entity::getId, Entity::getName));

Right ----> Use 4-parameter version of the Collectors.toMap to tell supplier to supply a new LinkedHashMap:

Map<Integer, String> mapping = list.stream().collect(Collectors.toMap(Entity::getId, Entity::getName, (u, v) -> u, LinkedHashMap::new));

This will help.