2016-02-18 3 views
19

Ich habe eine Java-Karte, Ich mag würde zu transformieren und Filter. Als triviales Beispiel, nehme ich alle Werte auf ganze Zahlen umwandeln will, dann die ungeraden Einträge entfernen.Transformation und eine Java-Map-Filter mit Strömen

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 liefert: {a=1234, c=3456}

Aber ich kann nicht helfen, aber frage mich, ob es einen Weg gibt ist .entrySet().stream() zweimal zu vermeiden rufen.

Gibt es eine Weise, die ich durchführen kann sowohl transformieren und Filteroperationen und rufe .collect() nur einmal am Ende?

+0

Ich glaube nicht, dass es möglich ist. Basierend auf javadoc "Ein Stream sollte nur einmal ausgeführt werden (durch einen Zwischen- oder Terminal-Stream-Vorgang). Dies schließt zum Beispiel" gegabelte "Streams aus, bei denen dieselbe Quelle zwei oder mehr Pipelines oder mehrere Traversierungen derselben durchführt stream. Eine Stream-Implementierung kann IllegalStateException auslösen, wenn sie erkennt, dass der Stream wiederverwendet wird. " – kosa

+0

@Namban Das ist nicht, worum es in der Frage geht. – immibis

Antwort

21

Ja, können Sie jeden Eintrag in einem anderen temporären Eintrag zuordnen, die den Schlüssel und die analysierten Integer-Wert halten werden. 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 
     )); 

Bitte beachte, dass ich Integer.valueOf anstelle von parseInt da wir eigentlich wollen eine Box int.


Wenn Sie den Luxus haben die StreamEx Bibliothek zu verwenden, können Sie es tun, ganz einfach:

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

Obwohl dies ein möglicher Hacky-Weg ist, habe ich das Gefühl, dass wir Performance + Lesbarkeit für wenige Zeilen Code opfern, oder? – kosa

+3

@Nambari Ich sehe nicht, warum es das "hacky" ist. Es ist nur ein Kartenfilter. Wenn Sie "AbstractMap.SimpleEntry" explizit verwenden, können Sie ein anderes "Paar" erstellen, aber ich halte es für angebracht, da wir bereits mit Karten arbeiten. – Tunaki

+1

Ich benutze "Hacky" nur wegen "temporären Eintrag" wir verfolgen, möglicherweise nicht korrekt Begriff. Ich mochte StreamEx Lösung. – kosa

3

Sie konnten die Stream.collect(supplier, accumulator, combiner) Methode verwenden, um die Einträge zu transformieren und sie bedingt 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 Inneren des Akkumulators, ich bin mit Optional Methoden sowohl die Transformation und das Prädikat anzuwenden, und, wenn das optionale v Alue ist noch vorhanden, ich füge es der Karte hinzu, die gesammelt wird.

+1

