Tengo un mapa dentro de un mapa que se ve así:
Map<String, Map<Integer, BigDecimal>> mapInMap; //--> with values like: /* "a": (1: BigDecimal.ONE), "b": (2: BigDecimal.TEN), "c": (1: BigDecimal.ZERO) */Y me gustaría combinar los mapas internos esperando el siguiente resultado:
Map<Integer, BigDecimal> innerMapCombined; //--> with values: /* 1: BigDecimal.ZERO, 2: BigDecimal.TEN */Esta es mi solución predefiniendo el mapa combinado y usando forEach:
Map<Integer, BigDecimal> combined = new HashMap<>(); mapInMap.forEach((str, innerMap) -> { innerMap.forEach(combined::putIfAbsent); }); Pero esto ignorará (1: BigDecimal.ZERO) .
¿Podría proporcionar una solución de 1 línea con java 8 stream?
El problema con su problema es que tan pronto como inicialice sus mapas y agregue las claves duplicadas en los mapas internos, volverá a escribir esas claves, ya que esos mapas no aceptan claves duplicadas. Por lo tanto, primero debe cambiar esto:
Map<String, Map<Integer, BigDecimal>> mapInMap; a un Map que permite claves duplicadas , por ejemplo Multimap de Google Guava:
Map<String, Multimap<Integer, BigDecimal>> mapInMap = new HashMap<>();donde los mapas internos se crean así:
Multimap<Integer, BigDecimal> x1 = ArrayListMultimap.create(); x1.put(1, BigDecimal.ONE); mapInMap.put("a", x1);Solo ahora puede intentar resolver su problema utilizando Java 8 Streams API. Por ejemplo:
Map<Integer, BigDecimal> map = multiMap.values() .stream() .flatMap(map -> map.entries().stream()) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v2)); Los conflictos de claves duplicadas se resuelven utilizando el parámetro mergeFunction del método toMap . Expresamos explícitamente tomar el segundo valor (v1, v2) -> v2 en caso de duplicados.
Problema:
Para abordar por qué su solución actual no funciona es porque el método Map#putIfAbsent solo agrega y no reemplaza un valor en un mapa si ya está presente.
Solución usando for-each:
Map#put es un camino a seguir, sin embargo, su limitación es que no puede decidir si desea mantener siempre el primer valor para dicha clave, calcular uno nuevo o usar siempre el último valor. Por tal razón, recomiendo usar una combinación de Map#computeIfPresent y Map#putIfAbsent o, mejor, un método que haga todo eso a la vez, que es Map#merge(K, V, BiFunction) con una función de reasignación de BiFunction remappingFunction :
remappingFunction - la función para volver a calcular un valor si está presente
Map<Integer, BigDecimal> resultMap = new HashMap<>(); for (Map<Integer, BigDecimal> map: mapInMap.values()) { for (Map.Entry<Integer, BigDecimal> entry: map.entrySet()) { resultMap.merge(entry.getKey(), entry.getValue(), (l, r) -> r); } }Solución usando Stream API:
Para reescribirlo en la solución Stream-alike, el enfoque sería idéntico. La única diferencia es la sintaxis declarativa de Stream API, sin embargo, la idea es la misma.
Simplemente haga un mapeo plano de la estructura y recopile en un mapa con un Collector.toMap(Function, Function, BinaryOperator usando BinaryOperator mergeFunction para fusionar claves duplicadas.
mergeFunction: una función de combinación, utilizada para resolver colisiones entre valores asociados con la misma clave, como se proporciona a Map.merge(Object, Object, BiFunction)
Map<Integer, BigDecimal> resultMap = mapInMap.values().stream() .flatMap(entries -> entries.entrySet().stream()) .collect(Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue, (l, r) -> r));Nota: @dreamcrash también merece crédito por su buena respuesta de Stream API en términos de velocidad.
Resultado:
{1=1, 2=10} es el resultado cuando extrae dicho mapa (tenga en cuenta que BigDecimal se imprime como un número). Esta salida coincide con su salida esperada.
1=BigDecimal.ZERO 2=BigDecimal.TEN Observe las similitudes entre Map#merge(K, V, BiFunction) y Collector.toMap(Function, Function, BinaryOperator que usan un enfoque muy similar para el mismo resultado.