Transformieren und filtern Sie eine Java Map mit Streams

Lesezeit: 9 Minuten

Benutzeravatar von Paul I
Paul I

Ich habe eine Java Map, die ich transformieren und filtern möchte. Nehmen wir als triviales Beispiel an, ich möchte alle Werte in Ganzzahlen konvertieren und dann die ungeraden Einträge entfernen.

Map<String, String> input = new HashMap<>();
input.put("a", "1234");
input.put("b", "2345");
input.put("c", "3456");
input.put("d", "4567");

Map<String, Integer> output = input.entrySet().stream()
        .collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> Integer.parseInt(e.getValue())
        ))
        .entrySet().stream()
        .filter(e -> e.getValue() % 2 == 0)
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));


System.out.println(output.toString());

Das ist richtig und ergibt: {a=1234, c=3456}

Ich kann jedoch nicht umhin, mich zu fragen, ob es eine Möglichkeit gibt, Anrufe zu vermeiden .entrySet().stream() zweimal.

Gibt es eine Möglichkeit, sowohl Transformations- als auch Filteroperationen durchzuführen und aufzurufen .collect() nur einmal am ende?

  • Ich glaube nicht, dass es möglich ist. Basierend auf javadoc “Ein Stream sollte nur einmal bearbeitet werden (durch Aufrufen einer Zwischen- oder End-Stream-Operation). Dies schließt beispielsweise “gegabelte” Streams aus, bei denen dieselbe Quelle zwei oder mehr Pipelines speist, oder mehrere Durchläufe derselben stream. Eine Stream-Implementierung kann IllegalStateException auslösen, wenn sie erkennt, dass der Stream wiederverwendet wird.”

    – kosa

    18. Februar 2016 um 16:25 Uhr

  • @Namban Darum geht es in der Frage nicht.

    – Benutzer253751

    18. Februar 2016 um 20:00 Uhr

Benutzeravatar von Tunaki
Tunaki

Ja, Sie können jeden Eintrag einem anderen temporären Eintrag zuordnen, der den Schlüssel und den geparsten ganzzahligen Wert enthält. Dann können Sie jeden Eintrag basierend auf seinem Wert filtern.

Map<String, Integer> output =
    input.entrySet()
         .stream()
         .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), Integer.valueOf(e.getValue())))
         .filter(e -> e.getValue() % 2 == 0)
         .collect(Collectors.toMap(
             Map.Entry::getKey,
             Map.Entry::getValue
         ));

Beachten Sie, dass ich verwendet habe Integer.valueOf anstatt parseInt da wir eigentlich eine Box wollen int.


Wenn Sie den Luxus haben, das zu nutzen StreamEx Bibliothek geht das ganz einfach:

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

  • Obwohl dies ein möglicher hackiger Weg ist, habe ich das Gefühl, dass wir Leistung und Lesbarkeit für wenige Codezeilen opfern, nicht wahr?

    – kosa

    18. Februar 2016 um 16:30 Uhr


  • @Nambari Ich verstehe nicht, warum es so “hacky” ist. Es ist nur ein Kartenfilter. Wenn es die explizite Verwendung von ist AbstractMap.SimpleEntrykönnen Sie eine weitere erstellen Pair aber ich denke, es ist hier angebracht, da wir uns bereits mit Karten befassen.

    – Tunaki

    18. Februar 2016 um 16:32 Uhr


  • Ich habe “hacky” nur wegen des “vorübergehenden Eintrags” verwendet, den wir verfolgen, möglicherweise ist der Begriff nicht korrekt. Ich mochte die StreamEx-Lösung.

    – kosa

    18. Februar 2016 um 16:34 Uhr

  • @Nambari, beachten Sie, dass StreamEx intern dasselbe tut, es ist nur ein syntaktischer Zucker.

    – Tagir Walejew

    19. Februar 2016 um 6:20 Uhr

  • @TagirValeev: Ja, ich weiß. Wenn die API nicht unterstützt, machen all diese Bibliotheken nur den sytaktischen Zucker.

    – kosa

    19. Februar 2016 um 14:22 Uhr

Eine Möglichkeit, das Problem mit viel geringerem Aufwand zu lösen, besteht darin, die Zuordnung und Filterung nach unten zum Kollektor zu verschieben.

Map<String, Integer> output = input.entrySet().stream().collect(
    HashMap::new,
    (map,e)->{ int i=Integer.parseInt(e.getValue()); if(i%2==0) map.put(e.getKey(), i); },
    Map::putAll);

Dies erfordert nicht die Schaffung von Zwischenprodukten Map.Entry Instanzen und noch besser, wird das Boxen verschieben int Werte bis zu dem Punkt, an dem die Werte tatsächlich zu den addiert werden Mapwas impliziert, dass vom Filter abgelehnte Werte überhaupt nicht eingerahmt werden.

