Ich weiß, wie man eine einfache Java List
von Y
-> Z
, d.
List<String> x;
List<Integer> y = x.stream()
.map(s -> Integer.parseInt(s))
.collect(Collectors.toList());
Nun möchte ich im Grunde dasselbe mit einer Map machen, d. H .:
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
-> Integer
beschränkt sein. Genau wie im obigen Beispiel List
möchte ich jede Methode (oder Konstruktor) aufrufen.
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()
-Aufruf erstellen, sodass die Arbeit mit dem collect()
-Aufruf gemischt wird.
Hier sind einige Variationen von Sotirios Delimanolis 'Antwort , die anfangs ziemlich gut war (+1). Folgendes berücksichtigen:
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 beispielsweise erforderlich, wenn Sie möchten, dass die Ausgabezuordnung einen Schlüssel hat, der eine Oberklasse des Schlüssels der Eingabezuordnung ist:
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 gibt auch ein Beispiel für die Werte der Karte, aber das ist wirklich sehr schön, und ich gebe zu, dass die Platzhalter für Y nur in Edge-Fällen hilfreich sind.)
Ein zweiter Punkt ist, dass ich den Stream nicht über die entrySet
der Eingabemaske liefe, sondern über die keySet
. Dies macht den Code ein wenig sauberer, denke ich, mit dem Preis, Werte aus der Karte anstatt aus dem Karteneintrag holen zu müssen. Übrigens hatte ich anfangs key -> key
als erstes Argument für toMap()
und dies schlug aus irgendeinem Grund mit einem Typinferenzfehler fehl. Das Ändern in (X key) -> key
hat ebenso funktioniert wie Function.identity()
.
Eine weitere Variante ist 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()
anstelle von Streams. Dies ist noch einfacher, denke ich, weil es auf die Sammler verzichtet, die mit Karten etwas unhandlich zu verwenden sind. Der Grund 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 entscheiden, ob Sie den Schlüssel oder den Map-Eintrag als diesen Wert verwenden möchten. Auf der Minus-Seite fehlt dies an der reichhaltigen, strengen Güte der anderen Ansätze. :-)
Eine generische Lösung 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));
Muss es absolut 100% funktionell und fließend sein? Wenn nicht, wie wäre es damit, das so kurz ist, wie es nur geht:
Map<String, Integer> output = new HashMap<>();
input.forEach((k, v) -> output.put(k, Integer.valueOf(v));
( wenn Sie mit der Schande und Schuld der Kombination von Streams mit Nebenwirkungen leben können )
Guavas Funktion Maps.transformValues
ist das, was Sie suchen, und es funktioniert gut mit Lambda-Ausdrücken:
Maps.transformValues(originalMap, val -> ...)
Meine StreamEx - Bibliothek, die die Standard-Stream-API erweitert, bietet eine EntryStream
-Klasse, die besser zum Transformieren von Karten geeignet ist:
Map<String, Integer> output = EntryStream.of(input).mapValues(Integer::valueOf).toMap();
Eine Alternative, die immer zu Lernzwecken vorhanden ist, besteht darin, Ihren benutzerdefinierten Collector durch Collector.of () zu erstellen, obwohl toMap () JDK-Collector hier knapp ist (+1 hier ).
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;}
));
Wenn Sie sich nicht für die Verwendung von Drittanbieter-Bibliotheken interessieren, verfügt mein cyclops-react lib über Erweiterungen für alle JDK Collection - Typen, einschließlich Map . Wir können die Karte einfach direkt mit dem Operator 'map' transformieren (standardmäßig wirkt die Karte auf die Werte in der Karte ein).
MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
.map(Integer::parseInt);
mit bimap können Schlüssel und Werte gleichzeitig transformiert werden
MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
.bimap(this::newKey,Integer::parseInt);
Die deklarative und einfachere Lösung wäre:
yourMutableMap.replaceAll ((key, val) -> return_value_of_bi_your_function); Beachten Sie, dass Sie Ihren Kartenstatus ändern. Dies ist möglicherweise nicht das, was Sie wollen.