Java8: HashMap <X, Y> zu HashMap <X, Z> mit Stream / Map-Reduce / Collector

209

Ich weiß, wie man ein einfaches Java Listvon Y-> "transformiert" Z, dh:

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

Jetzt möchte ich im Grunde dasselbe mit einer Karte machen, dh:

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-> beschränkt sein Integer. Genau wie im Listobigen Beispiel möchte ich eine beliebige Methode (oder einen Konstruktor) aufrufen.

Benjamin M.
quelle

Antworten:

372
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 Map.Entryin einem map()Aufruf keine neuen s erstellen, sodass die Arbeit in den collect()Aufruf eingemischt wird .

John Kugelman
quelle
59
Sie können ersetzen e -> e.getKey()mit Map.Entry::getKey. Aber das ist eine Frage des Geschmacks / Programmierstils.
Holger
5
Eigentlich ist es eine Frage der Leistung, Ihr Vorschlag ist dem Lambda-Stil etwas überlegen
Jon Burgin
36

Hier sind einige Variationen der Antwort von Sotirios Delimanolis , 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 erforderlich, wenn Sie beispielsweise möchten, dass die Ausgabekarte einen Schlüssel enthält, der eine Oberklasse des Schlüssels der Eingabekarte 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 es ist wirklich erfunden, und ich gebe zu, dass der begrenzte Platzhalter für Y nur in Randfällen hilfreich ist.)

Ein zweiter Punkt ist, dass entrySetich den Stream nicht über die Eingabe-Map laufen ließ , sondern über die keySet. Dies macht den Code meiner Meinung nach ein wenig sauberer, da Werte aus der Karte anstatt aus dem Karteneintrag abgerufen werden müssen. Übrigens hatte ich zunächst key -> keyals erstes Argument dazu toMap()und dies schlug aus irgendeinem Grund mit einem Typinferenzfehler fehl. Es (X key) -> keyfunktionierte genauso wie es funktionierte Function.identity().

Eine weitere Variation 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. Das ist noch einfacher, denke ich, weil es auf die Sammler verzichtet, deren Verwendung mit Karten etwas umständlich ist. Der Grund dafür ist, dass Map.forEach()der Schlüssel und der Wert als separate Parameter angegeben werden, während der Stream nur einen Wert hat - und Sie müssen auswählen, ob Sie den Schlüssel oder den Karteneintrag als diesen Wert verwenden möchten. Auf der negativen Seite fehlt dies die reiche, strömende Güte der anderen Ansätze. :-)

Stuart Marks
quelle
11
Function.identity()mag cool aussehen, aber da die erste Lösung eine Map / Hash-Suche für jeden Eintrag erfordert, während alle anderen Lösungen dies nicht tun, würde ich es nicht empfehlen.
Holger
13

Eine generische Lösung wie diese

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));
Sotirios Delimanolis
quelle
Netter Ansatz mit Generika. Ich denke, es kann ein bisschen verbessert werden - siehe meine Antwort.
Stuart Marks
13

Guavas Funktion Maps.transformValuesist genau das, wonach Sie suchen, und sie funktioniert gut mit Lambda-Ausdrücken:

Maps.transformValues(originalMap, val -> ...)
Alex Krauss
quelle
Ich mag diesen Ansatz, aber pass auf, dass du ihm keine java.util.Function übergibst. Da Eclipse com.google.common.base.Function erwartet, gibt es einen nicht hilfreichen Fehler aus. Es heißt, Function ist nicht für Function anwendbar, was verwirrend sein kann: "Die Methode transformValues ​​(Map <K, V1>, Function <? Super V1 , V2>) im Typ Maps gilt nicht für die Argumente (Map <Foo, Bar>, Function <Bar, Baz>) "
mskfisher
Wenn Sie a bestehen müssen java.util.Function, haben Sie zwei Möglichkeiten. 1. Vermeiden Sie das Problem, indem Sie ein Lambda verwenden, damit Java-Typ-Inferenz es herausfinden kann. 2. Verwenden Sie eine Methodenreferenz wie javaFunction :: apply, um ein neues Lambda zu erstellen, das durch Typinferenz ermittelt werden kann.
Joe
5

Meine StreamEx- Bibliothek, die die Standard-Stream-API erweitert, bietet eine EntryStreamKlasse, die sich besser zum Transformieren von Karten eignet :

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

Eine Alternative, die es zu Lernzwecken immer gibt, besteht darin, Ihren benutzerdefinierten Kollektor über Collector.of () zu erstellen, obwohl der JDK-Kollektor toMap () hier kurz und bündig 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;}
               ));
Ira
quelle
Ich habe mit diesem benutzerdefinierten Kollektor als Basis begonnen und wollte hinzufügen, dass zumindest bei Verwendung von parallelStream () anstelle von stream () der binaryOperator auf etwas Ähnliches umgeschrieben werden sollte, da sonst map2.entrySet().forEach(entry -> { if (map1.containsKey(entry.getKey())) { map1.get(entry.getKey()).merge(entry.getValue()); } else { map1.put(entry.getKey(),entry.getValue()); } }); return map1Werte beim Reduzieren verloren gehen.
user691154
3

Wenn es Ihnen nichts ausmacht, Bibliotheken von Drittanbietern zu verwenden, enthält meine Cyclops-React- Bibliothek Erweiterungen für alle JDK-Sammlungstypen , einschließlich Map . Wir können die Karte einfach direkt mit dem Operator 'map' transformieren (standardmäßig wirkt sich die Map auf die Werte in der Map aus).

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

Mit bimap können die Schlüssel und Werte gleichzeitig transformiert werden

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