Verglichen mit was Collectors.toMap(…) tut, wird die Bedienung auch durch die Verwendung vereinfacht Map.put statt Map.merge da wir vorher wissen, dass wir hier keine Schlüsselkollisionen behandeln müssen.

Solange Sie jedoch keine parallele Ausführung verwenden möchten, können Sie auch die gewöhnliche Schleife in Betracht ziehen

HashMap<String,Integer> output=new HashMap<>();
for(Map.Entry<String, String> e: input.entrySet()) {
    int i = Integer.parseInt(e.getValue());
    if(i%2==0) output.put(e.getKey(), i);
}

oder die interne Iterationsvariante:

HashMap<String,Integer> output=new HashMap<>();
input.forEach((k,v)->{ int i = Integer.parseInt(v); if(i%2==0) output.put(k, i); });

Letzterer ist recht kompakt und mindestens auf Augenhöhe mit allen anderen Varianten in Bezug auf Single-Thread-Performance.

  • Ich habe für Ihre Empfehlung einer einfachen Schleife gestimmt. Nur weil Sie Streams verwenden können, heißt das nicht, dass Sie es tun sollten.

    – Jeffrey Bosboom

    18. Februar 2016 um 20:04 Uhr

  • @Jeffrey Bosboom: ja, das gute alte for Schleife lebt noch. Obwohl ich im Fall von Karten gerne die verwende Map.forEach Methode für kleinere Schleifen, nur weil (k,v)-> ist viel schöner, als a zu erklären Map.Entry<PossiblyLongKeyType,PossiblyLongValueType> Variable und eventuell zwei weitere Variablen für den eigentlichen Schlüssel und Wert…

    – Holger

    18. Februar 2016 um 20:09 Uhr

Benutzeravatar von shmosel
Schmosel

Guaveist dein Freund:

Map<String, Integer> output = Maps.filterValues(Maps.transformValues(input, Integer::valueOf), i -> i % 2 == 0);

Denk daran, dass output ist eine transformierte, gefilterte Sicht von input. Sie müssen eine Kopie erstellen, wenn Sie sie unabhängig bearbeiten möchten.

Benutzeravatar von fps
fps

Du könntest die verwenden Stream.collect(supplier, accumulator, combiner) Methode, um die Einträge zu transformieren und sie bedingt zu akkumulieren:

Map<String, Integer> even = input.entrySet().stream().collect(
    HashMap::new,
    (m, e) -> Optional.ofNullable(e)
            .map(Map.Entry::getValue)
            .map(Integer::valueOf)
            .filter(i -> i % 2 == 0)
            .ifPresent(i -> m.put(e.getKey(), i)),
    Map::putAll);

System.out.println(even); // {a=1234, c=3456}

Hier, im Akkumulator, verwende ich Optional Methoden, um sowohl die Transformation als auch das Prädikat anzuwenden, und wenn der optionale Wert noch vorhanden ist, füge ich ihn der zu sammelnden Karte hinzu.

Alex - Benutzeravatar von GlassEditor.com
Alex – GlassEditor.com

Eine andere Möglichkeit, dies zu tun, besteht darin, die nicht gewünschten Werte aus der Transformation zu entfernen Map:

Map<String, Integer> output = input.entrySet().stream()
        .collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> Integer.parseInt(e.getValue()),
                (a, b) -> { throw new AssertionError(); },
                HashMap::new
         ));
output.values().removeIf(v -> v % 2 != 0);

Dies setzt voraus, dass Sie eine Veränderliche wollen Map als Ergebnis, wenn nicht, können Sie wahrscheinlich ein unveränderliches erstellen output.


Wenn Sie die Werte in denselben Typ umwandeln und die ändern möchten Map an Ort und Stelle könnte dies viel kürzer sein replaceAll:

input.replaceAll((k, v) -> v + " example");
input.values().removeIf(v -> v.length() > 10);

Dies setzt auch voraus input ist wandelbar.


Ich empfehle dies nicht, da es nicht für alle gültigen funktioniert Map Implementierungen und kann aufhören zu arbeiten HashMap in der Zukunft, aber Sie können derzeit verwenden replaceAll und wirf a HashMap um den Typ der Werte zu ändern:

((Map)input).replaceAll((k, v) -> Integer.parseInt((String)v));
Map<String, Integer> output = (Map)input;
output.values().removeIf(v -> v % 2 != 0);

Dadurch erhalten Sie auch Typsicherheitswarnungen und wenn Sie versuchen, einen Wert aus dem abzurufen Map durch eine Referenz des alten Typs wie folgt:

