¿Cómo mantengo el orden de iteración de una lista cuando uso Collections.toMap () en una secuencia?

82

Estoy creando una Mapde la Listsiguiente manera:

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

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

Quiero mantener el mismo orden de iteración que en el List. ¿Cómo puedo crear un LinkedHashMapusando los Collectors.toMap()métodos?

Ashika Umanga Umagiliya
fuente
1
por favor revise mi respuesta a continuación. Son solo 4 líneas de código usando un personalizado Supplier, Accumulatory Combinerpara el collectmétodo de su stream:)
hzitoun
1
La pregunta ya ha sido respondida, solo quiero trazar la ruta para encontrar la respuesta a esta pregunta. (1) Si desea ordenar en el mapa, debe usar LinkedHashMap (2) Collectors.toMap () tiene muchas implementaciones, una de que pide un mapa. Entonces, use un LinkedHashMap donde espera Map.
Satyendra Kumar

Respuestas:

116

La versión de 2 parámetros deCollectors.toMap() utiliza un 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);
}

Para usar la versión de 4 parámetros , puede reemplazar:

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

con:

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

O para hacerlo un poco más limpio, escriba un nuevo toLinkedMap()método y utilícelo:

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
        );
    }
}
podar
fuente
2
¿Por qué tanta complejidad? puede hacer eso fácilmente, verifique mi respuesta a continuación
hzitoun
1
mergeFunctionque en la versión de 4 parámetros Collectors.toMap()no tiene ni idea de qué clave se está fusionando, ambos uy vson los valores. Entonces, el mensaje sobre IllegalStateException no es tan correcto.
MoonFruit
1
@hzitoun porque si simplemente usa Map::put, termina con (posiblemente) valores diferentes para la misma clave. Al usar Map::put, eligió indirectamente que el primer valor que encuentra es incorrecto. ¿Es eso lo que quiere el usuario final? ¿Estás seguro? Si es así, entonces seguro, use Map::put. De lo contrario, no está seguro y no puede decidir: informe al usuario que su flujo se asignó a dos claves idénticas con valores diferentes. Habría comentado tu propia respuesta, pero actualmente está bloqueada.
Olivier Grégoire
69

Proporcionar su propia Supplier, Accumulatory 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);  // {a=1, bb=2, ccc=3}
hzitoun
fuente
1
@Sushil La respuesta aceptada permite reutilizar la lógica
cilindro.y
1
La secuencia se basa en los efectos secundarios, como map.put(..)cuál está mal.
Nikolas Charalambidis
¿Sabes qué es más rápido? ¿Estás usando tu versión o la respuesta aceptada?
nimo23
@Nikolas, ¿puedes explicar qué quieres decir con efectos secundarios ? De hecho, tengo el problema de decidir cuál elegir: stackoverflow.com/questions/61479650/…
nimo23
0

En Kotlin, toMap()se preserva el orden.

fun <K, V> Iterable<Pair<K, V>>.toMap(): Map<K, V>

Devuelve un nuevo mapa que contiene todos los pares clave-valor de la colección de pares dada.

El mapa devuelto conserva el orden de iteración de entrada de la colección original. Si cualquiera de los dos pares tuviera la misma clave, el último se agrega al mapa.

Aquí está su implementación:

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

El uso es simplemente:

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

La colección subyacente de mapes a LinkedHashMap(que está ordenada por inserción).

Mateen Ulhaq
fuente
0

Función simple para mapear una matriz de objetos por algún campo:

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()
);
Dmitri Algazin
fuente