2014-09-18 15 views
124

so come "trasformare" un semplice Java List da Y ->Z, vale a dire:Java8: HashMap <X, Y> per hashmap <X, Z> utilizzando Ruscello/Map-Reduce/Collector

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

Ora mi piacerebbe fare fondamentalmente la stessa cosa 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 deve essere limitata a String ->Integer. Proprio come nell'esempio List sopra, mi piacerebbe chiamare qualsiasi metodo (o costruttore).

risposta

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

Non è bello come il codice di elenco. Non è possibile creare nuovi Map.Entry s in una chiamata map() in modo che il lavoro venga mixato nella chiamata collect().

+38

È possibile sostituire 'e -> e.getKey()' 'con Map.Entry :: getKey'. Ma questa è una questione di gusto/stile di programmazione. – Holger

+3

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

10

Una soluzione generica questo modo

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

Un buon approccio con i generici. Penso che possa essere migliorato un po 'però - vedi la mia risposta. –

23

Ecco alcune varianti Sotirios Delimanolis' answer, che era abbastanza buono 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 jolly sarebbe necessario se, ad esempio, si voleva la mappa di output di avere una chiave che è una superclasse di 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 artificiosa e ammetto che avere il carattere jolly limitato per Y aiuta solo nei casi limite.)

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

Ancora un'altra variante è 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é dispensa con i collezionisti, che sono piuttosto 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 utilizzare la chiave o la voce della mappa come tale valore. Sul lato negativo, questo manca della ricca, fluida bontà degli altri approcci. :-)

+0

Non conoscevo 'Funzione # identity()'. Roba forte. –

+11

'Function.identity()' potrebbe sembrare interessante, ma poiché la prima soluzione richiede una ricerca map/hash per ogni voce, mentre tutte le altre soluzioni no, non la consiglierei. – Holger

4

mio StreamEx biblioteca che migliora flusso di API standard fornisce una classe EntryStream che si adatta meglio per le mappe di trasformazione:

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

Se non ti dispiace utilizzando le librerie 3rd party, il mio cyclops-react lib ha estensioni per tutti i tipi, tra JDK CollectionMap. Possiamo semplicemente trasformare la mappa direttamente usando l'operatore 'mappa' (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 utilizzato per trasformare le chiavi ei valori allo stesso tempo

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

Un'alternativa che esiste da sempre per l'apprendimento scopo è quello di costruire la vostra collezione personalizzata attraverso Collector.of() anche se toMap() Il raccoglitore JDK qui è succinto (+1 here).

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

Ho iniziato con questo collector personalizzato come base e volevo aggiungere che, almeno quando si utilizza parallelStream() anziché stream(), binaryOperator dovrebbe essere riscritto in qualcosa di simile a 'map2.entrySet(). ForEach (entry - > {if (map1.containsKey (entry.getKey())) {map1.get (entry.getKey()). unione (entry.getValue());} else {map1.put (entry.getKey(), voce .getValue());}}); restituire map1' o i valori andranno persi durante la riduzione. – user691154

3

funzione di Guava Maps.transformValues è quello che stai cercando, e funziona bene con le espressioni lambda:

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

Mi piace questo approccio, ma fai attenzione a non passare a java.util.Function. Dal momento che si aspetta com.google.common.base.Function, Eclipse dà un errore inutile - dice che Function non è applicabile a Function, che può confondere: "Il metodo transformValues ​​(Map , Function ) nel tipo Maps non è applicabile per gli argomenti (Mappa , Funzione ) " – mskfisher