2015-05-29 1 views
16

Mit Java 8 Lambdas, was ist der "beste" Weg, um effektiv eine neue List<T> gegeben List<K> möglichen Tasten und eine Map<K,V>? Dies ist das Szenario, in dem Sie einen List von möglichen Map Schlüsseln erhalten und voraussichtlich einen List<T> generieren, wobei T ein Typ ist, der basierend auf einem Aspekt von V die Kartenwerttypen erstellt wurde.Wie erstelle ich eine Liste <T> von Karte <K,V> und Liste <K> von Schlüsseln?

Ich habe ein paar erforscht und fühle mich nicht wohl behaupten, dass eine Möglichkeit besser ist als die andere (mit vielleicht einer Ausnahme - siehe Code). Ich werde "Best" als eine Kombination aus Code-Klarheit und Laufzeit-Effizienz klarstellen. Das sind, was ich mir ausgedacht habe. Ich bin mir sicher, dass jemand es besser machen kann, was ein Aspekt dieser Frage ist. Ich mag den filter Aspekt der meisten nicht, da es bedeutet, dass Zwischenstrukturen und mehrere Durchläufe über den Namen List erstellt werden müssen. Im Moment wähle ich Beispiel 6 - eine einfache Schleife. (HINWEIS: Einige kryptischen Gedanken sind in den Code Kommentare, vor allem „extern referenzieren müssen ...“ Das bedeutet, extern aus der Lambda-.)

public class Java8Mapping { 
    private final Map<String,Wongo> nameToWongoMap = new HashMap<>(); 
    public Java8Mapping(){ 
     List<String> names = Arrays.asList("abbey","normal","hans","delbrook"); 
     List<String> types = Arrays.asList("crazy","boring","shocking","dead"); 
     for(int i=0; i<names.size(); i++){ 
      nameToWongoMap.put(names.get(i),new Wongo(names.get(i),types.get(i))); 
     } 
    } 

    public static void main(String[] args) { 
     System.out.println("in main"); 
     Java8Mapping j = new Java8Mapping(); 
     List<String> testNames = Arrays.asList("abbey", "froderick","igor"); 
     System.out.println(j.getBongosExample1(testNames).stream().map(Bongo::toString).collect(Collectors.joining(", "))); 
     System.out.println(j.getBongosExample2(testNames).stream().map(Bongo::toString).collect(Collectors.joining(", "))); 
     System.out.println(j.getBongosExample3(testNames).stream().map(Bongo::toString).collect(Collectors.joining(", "))); 
     System.out.println(j.getBongosExample4(testNames).stream().map(Bongo::toString).collect(Collectors.joining(", "))); 
     System.out.println(j.getBongosExample5(testNames).stream().map(Bongo::toString).collect(Collectors.joining(", "))); 
     System.out.println(j.getBongosExample6(testNames).stream().map(Bongo::toString).collect(Collectors.joining(", "))); 
    } 

    private static class Wongo{ 
     String name; 
     String type; 
     public Wongo(String s, String t){name=s;type=t;} 
     @Override public String toString(){return "Wongo{name="+name+", type="+type+"}";} 
    } 

    private static class Bongo{ 
     Wongo wongo; 
     public Bongo(Wongo w){wongo = w;} 
     @Override public String toString(){ return "Bongo{wongo="+wongo+"}";} 
    } 

    // 1: Create a list externally and add items inside 'forEach'. 
    //  Needs to externally reference Map and List 
    public List<Bongo> getBongosExample1(List<String> names){ 
     final List<Bongo> listOne = new ArrayList<>(); 
     names.forEach(s -> { 
        Wongo w = nameToWongoMap.get(s); 
        if(w != null) { 
         listOne.add(new Bongo(nameToWongoMap.get(s))); 
        } 
       }); 
     return listOne; 
    } 

    // 2: Use stream().map().collect() 
    // Needs to externally reference Map 
    public List<Bongo> getBongosExample2(List<String> names){ 
     return names.stream() 
       .filter(s -> nameToWongoMap.get(s) != null) 
       .map(s -> new Bongo(nameToWongoMap.get(s))) 
       .collect(Collectors.toList()); 
    } 

