2014-09-18 8 views
124

Ich weiß, wie man "transformieren", um eine einfache Java-List von Y ->Z, das heißt:Java8: HashMap <X, Y> Hashmap <X, Z> mit Strom/Map-Reduce/Collector

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

Nun möchte Ich mag an im Grunde das gleiche mit einer Karte, das heißt: ->Integer

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

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

die Lösung sollte nicht auf String begrenzt werden. Genau wie im oben genannten Beispiel möchte ich jede Methode (oder Konstruktor) aufrufen.

Antwort

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

Es ist nicht ganz so schön wie der Listencode. Sie können keine neuen Map.Entry s in einem map() Anruf erstellen, so dass die Arbeit in den collect() Aufruf gemischt wird.

+38

Sie können ersetzen 'e -> e.getKey()' 'mit Map.Entry :: getKey'. Aber das ist eine Frage des Geschmacks-/Programmierstils. – Holger

+3

Eigentlich ist es eine Frage der Leistung, Ihr Vorschlag ist dem Lambda-Stil etwas überlegen. –

10

Eine generische Lösung wie so

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

Beispiel

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

Netter Ansatz mit Generika. Ich denke, dass es ein bisschen verbessert werden kann - siehe meine Antwort. –

23

Hier sind einige Variationen auf Sotirios Delimanolis' answer, das war ziemlich gut mit (+1) zu beginnen. Beachten Sie Folgendes:

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

Ein paar Punkte hier. Erstens ist die Verwendung von Platzhaltern in den Generika; Dies macht die Funktion etwas flexibler. Ein Platzhalter wäre notwendig, wenn zum Beispiel, können Sie die Ausgabe der Karte wollte einen Schlüssel haben, ist eine übergeordnete Klasse der Eingangskarte Schlüssel:

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

(Es ist auch ein Beispiel für die Karte Werte, aber es ist wirklich gekünstelt , und ich gebe zu, dass die begrenzte Wildcard für Y nur in Rand Fällen hilft.)

Ein zweiter Punkt ist, dass statt den Strom über die Eingabe Map entrySet lief ich es über die keySet. Das macht den Code ein wenig sauberer, auf Kosten von Werten aus der Map statt aus dem Map-Eintrag. Übrigens hatte ich zunächst key -> key als erstes Argument zu toMap() und dies fehlgeschlagen mit einem Typ Inferenz Fehler aus irgendeinem Grund. Ändern Sie es zu (X key) -> key gearbeitet, wie auch Function.identity().

Noch eine andere Variante wie folgt:

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

Dies verwendet Map.forEach() statt Streams. Das ist noch einfacher, denke ich, weil es auf die Sammler verzichtet, die mit Karten etwas unbeholfen sind. Der Grund dafür ist, dass Map.forEach() den Schlüssel und den Wert als separate Parameter angibt, während der Stream nur einen Wert hat - und Sie müssen wählen, ob Sie den Schlüssel oder den Map-Eintrag als diesen Wert verwenden möchten. Auf der Minus-Seite mangelt es der reichen, stromreichen Güte der anderen Ansätze. :-)

+0

Ich wusste nicht über 'Function # identity()'. Cooles Zeug. –

+11

'Function.identity()' könnte cool aussehen, aber da die erste Lösung eine Map/Hash-Suche für jeden Eintrag erfordert, während alle anderen Lösung nicht, würde ich es nicht empfehlen. – Holger

4

Meine StreamEx Bibliothek, die Standard-Stream API verbessert liefert eine EntryStream Klasse, die für die Transformation von Karten besser passt:

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

Wenn Sie nicht 3rd-Party-Bibliotheken nichts dagegen, meine cyclops-react lib hat Erweiterungen für alle JDK Collection-Typen, einschließlich Map. Wir können die Map einfach mit dem Operator 'map' transformieren (standardmäßig wirkt die Karte auf die Werte in der Karte).

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

bimap kann verwendet werden, um die Schlüssel und Werte in der gleichen Zeit zu transformieren

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

Eine Alternative, die für das Lernen Zweck immer existiert, ist Ihren benutzerdefinierten Collector durch Collector.of zu bauen(), obwohl toMap() JDK-Kollektor hier ist prägnant (+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

Ich begann mit diesem benutzerdefinierten Kollektor als Basis und wollte das hinzufügen, zumindest wenn ich parallelStream() anstelle von stream() benutze, sollte der binaryOperator in etwas neu geschrieben werden, das "map2.entrySet() ähnlich ist. ForEach (entry - > {if (map1.containsKey (entry.getKey())) {map1.get (entry.getKey()). merge (eintrag.getValue());} else {map1.put (entry.getKey(), entry .Wert erhalten()); } }); return map1' oder Werte werden beim Reduzieren verloren gehen. – user691154

3

Guava die Funktion Maps.transformValues ist das, was Sie suchen, und es funktioniert gut mit Lambda-Ausdrücke:

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

Ich mag diesen Ansatz, aber seien Sie vorsichtig, es nicht eine java.util.Function zu übergeben. Da es com.google.common.base.Function erwartet, gibt Eclipse einen nicht hilfreichen Fehler - es sagt Funktion ist nicht anwendbar für Funktion, die verwirrend sein kann: "Die Methode transformValues ​​(Map , Funktion ) im Typ Karten ist nicht anwendbar für die Argumente (Karte , Funktion ) " – mskfisher