En Java 8, ¿cómo transformo un Mapa <K, V> en otro Mapa <K, V> usando una lambda?

140

Recién comencé a mirar Java 8 y a probar lambdas pensé que trataría de reescribir una cosa muy simple que escribí recientemente. Necesito convertir un Mapa de Cadena a Columna en otro Mapa de Cadena a Columna donde la Columna en el nuevo Mapa es una copia defensiva de la Columna en el primer Mapa. La columna tiene un constructor de copia. Lo más cercano que tengo hasta ahora es:

    Map<String, Column> newColumnMap= new HashMap<>();
    originalColumnMap.entrySet().stream().forEach(x -> newColumnMap.put(x.getKey(), new Column(x.getValue())));

pero estoy seguro de que debe haber una mejor manera de hacerlo y agradecería algunos consejos.

annesadleir
fuente

Respuestas:

222

Podrías usar un colector :

import java.util.*;
import java.util.stream.Collectors;

public class Defensive {

  public static void main(String[] args) {
    Map<String, Column> original = new HashMap<>();
    original.put("foo", new Column());
    original.put("bar", new Column());

    Map<String, Column> copy = original.entrySet()
        .stream()
        .collect(Collectors.toMap(Map.Entry::getKey,
                                  e -> new Column(e.getValue())));

    System.out.println(original);
    System.out.println(copy);
  }

  static class Column {
    public Column() {}
    public Column(Column c) {}
  }
}
McDowell
fuente
66
Creo que lo importante (y levemente contra-intuitivo) a tener en cuenta en lo anterior es que la transformación tiene lugar en el recopilador, en lugar de en una operación de flujo map ()
Brian Agnew
24
Map<String, Integer> map = new HashMap<>();
map.put("test1", 1);
map.put("test2", 2);

Map<String, Integer> map2 = new HashMap<>();
map.forEach(map2::put);

System.out.println("map: " + map);
System.out.println("map2: " + map2);
// Output:
// map:  {test2=2, test1=1}
// map2: {test2=2, test1=1}

Puedes usar el forEachmétodo para hacer lo que quieras.

Lo que estás haciendo allí es:

map.forEach(new BiConsumer<String, Integer>() {
    @Override
    public void accept(String s, Integer integer) {
        map2.put(s, integer);     
    }
});

Lo que podemos simplificar en una lambda:

map.forEach((s, integer) ->  map2.put(s, integer));

Y debido a que solo estamos llamando a un método existente, podemos usar una referencia de método , que nos da:

map.forEach(map2::put);
Arrem
fuente
8
Me gusta cómo has explicado exactamente lo que está sucediendo detrás de escena aquí, lo hace mucho más claro. Pero creo que eso solo me da una copia directa de mi mapa original, no un nuevo mapa donde los valores se han convertido en copias defensivas. ¿Te entiendo correctamente que la razón por la que podemos usar (map2 :: put) es que los mismos argumentos entran en el lambda, por ejemplo (s, entero) que en el método put? Entonces, para hacer una copia defensiva, ¿sería necesario originalColumnMap.forEach((string, column) -> newColumnMap.put(string, new Column(column)));o podría acortar eso?
annesadleir
Sí es usted. Si solo está pasando los mismos argumentos, puede usar una referencia de método, sin embargo, en este caso, ya que tiene el new Column(column)parámetro, tendrá que ir con eso.
Arrem
Me gusta más esta respuesta porque funciona si ambos mapas ya se le proporcionaron, como en la renovación de la sesión.
David Stanley
No estoy seguro de entender el punto de tal respuesta. Está reescribiendo el constructor de la mayoría de las implementaciones de mapas de un mapa existente, como este .
Kineolyan
13

La forma sin volver a insertar todas las entradas en el nuevo mapa debería ser la más rápida , ya que HashMap.cloneinternamente también realiza la repetición.