    // 3: Create custom Collector 
    // Needs to externally reference Map 
    public List<Bongo> getBongosExample3(List<String> names){ 
     Function<List<Wongo>,List<Bongo>> finisher = list -> list.stream().map(Bongo::new).collect(Collectors.toList()); 
     Collector<String,List<Wongo>,List<Bongo>> bongoCollector = 
       Collector.of(ArrayList::new,getAccumulator(),getCombiner(),finisher, Characteristics.UNORDERED); 

     return names.stream().collect(bongoCollector); 
    } 
    // example 3 helper code 
    private BiConsumer<List<Wongo>,String> getAccumulator(){ 
     return (list,string) -> { 
      Wongo w = nameToWongoMap.get(string); 
      if(w != null){ 
       list.add(w); 
      } 
     }; 
    } 
    // example 3 helper code 
    private BinaryOperator<List<Wongo>> getCombiner(){ 
     return (l1,l2) -> { 
      l1.addAll(l2); 
      return l1; 
     }; 
    } 

    // 4: Use internal Bongo creation facility 
    public List<Bongo> getBongosExample4(List<String> names){ 
     return names.stream().filter(s->nameToWongoMap.get(s) != null).map(s-> new Bongo(nameToWongoMap.get(s))).collect(Collectors.toList()); 
    } 

    // 5: Stream the Map EntrySet. This avoids referring to anything outside of the stream, 
    // but bypasses the lookup benefit from Map. 
    public List<Bongo> getBongosExample5(List<String> names){ 
     return nameToWongoMap.entrySet().stream().filter(e->names.contains(e.getKey())).map(e -> new Bongo(e.getValue())).collect(Collectors.toList()); 
    } 

    // 6: Plain-ol-java loop 
    public List<Bongo> getBongosExample6(List<String> names){ 
     List<Bongo> bongos = new ArrayList<>(); 
     for(String s : names){ 
      Wongo w = nameToWongoMap.get(s); 
      if(w != null){ 
       bongos.add(new Bongo(w)); 
      } 
     } 
     return bongos; 
    } 
} 
+1

Sie haben 'K' und' V', aber was ist 'T'? – user2357112

+0

@ user2357112 Bearbeitet. T ist ein Typ, der nicht in der Map enthalten ist, aber anhand der Map-Werte erstellt wird. Ich hoffe, das hilft. – MadConan

+3

plain-ol-java für den Sieg! – ZhongYu

Antwort

11

Wenn namesToWongoMap ist eine Instanzvariable, können Sie nicht vermeide wirklich ein Capturing Lambda.

Sie können den Stream aufzuräumen durch die Operationen Aufspaltung ein wenig mehr:

return names.stream() 
    .map(n -> namesToWongoMap.get(n)) 
    .filter(w -> w != null) 
    .map(w -> new Bongo(w)) 
    .collect(toList()); 
return names.stream() 
    .map(namesToWongoMap::get) 
    .filter(Objects::nonNull) 
    .map(Bongo::new) 
    .collect(toList()); 

diese Weise werden Sie nicht get zweimal nennen.

Dies ist sehr ähnlich wie die for Schleife, außer zum Beispiel könnte es theoretisch parallelisiert werden, wenn namesToWongoMap nicht gleichzeitig mutiert werden kann.

Ich mag die filter Aspekt der meisten nicht, da es bedeutet List Zwischenstrukturen und mehrere Durchgänge über die Namen erstellen zu müssen.

Es gibt keine Zwischenstrukturen und es gibt nur einen Durchlauf über die List. Eine Stream-Pipeline sagt "für jedes Element ... mache diese Abfolge von Operationen". Jedes Element wird einmal besucht und die Pipeline wird angewendet.

Hier sind einige relevante Zitate aus dem java.util.stream package description:

Ein Strom nicht eine Datenstruktur ist, die Elemente speichert; Stattdessen überträgt es Elemente aus einer Quelle wie einer Datenstruktur, einem Array, einer Generatorfunktion oder einem E/A-Kanal durch eine Pipeline von Rechenoperationen.

Die Verarbeitung von Strömen ermöglicht eine erhebliche Effizienz; In einer Pipeline wie dem obigen Filter-Map-Sum-Beispiel können Filterung, Abbildung und Summierung zu einem einzigen Durchlauf der Daten mit minimalem Zwischenzustand verschmolzen werden.

+0

Schön. Einfach und sauber. Ich mag das! – MadConan

+1

Ich denke, ich setze mich für einen oder zwei Tage darauf und schaue von anderen herein. Ich bin gespannt, ob sich irgendjemand etwas Besseres einfallen lassen kann, obwohl ich das bezweifle. :) – MadConan

3

Ein Ansatz, den ich nicht sehen, ist retainAll:

public List<Bongo> getBongos(List<String> names) { 
    Map<String, Wongo> copy = new HashMap<>(nameToWongoMap); 
    copy.keySet().retainAll(names); 

    return copy.values().stream().map(Bongo::new).collect(
     Collectors.toList()); 
} 

Die zusätzliche Karte eine minimalen Leistungseinbußen, da es das Kopieren von Zeigern auf Objekte, nicht die Objekte selbst gerade ist.

+1

Weeelllll ... Ich denke, es gibt einen * relativ * hohen Betrag von Overhead mit dem Erstellen aller Knoten für jeden Eintrag verbunden. Abgehen von [diesem älteren Artikel] (http://www.javacodegeeks.com/2010/08/java-best-practices-vector-arraylist.html). Aber deine Antwort ist schön und sauber und der Overhead ist vielleicht kein Faktor. – MadConan

7

Radiodef's answer ziemlich viel genagelt, denke ich. Die Lösung gibt gegeben:

return names.stream() 
    .map(namesToWongoMap::get) 
    .filter(Objects::nonNull) 
    .map(Bongo::new) 
    .collect(toList()); 

ist wahrscheinlich über die besten, die 8.

in Java getan werden kann, wollte ich eine kleine Falte in das erwähnen, though. Der Map.get Aufruf gibt null zurück, wenn der Name nicht in der Karte vorhanden ist und dieser anschließend herausgefiltert wird. Es ist nichts falsch mit diesem per se, obwohl es Semantik null-bedeutet-nicht-Gegenwart in die Pipeline-Struktur backen.

In gewissem Sinne würden wir eine Mapper-Pipeline-Operation wünschen, die die Wahl hat, null oder eins zurückzugeben. Eine Möglichkeit, dies mit Streams zu tun, ist mit flatMap. Die Flatmapper-Funktion kann eine beliebige Anzahl von Elementen in den Stream zurückgeben, aber in diesem Fall wollen wir nur Null oder Eins. Hier ist, wie das tun:

return names.stream() 
    .flatMap(name -> { 
     Wongo w = nameToWongoMap.get(name); 
     return w == null ? Stream.empty() : Stream.of(w); 
    }) 
    .map(Bongo::new) 
    .collect(toList()); 

Ich gebe zu, das ziemlich klobig ist und so würde ich nicht empfehlen dies zu tun. Ein etwas besser, aber etwas dunkel Ansatz ist dies:

return names.stream() 
    .flatMap(name -> Optional.ofNullable(nameToWongoMap.get(name)) 
          .map(Stream::of).orElseGet(Stream::empty)) 
    .map(Bongo::new) 
    .collect(toList()); 

aber ich bin noch nicht sicher, ob ich dies empfehlen, wie es steht. Die Verwendung von flatMap weist jedoch auf einen anderen Ansatz hin. Wenn Sie eine kompliziertere Richtlinie für den Umgang mit dem nicht vorhandenen Fall haben, können Sie dies in eine Hilfsfunktion umwandeln, die einen Stream zurückgibt, der das Ergebnis enthält, oder einen leeren Stream, wenn es kein Ergebnis gibt.

Schließlich JDK 9 - noch in der Entwicklung zum Zeitpunkt des Schreibens - hat Stream.ofNullable hinzugefügt, die in genau diesen Situationen nützlich ist:

return names.stream() 
    .flatMap(name -> Stream.ofNullable(nameToWongoMap.get(name))) 
    .map(Bongo::new) 
    .collect(toList()); 

Als beiseite, hat JDK 9 hinzugefügt auch Optional.stream, die eine schafft Null-oder-ein-Stream von einem Optional. Dies ist nützlich in Fällen, in denen Sie eine Funktion zum Zurückgeben von Optionen von innerhalb flatMap aufrufen möchten. Weitere Informationen finden Sie unter this answer und this answer.

+2

Ich habe bereits [StreamEx.ofNullable (obj)] (http://amaembo.github.io/streamex/javadoc/javax/util/streamex/StreamEx.html#ofNullable-T-) und [StreamEx.of (Optional) hinzugefügt. ] (http: //amaembo.github.io/streamex/javadoc/javax/util/streamex/StreamEx.html # von-java.util.Optional-) zu meiner Bibliothek. Eigentlich kann sogar ohne 3rd-Party-Bibliotheken und den Wechsel zu JDK9 jeder in einer projektspezifischen Utility-Klasse ähnliche statische Methoden erstellen und verwenden. –