String ex = input.get("a");

Es wird a werfen ClassCastException.


Sie könnten den ersten Transformationsteil in eine Methode verschieben, um die Boilerplate zu vermeiden, wenn Sie erwarten, dass sie häufig verwendet wird:

public static <K, VO, VN, M extends Map<K, VN>> M transformValues(
        Map<? extends K, ? extends VO> old, 
        Function<? super VO, ? extends VN> f, 
        Supplier<? extends M> mapFactory){
    return old.entrySet().stream().collect(Collectors.toMap(
            Entry::getKey, 
            e -> f.apply(e.getValue()), 
            (a, b) -> { throw new IllegalStateException("Duplicate keys for values " + a + " " + b); },
            mapFactory));
}

Und benutze es so:

    Map<String, Integer> output = transformValues(input, Integer::parseInt, HashMap::new);
    output.values().removeIf(v -> v % 2 != 0);

Beachten Sie, dass die Ausnahme für doppelte Schlüssel ausgelöst werden kann, wenn beispielsweise die old Map ist ein IdentityHashMap und das mapFactory schafft ein HashMap.

  • Dein "Duplicate keys " + a + " " + b Meldung ist irreführend: a Und b sind eigentlich Werte, keine Schlüssel.

    – Tagir Walejew

    19. Februar 2016 um 6:22 Uhr

  • @TagirValeev ja, das ist mir aufgefallen, aber genauso wird es gemacht Collectors für die Version mit zwei Argumenten von toMap und das Ändern zu dem, was ich in Betracht gezogen hatte, würde eine Bildlaufleiste auf das Codefeld setzen, also beschloss ich, es zu lassen. Ich werde es jetzt ändern, da Sie es auch für irreführend halten.

    – Alex – GlassEditor.com

    19. Februar 2016 um 6:28 Uhr

  • Ja, das ist ein bekanntes Problem, das bereits behoben wurde Java-9 (aber nicht nach Java-8 zurückportiert). Java-9 zeigt einen kollidierenden Schlüssel sowie beide Werte in der Ausnahmemeldung an.

    – Tagir Walejew

    19. Februar 2016 um 6:43 Uhr


  • Das Lustige an Ihrem unsicheren Hack ist, dass die groupingBy Collector tut etwas Ähnliches hinter den Kulissen, was eine große Überraschung sein kann, wenn Sie einen Lieferanten verwenden, der Kartenimplementierungen erstellt, die Typsicherheit erzwingen, z ()->Collections.checkedMap(new HashMap<>(), …)

    – Holger

    19. Februar 2016 um 9:40 Uhr


Hier ist Code von Abakus-gemein

Map<String, String> input = N.asMap("a", "1234", "b", "2345", "c", "3456", "d", "4567");

Map<String, Integer> output = Stream.of(input)
                          .groupBy(e -> e.getKey(), e -> N.asInt(e.getValue()))
                          .filter(e -> e.getValue() % 2 == 0)
                          .toMap(Map.Entry::getKey, Map.Entry::getValue);

N.println(output.toString());

Erklärung: Ich bin der Entwickler von abacus-common.

  • Dein "Duplicate keys " + a + " " + b Meldung ist irreführend: a Und b sind eigentlich Werte, keine Schlüssel.

    – Tagir Walejew

    19. Februar 2016 um 6:22 Uhr

  • @TagirValeev ja, das ist mir aufgefallen, aber genauso wird es gemacht Collectors für die Version mit zwei Argumenten von toMap und das Ändern zu dem, was ich in Betracht gezogen hatte, würde eine Bildlaufleiste auf das Codefeld setzen, also beschloss ich, es zu lassen. Ich werde es jetzt ändern, da Sie es auch für irreführend halten.

    – Alex – GlassEditor.com

    19. Februar 2016 um 6:28 Uhr

  • Ja, das ist ein bekanntes Problem, das bereits behoben wurde Java-9 (aber nicht nach Java-8 zurückportiert). Java-9 zeigt einen kollidierenden Schlüssel sowie beide Werte in der Ausnahmemeldung an.

    – Tagir Walejew

    19. Februar 2016 um 6:43 Uhr


  • Das Lustige an Ihrem unsicheren Hack ist, dass die groupingBy Collector tut etwas Ähnliches hinter den Kulissen, was eine große Überraschung sein kann, wenn Sie einen Lieferanten verwenden, der Kartenimplementierungen erstellt, die Typsicherheit erzwingen, z ()->Collections.checkedMap(new HashMap<>(), …)

    – Holger

    19. Februar 2016 um 9:40 Uhr


1444380cookie-checkTransformieren und filtern Sie eine Java Map mit Streams

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

Privacy policy