Map<String, Column> newColumnMap = originalColumnMap.clone();
newColumnMap.replaceAll((s, c) -> new Column(c));
leventov
fuente
Esa es una forma muy legible de hacerlo y bastante concisa. Parece tan bien como HashMap.clone () hay Map<String,Column> newColumnMap = new HashMap<>(originalColumnMap);. No sé si es diferente bajo las sábanas.
annesadleir
2
Este enfoque tiene la ventaja de mantener el Mapcomportamiento de la implementación, es decir, si Mapes una EnumMapo una SortedMap, el resultado Maptambién lo será. En el caso de un SortedMapcon un especial Comparator, puede hacer una gran diferencia semántica. Bueno, lo mismo se aplica a un IdentityHashMap...
Holger
8

Mantenlo simple y usa Java 8: -

 Map<String, AccountGroupMappingModel> mapAccountGroup=CustomerDAO.getAccountGroupMapping();
 Map<String, AccountGroupMappingModel> mapH2ToBydAccountGroups = 
              mapAccountGroup.entrySet().stream()
                         .collect(Collectors.toMap(e->e.getValue().getH2AccountGroup(),
                                                   e ->e.getValue())
                                  );
Anant Khurana
fuente
en este caso: map.forEach (map2 :: put); es simple :)
ses
5

Si usa Guava (v11 mínimo) en su proyecto, puede usar Maps :: transformValues .

Map<String, Column> newColumnMap = Maps.transformValues(
  originalColumnMap,
  Column::new // equivalent to: x -> new Column(x) 
)

Nota: Los valores de este mapa se evalúan perezosamente. Si la transformación es costosa, puede copiar el resultado en un nuevo mapa como se sugiere en los documentos de Guava.

To avoid lazy evaluation when the returned map doesn't need to be a view, copy the returned map into a new map of your choosing.
Andrea Bergonzo
fuente
Nota: De acuerdo con los documentos, esto devuelve una vista perezosa evaluada en el OriginalColumnMap, por lo que la función Column :: new se vuelve a evaluar cada vez que se accede a la entrada (lo que puede no ser deseable cuando la función de mapeo es costosa)
Erric
Correcto. Si la transformación es costosa, probablemente esté bien con la sobrecarga de copiar eso en un nuevo mapa como se sugiere en los documentos. To avoid lazy evaluation when the returned map doesn't need to be a view, copy the returned map into a new map of your choosing.
Andrea Bergonzo
Es cierto, pero podría valer la pena agregarlo como una nota al pie en la respuesta para aquellos que tienden a saltear la lectura de los documentos.
Erric
@Erric Tiene sentido, recién agregado.
Andrea Bergonzo
3

Aquí hay otra forma que le da acceso a la clave y al valor al mismo tiempo, en caso de que tenga que hacer algún tipo de transformación.

Map<String, Integer> pointsByName = new HashMap<>();
Map<String, Integer> maxPointsByName = new HashMap<>();

Map<String, Double> gradesByName = pointsByName.entrySet().stream()
        .map(entry -> new AbstractMap.SimpleImmutableEntry<>(
                entry.getKey(), ((double) entry.getValue() /
                        maxPointsByName.get(entry.getKey())) * 100d))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Lucas Ross
fuente
1

Si no le importa usar bibliotecas de terceros, mi libra cyclops-react tiene extensiones para todos los tipos de colección JDK , incluido Map . Puede usar directamente los métodos de mapa o bimap para transformar su mapa. Un MapX se puede construir a partir de un mapa existente, por ejemplo.

  MapX<String, Column> y = MapX.fromMap(orgColumnMap)
                               .map(c->new Column(c.getValue());

Si también desea cambiar la clave, puede escribir

  MapX<String, Column> y = MapX.fromMap(orgColumnMap)
                               .bimap(this::newKey,c->new Column(c.getValue());

bimap se puede usar para transformar las claves y los valores al mismo tiempo.

A medida que MapX extiende el Mapa, el mapa generado también se puede definir como

  Map<String, Column> y
John McClean
fuente