2015-05-29 15 views
16

Utilizzo di Java 8 lambda, qual è il modo "migliore" per creare in modo efficace un nuovo List<T> dato un List<K> di chiavi possibili e un Map<K,V>? Questo è lo scenario in cui viene fornito un List di chiavi possibili Map e si prevede di generare un List<T> dove T è un tipo che è costruito sulla base di alcuni aspetti di V, i tipi di valore della mappa.Come creare un elenco <T> dalla mappa <K,V> e nell'elenco <K> di chiavi?

Ne ho esplorati alcuni e non mi sento a mio agio affermando che un modo è migliore di un altro (con forse un'eccezione - vedi codice). Chiarirò "il migliore" come combinazione di chiarezza del codice e efficienza di runtime. Questi sono ciò che mi è venuto in mente. Sono sicuro che qualcuno può fare meglio, che è un aspetto di questa domanda. Non mi piace l'aspetto filter della maggior parte in quanto significa dover creare strutture intermedie e passaggi multipli sui nomi List. In questo momento, sto optando per l'esempio 6: un semplice ciclo di apprendimento. (NOTA: Alcuni pensieri criptici sono nei commenti di codice, in particolare "necessario fare riferimento a esternamente ..." Questo significa esterno dalla 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

Hai 'K' e' V', ma cosa c'è 'T'? – user2357112

+0

@ user2357112 Modificato. T è un tipo non nella mappa, ma è costruito usando i valori della mappa. Spero possa aiutare. – MadConan

+3

plain-ol-java per la vittoria! – ZhongYu

risposta

11

Se namesToWongoMap è una variabile di istanza, non è possibile davvero evitare un lambda catturante.

Si può ripulire il torrente suddividendo le operazioni un po 'più:

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

In questo modo non si chiama get due volte.

Questo è molto simile al ciclo for, ad eccezione, ad esempio, potrebbe teoricamente essere parallelizzato se namesToWongoMap non può essere mutato contemporaneamente.

Non mi piace l'aspetto filter di più in quanto significa dover creare strutture intermedie e passaggi multipli sui nomi List.

Non ci sono strutture intermedie e c'è solo una passata su List. Una pipeline di flusso dice "per ogni elemento ... fai questa sequenza di operazioni". Ogni elemento viene visitato una volta e la pipeline viene applicata.

Ecco alcune citazioni importanti dalla java.util.stream package description:

Un flusso non è una struttura di dati che memorizza gli elementi; invece, trasmette elementi da una sorgente come una struttura dati, un array, una funzione generatore o un canale I/O, attraverso una pipeline di operazioni computazionali.

I flussi di elaborazione pigramente consentono notevoli efficienze; in una pipeline come l'esempio di filtro-mappa-somma sopra, filtraggio, mappatura e sommatoria possono essere fusi in un singolo passaggio sui dati, con uno stato intermedio minimo.

+0

Bello. Semplice e pulito. Mi piace! – MadConan

+1

Penso che mi siederà su questo per un giorno o due e vedrò degli altri suonare. Mi interessa vedere se qualcuno può inventare qualcosa di meglio, anche se ne dubito. :) – MadConan

3

Un approccio non ho visto è 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()); 
} 

L'extra Map è un calo di prestazioni minime, dal momento che è solo la copia di puntatori a oggetti, non gli oggetti stessi.

+1

Weeelllll ... Penso che ci sia un * relativamente * alto ammontare di spese generali associate alla creazione di tutti i nodi per ogni voce. Uscendo da [questo vecchio articolo] (http://www.javacodegeeks.com/2010/08/java-best-practices-vector-arraylist.html). Ma la tua risposta è bella e pulita, e l'overhead potrebbe non essere un fattore. – MadConan

7

Radiodef's answer praticamente inchiodato, penso. La soluzione data c'è:

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

è probabilmente la migliore che si può fare in Java 8.

volevo parlare di una piccola ruga in questo, però. La chiamata Map.get restituisce null se il nome non è presente nella mappa e viene successivamente filtrato. Non c'è niente di sbagliato in questo di per se, sebbene cuoci la semantica null-significa-non-presente nella struttura della pipeline.

In un certo senso vorremmo un'operazione di pipeline mapper che ha una scelta di restituire zero o un elemento. Un modo per farlo con i flussi è con flatMap. La funzione flatmapper può restituire un numero arbitrario di elementi nello stream, ma in questo caso vogliamo solo zero o uno. Ecco come fare:

Ammetto che questo è piuttosto goffo e quindi non consiglierei di farlo. Un approccio leggermente migliore, ma un po 'oscuro è questo:

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

ma non sono ancora sicuro io consiglierei questo così com'è.

L'uso di flatMap indica comunque un altro approccio. Se si dispone di una politica più complessa su come gestire il caso non presente, è possibile effettuare il refactoring in una funzione di supporto che restituisce un flusso contenente il risultato o uno stream vuoto se non ci sono risultati.

Infine, JDK 9 - ancora in fase di sviluppo stesura di questo documento - ha aggiunto Stream.ofNullable che è utile in esattamente queste situazioni:

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

Per inciso, JDK 9 ha anche aggiunto che crea un Optional.stream flusso zero o uno da Optional. Ciò è utile nei casi in cui si desidera chiamare una funzione di ritorno opzionale all'interno di flatMap. Vedi this answer e this answer per ulteriori discussioni.

+2

Ho già aggiunto [StreamEx.ofNullable (obj)] (http://amaembo.github.io/streamex/javadoc/javax/util/streamex/StreamEx.html#ofNullable-T-) e [StreamEx.of (Opzionale) ] (http: //amaembo.github.io/streamex/javadoc/javax/util/streamex/StreamEx.html # of-java.util.Optional-) alla mia libreria. In realtà anche senza librerie di terze parti e passando a JDK9 chiunque può creare metodi statici simili in alcune classi di utilità specifiche del progetto e usarle. –

Problemi correlati