Java8: HashMap <X, Y> para HashMap <X, Z> usando Stream / Map-Reduce / Collector

209

Eu sei como "transformar" um Java simples Listde Y-> Z, ou seja:

List<String> x;
List<Integer> y = x.stream()
        .map(s -> Integer.parseInt(s))
        .collect(Collectors.toList());

Agora eu gostaria de fazer basicamente o mesmo com um mapa, ou seja:

INPUT:
{
  "key1" -> "41",    // "41" and "42"
  "key2" -> "42      // are Strings
}

OUTPUT:
{
  "key1" -> 41,      // 41 and 42
  "key2" -> 42       // are Integers
}

A solução não deve ser limitada a String-> Integer. Assim como no Listexemplo acima, eu gostaria de chamar qualquer método (ou construtor).

Benjamin M
fonte

Respostas:

372
Map<String, String> x;
Map<String, Integer> y =
    x.entrySet().stream()
        .collect(Collectors.toMap(
            e -> e.getKey(),
            e -> Integer.parseInt(e.getValue())
        ));

Não é tão bom quanto o código da lista. Você não pode construir novos Map.Entrys em uma map()chamada, para que o trabalho seja misturado à collect()chamada.

John Kugelman
fonte
59
Você pode substituir e -> e.getKey()por Map.Entry::getKey. Mas isso é uma questão de gosto / estilo de programação.
Holger
5
Na verdade, é uma questão de desempenho, o seu sugerindo sendo ligeiramente superior ao 'estilo' lambda
Jon Burgin
36

Aqui estão algumas variações da resposta de Sotirios Delimanolis , que foi muito boa para começar (+1). Considere o seguinte:

static <X, Y, Z> Map<X, Z> transform(Map<? extends X, ? extends Y> input,
                                     Function<Y, Z> function) {
    return input.keySet().stream()
        .collect(Collectors.toMap(Function.identity(),
                                  key -> function.apply(input.get(key))));
}

Alguns pontos aqui. Primeiro é o uso de curingas nos genéricos; isso torna a função um pouco mais flexível. Um curinga seria necessário se, por exemplo, você desejasse que o mapa de saída tivesse uma chave que seja uma superclasse da chave do mapa de entrada:

Map<String, String> input = new HashMap<String, String>();
input.put("string1", "42");
input.put("string2", "41");
Map<CharSequence, Integer> output = transform(input, Integer::parseInt);

(Também há um exemplo para os valores do mapa, mas é realmente artificial, e admito que ter o curinga limitado para Y ajuda apenas em casos extremos.)

Um segundo ponto é que, em vez de executar o fluxo sobre o mapa de entrada entrySet, eu o executei no keySet. Isso torna o código um pouco mais limpo, eu acho, ao custo de ter que buscar valores fora do mapa, e não na entrada do mapa. Aliás, eu inicialmente tive key -> keycomo o primeiro argumento toMap()e isso falhou com um erro de inferência de tipo por algum motivo. Mudando para (X key) -> keyfuncionou, assim comoFunction.identity() .

Ainda outra variação é a seguinte:

static <X, Y, Z> Map<X, Z> transform1(Map<? extends X, ? extends Y> input,
                                      Function<Y, Z> function) {
    Map<X, Z> result = new HashMap<>();
    input.forEach((k, v) -> result.put(k, function.apply(v)));
    return result;
}

Isso usa em Map.forEach()vez de fluxos. Isso é ainda mais simples, eu acho, porque dispensa os colecionadores, que são um pouco desajeitados para usar nos mapas. O motivo é que Map.forEach()fornece a chave e o valor como parâmetros separados, enquanto o fluxo possui apenas um valor - e você deve escolher se deseja usar a chave ou a entrada do mapa como esse valor. No lado negativo, falta a bondade rica e fluida das outras abordagens. :-)

Stuart Marks
fonte
11
Function.identity()pode parecer legal, mas como a primeira solução exige uma pesquisa de mapa / hash para todas as entradas, enquanto todas as outras soluções não, eu não recomendaria.
Holger
13

Uma solução genérica como essa

public static <X, Y, Z> Map<X, Z> transform(Map<X, Y> input,
        Function<Y, Z> function) {
    return input
            .entrySet()
            .stream()
            .collect(
                    Collectors.toMap((entry) -> entry.getKey(),
                            (entry) -> function.apply(entry.getValue())));
}

Exemplo

Map<String, String> input = new HashMap<String, String>();
input.put("string1", "42");
input.put("string2", "41");
Map<String, Integer> output = transform(input,
            (val) -> Integer.parseInt(val));
Sotirios Delimanolis
fonte
Boa abordagem usando genéricos. Eu acho que isso pode ser melhorado um pouco - veja minha resposta.
Stuart Marks
13

A função do Guava Maps.transformValuesé o que você está procurando e funciona bem com expressões lambda:

Maps.transformValues(originalMap, val -> ...)
Alex Krauss
fonte
Eu gosto dessa abordagem, mas tome cuidado para não passar uma java.util.Function. Como ele espera com.google.common.base.Function, o Eclipse fornece um erro inútil - ele diz que Function não é aplicável a Function, o que pode ser confuso: "O método transformValues ​​(Map <K, V1>, Function <? Super V1 , V2>) no tipo Mapas não é aplicável aos argumentos (Mapa <Foo, Bar>, Função <Bar, Baz>) "
mskfisher
Se você deve passar a java.util.Function, você tem duas opções. 1. Evite o problema usando um lambda para permitir que a inferência de tipo Java descubra. 2. Use uma referência de método como javaFunction :: apply para produzir um novo lambda que a inferência de tipo possa descobrir.
3128 Joe
5

Minha biblioteca StreamEx , que aprimora a API de fluxo padrão, fornece uma EntryStreamclasse que melhor se adapta à transformação de mapas:

Map<String, Integer> output = EntryStream.of(input).mapValues(Integer::valueOf).toMap();
Tagir Valeev
fonte
4

Uma alternativa que sempre existe para fins de aprendizado é criar seu coletor personalizado por meio de Collector.of (), embora o coletor toMap () JDK aqui seja sucinto (+1 aqui ).

Map<String,Integer> newMap = givenMap.
                entrySet().
                stream().collect(Collector.of
               ( ()-> new HashMap<String,Integer>(),
                       (mutableMap,entryItem)-> mutableMap.put(entryItem.getKey(),Integer.parseInt(entryItem.getValue())),
                       (map1,map2)->{ map1.putAll(map2); return map1;}
               ));
Ira
fonte
Comecei com esse coletor personalizado como base e queria acrescentar que, pelo menos ao usar parallelStream () em vez de stream (), o binaryOperator deve ser reescrito para algo mais semelhante map2.entrySet().forEach(entry -> { if (map1.containsKey(entry.getKey())) { map1.get(entry.getKey()).merge(entry.getValue()); } else { map1.put(entry.getKey(),entry.getValue()); } }); return map1ou valores serão perdidos ao reduzir.
user691154
3

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 . Podemos apenas transformar o mapa diretamente usando o operador 'map' (por padrão, o mapa atua sobre os valores no mapa).

   MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                                .map(Integer::parseInt);

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

  MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                               .bimap(this::newKey,Integer::parseInt);
John McClean
fonte