Java8: HashMap <X, Y> su HashMap <X, Z> utilizzando Stream / Map-Reduce / Collector


210

So come "trasformare" un semplice Java Listda Y-> Z, ovvero:

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

Ora vorrei fare sostanzialmente lo stesso con una mappa, vale a dire:

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

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

La soluzione non dovrebbe essere limitata a String-> Integer. Proprio come Listnell'esempio sopra, vorrei chiamare qualsiasi metodo (o costruttore).

Risposte:


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

Non è così bello come il codice dell'elenco. Non è possibile costruire nuovi messaggi Map.Entryin una map()chiamata, quindi il lavoro viene mischiato nella collect()chiamata.


59
È possibile sostituire e -> e.getKey()con Map.Entry::getKey. Ma è una questione di gusti / stile di programmazione.
Holger,

5
In realtà è una questione di prestazioni, il tuo suggerimento è leggermente superiore allo "stile" lambda
Jon Burgin

36

Ecco alcune variazioni sulla risposta di Sotirios Delimanolis , che era abbastanza buona per cominciare (+1). Considera quanto segue:

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

Un paio di punti qui. Il primo è l'uso di caratteri jolly nei generici; questo rende la funzione un po 'più flessibile. Un carattere jolly sarebbe necessario se, ad esempio, si desidera che la mappa di output abbia una chiave che è una superclasse della chiave della mappa di input:

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

(C'è anche un esempio per i valori della mappa, ma è davvero inventato, e ammetto che avere il jolly limitato per Y aiuta solo nei casi limite.)

Un secondo punto è che invece di eseguire lo stream sulla mappa di input entrySet, l'ho eseguito su keySet. Questo rende il codice un po 'più pulito, credo, a costo di dover recuperare valori dalla mappa anziché dalla voce della mappa. Per inciso, inizialmente ho avuto key -> keycome primo argomento toMap()e questo non è riuscito con un errore di inferenza del tipo per qualche motivo. Cambiandolo in (X key) -> keyfunzionato, come ha fatto Function.identity().

Ancora un'altra variazione è la seguente:

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;
}

Questo utilizza Map.forEach()invece di flussi. Questo è ancora più semplice, penso, perché fa a meno dei collezionisti, che sono un po 'goffi da usare con le mappe. Il motivo è che Map.forEach()fornisce la chiave e il valore come parametri separati, mentre lo stream ha un solo valore - e devi scegliere se usare la chiave o la voce della mappa come quel valore. Sul lato negativo, questo manca della bontà ricca e fluida degli altri approcci. :-)


11
Function.identity()potrebbe sembrare bello, ma poiché la prima soluzione richiede una ricerca di mappe / hash per ogni voce, mentre tutte le altre soluzioni non lo fanno, non lo consiglierei.
Holger,

13

Una soluzione generica come questa

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

Esempio

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));

Bel approccio usando i generici. Penso che possa essere migliorato un po 'però - vedi la mia risposta.
Stuart Marks,

13

La funzione di Guava Maps.transformValuesè ciò che stai cercando e funziona bene con le espressioni lambda:

Maps.transformValues(originalMap, val -> ...)

Mi piace questo approccio, ma fai attenzione a non passarlo a java.util.Function. Poiché si aspetta com.google.common.base.Function, Eclipse fornisce un errore inutile: dice che la funzione non è applicabile per la funzione, il che può creare confusione: "Il metodo transformValues ​​(Mappa <K, V1>, Funzione <? Super V1 , V2>) nel tipo Mappe non applicabile per gli argomenti (Mappa <Foo, Bar>, Funzione <Bar, Baz>) "
mskfisher

Se devi passare a java.util.Function, hai due opzioni. 1. Evita il problema usando un lambda per far capire l'inferenza del tipo Java. 2. Utilizzare un riferimento di metodo come javaFunction :: apply per produrre una nuova lambda che l'inferenza del tipo possa capire.
Joe,


5

La mia libreria StreamEx che migliora l'API stream standard fornisce una EntryStreamclasse che si adatta meglio alla trasformazione delle mappe:

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

4

Un'alternativa che esiste sempre a scopo di apprendimento è quella di costruire il tuo raccoglitore personalizzato tramite Collector.of () sebbene toMap () il raccoglitore JDK qui sia succinto (+1 qui ).

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

Ho iniziato con questo raccoglitore personalizzato come base e volevo aggiungere che, almeno quando si utilizza parallelStream () anziché stream (), BinaryOperator dovrebbe essere riscritto in qualcosa di più simile map2.entrySet().forEach(entry -> { if (map1.containsKey(entry.getKey())) { map1.get(entry.getKey()).merge(entry.getValue()); } else { map1.put(entry.getKey(),entry.getValue()); } }); return map1o i valori andranno persi durante la riduzione.
user691154

3

Se non ti dispiace usare librerie di terze parti, la mia libreria cyclops- reazioni ha estensioni per tutti i tipi di raccolte JDK , inclusa Map . Possiamo semplicemente trasformare la mappa direttamente usando l'operatore 'map' (per impostazione predefinita la mappa agisce sui valori nella mappa).

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

bimap può essere usato per trasformare chiavi e valori contemporaneamente

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.