Java8: HashMap zu HashMap mit Stream / Map-Reduce / Collector

Lesezeit: 5 Minuten

Benutzer-Avatar
Benjamin m

Ich weiß, wie man ein einfaches Java “transformiert”. List aus Y -> Zdh:

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

Benutzer-Avatar
John Kugelmann

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. Neu konstruieren kann man nicht Map.Entrys in a map() Anruf, damit die Arbeit in die gemischt wird collect() Anruf.

  • Sie können ersetzen e -> e.getKey() mit Map.Entry::getKey. Aber das ist Geschmackssache/Programmierstil.

    – Holger

    18. September 2014 um 9:49 Uhr

  • Eigentlich ist es eine Frage der Leistung, Sie schlagen vor, dem Lambda-Stil etwas überlegen zu sein.

    – Jon Burgin

    20. April 2017 um 20:35 Uhr

Benutzer-Avatar
Stuart Mark

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. Das erste ist die Verwendung von Platzhaltern in den Generika; dadurch wird 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 es ist wirklich erfunden, und ich gebe zu, dass der begrenzte Platzhalter für Y nur in Grenzfällen hilft.)

Ein zweiter Punkt ist, dass der Stream nicht über die Eingabekarte läuft entrySetlief ich es über die keySet. Das macht den Code ein wenig sauberer, denke ich, auf Kosten der Notwendigkeit, Werte aus der Map statt aus dem Map-Eintrag zu holen. Übrigens hatte ich anfangs key -> key als erstes Argument zu toMap() und dies schlug aus irgendeinem Grund mit einem Typrückschlussfehler fehl. Ändern Sie es zu (X key) -> key funktionierte, wie auch Function.identity().

Noch eine andere 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 nutzt Map.forEach() statt Strömen. Das ist noch einfacher, finde ich, weil es auf die etwas umständlich zu handhabenden Kollektoren mit Karten verzichtet. Der Grund ist, dass Map.forEach() gibt den Schlüssel und den Wert als separate Parameter an, während der Stream nur einen Wert hat – und Sie müssen wählen, ob Sie den Schlüssel oder den Karteneintrag als diesen Wert verwenden möchten. Auf der Minusseite fehlt die reichhaltige, fließende Güte der anderen Ansätze. 🙂

  • 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

    18. September 2014 um 9:53 Uhr

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

Die Funktion der Guave Maps.transformValues ist das, wonach Sie suchen, und es funktioniert gut mit Lambda-Ausdrücken:

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

Benutzer-Avatar
Lukas Edder

Muss es unbedingt 100% funktional und flüssig sein? Wenn nicht, wie wäre es damit, was so kurz wie möglich ist:

Map<String, Integer> output = new HashMap<>();
input.forEach((k, v) -> output.put(k, Integer.valueOf(v));

(wenn Sie mit der Scham und Schuld leben können, Streams mit Nebenwirkungen zu kombinieren)

Benutzer-Avatar
Tagir Walejew

Mein StreamEx Bibliothek, die die Standard-Stream-API verbessert, bietet eine EntryStream Klasse, die besser zum Transformieren von Karten geeignet ist:

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

Benutzer-Avatar
Gemeinschaft

Eine Alternative, die immer zu Lernzwecken vorhanden ist, besteht darin, Ihren benutzerdefinierten Kollektor über Collector.of() zu erstellen, obwohl toMap() JDK-Kollektor hier prägnant 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;}
               ));

  • Ich habe mit diesem benutzerdefinierten Kollektor als Basis begonnen und wollte hinzufügen, dass der binaryOperator zumindest bei Verwendung von parallelStream() anstelle von stream() in etwas Ähnliches umgeschrieben werden sollte map2.entrySet().forEach(entry -> { if (map1.containsKey(entry.getKey())) { map1.get(entry.getKey()).merge(entry.getValue()); } else { map1.put(entry.getKey(),entry.getValue()); } }); return map1 oder Werte gehen beim Reduzieren verloren.

    – Benutzer691154

    14. Dezember 2016 um 9:41 Uhr


1357540cookie-checkJava8: HashMap zu HashMap mit Stream / Map-Reduce / Collector

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy