How do I keep the iteration order of a List when using Collections.toMap() on a stream?
JavaCollectionsJava 8Java StreamJava Problem Overview
I am creating a Map
from a List
as follows:
List<String> strings = Arrays.asList("a", "bb", "ccc");
Map<String, Integer> map = strings.stream()
.collect(Collectors.toMap(Function.identity(), String::length));
I want to keep the same iteration order as was in the List
. How can I create a LinkedHashMap
using the Collectors.toMap()
methods?
Java Solutions
Solution 1 - Java
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
);
}
}
Solution 2 - Java
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
(map, item) -> map.put(item, item.length()), // Accumulator
Map::putAll); // Combiner
System.out.println(mapInOrder); // prints {a=1, bb=2, ccc=3}
Solution 3 - Java
In Kotlin, toMap()
is order-preserving.
> fun
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).
Solution 4 - Java
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.
Solution 5 - Java
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()
);
Solution 6 - Java
Since Java 9 you can collect a list of [map entries][1] 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][2] 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}]
[1]: https://docs.oracle.com/javase/9/docs/api/java/util/Map.html#entry-K-V- "Map.entry(K,V)" [2]: https://docs.oracle.com/javase/9/docs/api/java/util/Map.html#of-K-V- "Map.of(K,V)"