How do I keep the iteration order of a List when using Collections.toMap() on a stream?

JavaCollectionsJava 8Java Stream

Java 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 Iterable>.toMap(): Map > > 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).

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)"

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAshika Umanga UmagiliyaView Question on Stackoverflow
Solution 1 - JavaprungeView Answer on Stackoverflow
Solution 2 - JavahzitounView Answer on Stackoverflow
Solution 3 - JavaMateen UlhaqView Answer on Stackoverflow
Solution 4 - JavaCodingBeeView Answer on Stackoverflow
Solution 5 - JavaDmitri AlgazinView Answer on Stackoverflow
Solution 6 - Javauser15402945View Answer on Stackoverflow