No Java 8, como transformar um mapa <K, V> em outro mapa <K, V> usando um lambda?

140

Acabei de começar a olhar para o Java 8 e experimentar lambdas. Pensei em tentar reescrever uma coisa muito simples que escrevi recentemente. Preciso transformar um mapa de seqüência de caracteres em coluna em outro mapa de seqüência de caracteres em coluna onde a coluna no novo mapa é uma cópia defensiva da coluna no primeiro mapa. A coluna possui um construtor de cópias. O mais próximo que eu tenho até agora é:

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

mas tenho certeza de que deve haver uma maneira melhor de fazer isso e ficaria grato por alguns conselhos.

annesadleir
fonte

Respostas:

222

Você pode usar um coletor :

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
fonte
6
Eu acho que o importante (e ligeira contra-intuitivo) para nota no exemplo acima é que a transformação ocorre no coletor, em vez do que em uma operação corrente do mapa ()
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}

Você pode usar o forEachmétodo para fazer o que quiser.

O que você está fazendo lá é:

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

Que podemos simplificar em um lambda:

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

E, como estamos chamando um método existente, podemos usar uma referência de método , o que nos fornece:

map.forEach(map2::put);
Arrem
fonte
8
Eu gosto de como você explicou exatamente o que está acontecendo nos bastidores aqui, isso torna muito mais claro. Mas acho que isso me dá uma cópia direta do meu mapa original, não um novo mapa onde os valores foram transformados em cópias defensivas. Estou entendendo corretamente que o motivo pelo qual podemos apenas usar (map2 :: put) é que os mesmos argumentos estão entrando no lambda, por exemplo (s, número inteiro), como no método put? Então, para fazer uma cópia defensiva, seria necessário originalColumnMap.forEach((string, column) -> newColumnMap.put(string, new Column(column)));ou eu poderia reduzi-lo?
annesadleir
Sim você é. Se você está apenas passando os mesmos argumentos, pode usar uma referência de método, no entanto, neste caso, como você tem o new Column(column)paremômetro, precisará seguir com isso.
Arrem 30/03
Gosto mais dessa resposta porque ela funciona se os dois mapas já foram fornecidos a você, como na renovação da sessão.
18118 David Stanley #
Não tenho certeza de entender o objetivo dessa resposta. Ele está reescrevendo o construtor da maioria das implementações do Mapa a partir de um Mapa existente - como este .
Kineolyan 24/04
13

O caminho sem reinserir todas as entradas no novo mapa deve ser o mais rápido possível, porque também HashMap.clonerealiza rehash internamente.

Map<String, Column> newColumnMap = originalColumnMap.clone();
newColumnMap.replaceAll((s, c) -> new Column(c));
leventov
fonte
Essa é uma maneira muito legível e concisa. Parece que, assim como HashMap.clone (), existe Map<String,Column> newColumnMap = new HashMap<>(originalColumnMap);. Não sei se é diferente debaixo das cobertas.
annesadleir
2
Essa abordagem tem a vantagem de manter o Mapcomportamento da implementação, ou seja, se Mapé um EnumMapou a SortedMap, o resultado Maptambém será. No caso de um SortedMapcom um especial Comparator, pode fazer uma enorme diferença semântica. Oh bem, o mesmo se aplica a um IdentityHashMap
Holger
8

Mantenha-o simples e use o 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
fonte
neste caso: map.forEach (map2 :: put); é simples :)
ses
5

Se você usar o Guava (mínimo da v11) em seu projeto, poderá usar o Maps :: transformValues .

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

Nota: Os valores deste mapa são avaliados preguiçosamente. Se a transformação for cara, você pode copiar o resultado para um novo mapa, como sugerido nos documentos do 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
fonte
Nota: De acordo com os documentos, isso retorna uma visualização preguiçosa avaliada no originalColumnMap, para que a função Column :: new seja reavaliada sempre que a entrada for acessada (o que pode não ser desejável quando a função de mapeamento é cara)
Erric
Corrigir. Se a transformação for cara, você provavelmente está bem com a sobrecarga de copiar isso para um novo mapa, como sugerido nos 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 11/11/19
É verdade, mas pode valer a pena adicionar como nota de rodapé na resposta para aqueles que tendem a pular a leitura dos documentos.
Erric
@ Erric Faz sentido, acabou de adicionar.
Andrea Bergonzo 20/11/19
3

Aqui está outra maneira que fornece acesso à chave e ao valor ao mesmo tempo, caso você precise fazer algum tipo de transformação.

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
fonte
1

Se você não se importa em usar bibliotecas de terceiros, minha lib cyclops- react possui extensões para todos os tipos de coleções do JDK , incluindo o Map . Você pode usar diretamente os métodos de mapa ou bimap para transformar seu mapa. Um MapX pode ser construído a partir de um mapa existente, por exemplo.

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

Se você também deseja alterar a chave, pode escrever

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

O bimap pode ser usado para transformar as chaves e os valores ao mesmo tempo.

À medida que o MapX estende o mapa, o mapa gerado também pode ser definido como

  Map<String, Column> y
John McClean
fonte