Sehr nah an [meiner ersten Variante] (http://Stackoverflow.com/a/35490546/2711488), aber ich denke nicht, dass die Verwendung von 'Optional' ist ein Gewinn hier ... – Holger

+0

@Holger es ist nur alles zu halten in einer Zeile. Wie auch immer, ich hatte deine Antwort nicht gesehen. Der Gewinn (wenn wir sehr subtil werden) könnte sein, dass 'Optional.ofNullable()' Nullschlüssel zulässt. –

+1

Wir haben zur gleichen Zeit geschrieben. Die 'Optional'-Kette ist tatsächlich nicht eine Zeile (oder eine sehr große), sondern ein einzelner Ausdruck. Aber beginnend mit einer bestimmten Ausdrucksgröße erscheinen die zwei geschweiften Klammern, die für eine Anweisung Lambda benötigt werden, nicht mehr so ​​teuer ... – Holger

3

Ein anderer Weg, dies zu tun ist, um die Werte, die Sie aus der transformierten Map nicht wollen, zu entfernen:

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, Sie wollen ein veränderliches Map als das Ergebnis, wenn nicht, können Sie wahrscheinlich eine unveränderliche erstellen ein von output.


Wenn Sie die Werte in der gleichen Art transformieren und wollen, dass die Map an seinem Platz modifizieren diese viel kürzer mit replaceAll sein könnte:

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

Dies auch input Wandelbare annimmt.


ich nicht empfehlen dies zu tun, weil es nicht für alle gültigen Map Implementierungen arbeiten und stoppen können für HashMap in Zukunft arbeiten, aber Sie derzeit replaceAll verwenden können und werfen einen HashMap die Art des ändern Werte:

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

Dies wird Ihnen auch geben Sie Sicherheitswarnungen geben und wenn Sie versuchen, einen Wert aus dem Map durch eine Referenz des alten Typs wie folgt abzurufen:

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

Es wird eine ClassCastException werfen.


Sie den ersten Teil in ein Verfahren verwandeln bewegen könnte die vorformulierten zu vermeiden, wenn Sie erwarten, eine Menge, es zu benutzen:

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 es wie folgt verwenden:

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

Hinweis dass die doppelte Schlüsselausnahme ausgelöst werden kann, wenn z. B. die oldMap eine IdentityHashMap ist und die mapFactory eineerstellt.

+1

Ihre "Duplicate keys" + a + "" + b "Nachricht ist irreführend:' a' und 'b' sind eigentlich Werte, keine Schlüssel. –

+0

@TagirValeev Ja, das ist mir aufgefallen, aber es ist genau so, wie es in 'Collectors' für die Zwei-Argument-Version von' toMap' gemacht wurde. Wenn ich es in eine Code-Box änderte, entschied ich mich zu gehen es. Ich werde es jetzt ändern, da Sie es auch für irreführend halten. – Alex

+1

Ja, dies ist ein bekanntes Problem, das bereits in [Java-9] (https://bugs.openjdk.java.net/browse/JDK-8040892) behoben wurde (aber nicht nach Java-8 zurückportiert wurde). Java-9 zeigt kollidierende Schlüssel sowie beide Werte in der Ausnahmebedingungsnachricht an. –

11

Eine Möglichkeit, das Problem mit einem viel geringeren Overhead zu lösen, besteht darin, das Mapping und die Filterung auf den 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 Zwischen Map.Entry Instanzen und noch besser, wird die Box von int Werte auf den Punkt verschieben, wenn die Werte in die Map tatsächlich hinzugefügt werden, die durch den Filter zurückgewiesen Werte bedeutet, dass überhaupt nicht verpackt.

Im Vergleich zu dem, was Collectors.toMap(…) der Fall ist, wird der Betrieb auch unter Verwendung von Map.put anstatt Map.merge vereinfacht, da wir vorher wissen, dass wir hier keine Schlüssel Kollisionen behandeln müssen.

jedoch, solange Sie nicht die parallele Ausführung nutzen möchten, betrachten Sie können auch die gewöhnliche Schleife

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 Iteration Variante:

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

letzteres ganz kompakt ist und zumindest auf Augenhöhe mit allen anderen Varianten in Bezug auf Single-Thread-Performance.

+0

Ich habe für Ihre Empfehlung eine einfache Schleife upvoted. Nur weil du Streams verwenden kannst, heißt das nicht, dass du es tun solltest. –

+2

@ Jeffrey Bosboom: Ja, die gute alte 'for' Schleife ist noch am Leben. Obwohl ich im Fall von Maps die Methode 'Map.forEach' für kleinere Schleifen verwende, nur weil' (k, v) -> 'viel schöner ist, als eine' Map.Entry 'Variable und zu deklarieren möglicherweise noch zwei weitere Variablen für den eigentlichen Schlüssel und Wert ... – Holger

1

Guava ist dein Freund:

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

Beachten Sie, dass output eine transformierte, gefiltert Ansicht von input. Sie müssen eine Kopie erstellen, wenn Sie unabhängig voneinander arbeiten möchten.

0

Hier ist der Code von AbacusUtil

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 AbacusUtil.

Verwandte Themen