2016-06-24 38 views
17

Quello che voglio fare è mostrato di seguito in 2 chiamate di streaming. Voglio dividere una raccolta in 2 nuove collezioni basate su alcune condizioni. Idealmente, voglio farlo in 1. Ho visto condizioni utilizzate per la funzione .map dei flussi, ma non ho trovato nulla per il forEach. Qual è il modo migliore per ottenere ciò che voglio?Come utilizzare la logica if-else in Java 8 stream forEach

animalMap.entrySet().stream() 
      .filter(pair-> pair.getValue() != null) 
      .forEach(pair-> myMap.put(pair.getKey(), pair.getValue())); 

    animalMap.entrySet().stream() 
      .filter(pair-> pair.getValue() == null) 
      .forEach(pair-> myList.add(pair.getKey())); 
+1

Sembra una situazione in cui gli stream non ti stanno facendo alcun favore. Si nasconde semplicemente la sintassi del flusso di controllo con le API in un modo che risulta essere scomodo e il tuo 'forEach' lambda è di stato. – Radiodef

risposta

28

Basta inserire la condizione nella lambda stessa, ad es.

animalMap.entrySet().stream() 
     .forEach(
       pair -> { 
        if (pair.getValue() != null) { 
         myMap.put(pair.getKey(), pair.getValue()); 
        } else { 
         myList.add(pair.getKey()); 
        } 
       } 
     ); 

Naturalmente, ciò presuppone che entrambe le collezioni (myMap e myList) vengono dichiarati e inizializzati prima del pezzo di codice sopra.


Aggiornamento: utilizzando Map.forEach rende il codice più corto, più efficiente e leggibile, come Jorn Vernee gentilmente suggerito:

animalMap.forEach(
      (key, value) -> { 
       if (value != null) { 
        myMap.put(key, value); 
       } else { 
        myList.add(key); 
       } 
      } 
    ); 
+3

Si potrebbe usare '' 'Map.forEach''' invece, sarebbe un po 'più conciso. –

+1

@JornVernee, grazie mille per il suggerimento. –

+1

Puoi anche usare le parentesi '{...}' nell'espressione lambda, se è più di un semplice ternario in grado di gestire. – dcsohl

3

Il problema utilizzando stream().forEach(..) con una chiamata a add o put all'interno della forEach (in modo da mutare l'istanza esterna myMap o myList) è possibile eseguire facilmente problemi di concorrenza se qualcuno trasforma lo stream in parallelo a e la collezione che stai modificando non è thread-safe.

Un approccio che è possibile eseguire è la prima partizione delle voci nella mappa originale. Una volta ottenuto ciò, prendi l'elenco corrispondente di voci e raccoglile nella mappa e nell'elenco appropriato.

Map<Boolean, List<Map.Entry<K, V>>> partitions = 
    animalMap.entrySet() 
      .stream() 
      .collect(partitioningBy(e -> e.getValue() == null)); 

Map<K, V> myMap = 
    partitions.get(false) 
       .stream() 
       .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); 

List<K> myList = 
    partitions.get(true) 
       .stream() 
       .map(Map.Entry::getKey) 
       .collect(toList()); 

... o se si desidera farlo in un solo passaggio, implementare una collezione personalizzata (assumendo una esiste Tuple2<E1, E2> classe, è possibile creare il proprio), ad esempio:

public static <K,V> Collector<Map.Entry<K, V>, ?, Tuple2<Map<K, V>, List<K>>> customCollector() { 
    return Collector.of(
      () -> new Tuple2<>(new HashMap<>(), new ArrayList<>()), 
      (pair, entry) -> { 
       if(entry.getValue() == null) { 
        pair._2.add(entry.getKey()); 
       } else { 
        pair._1.put(entry.getKey(), entry.getValue()); 
       } 
      }, 
      (p1, p2) -> { 
       p1._1.putAll(p2._1); 
       p1._2.addAll(p2._2); 
       return p1; 
      }); 
} 

con la sua utilizzo:

Tuple2<Map<K, V>, List<K>> pair = 
    animalMap.entrySet().parallelStream().collect(customCollector()); 

È possibile ottimizzarlo di più se lo si desidera, ad esempio fornendo un predicato come parametro.

3

Nella maggior parte dei casi, quando si utilizza forEach su un flusso, è necessario riconsiderare se si sta utilizzando lo strumento giusto per il proprio lavoro o se lo si sta utilizzando nel modo corretto.

In genere, è necessario cercare un'operazione di terminale appropriata che esegua ciò che si desidera ottenere o per un servizio di raccolta appropriato. Ora, ci sono i collezionisti per produrre Map se List s, ma nessun collettore pronto all'uso per combinare due diversi raccoglitori, in base a un predicato.

Ora, this answer contiene un raccoglitore per la combinazione di due collettori. Usando questo collettore, è possibile ottenere l'attività come

Pair<Map<KeyType, Animal>, List<KeyType>> pair = animalMap.entrySet().stream() 
    .collect(conditional(entry -> entry.getValue() != null, 
      Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue), 
      Collectors.mapping(Map.Entry::getKey, Collectors.toList()))); 
Map<KeyType,Animal> myMap = pair.a; 
List<KeyType> myList = pair.b; 

Ma forse, si può risolvere questo compito specifico in modo più semplice. Uno di voi risultati corrisponde al tipo di input; è la stessa mappa appena spogliata delle voci che mappano a null.Se la vostra mappa originale è mutevole e non è necessario che in seguito, si può solo raccogliere la lista e rimuovere questi tasti dalla mappa originale in quanto si escludono a vicenda:

List<KeyType> myList=animalMap.entrySet().stream() 
    .filter(pair -> pair.getValue() == null) 
    .map(Map.Entry::getKey) 
    .collect(Collectors.toList()); 

animalMap.keySet().removeAll(myList); 

Si noti che è possibile rimuovere i mapping di null anche senza avere l'elenco delle altre chiavi:

animalMap.values().removeIf(Objects::isNull); 

o

animalMap.values().removeAll(Collections.singleton(null)); 

Se non puoi (o non vuoi) modificare la mappa originale, c'è ancora una soluzione senza un raccoglitore personalizzato. Come accennato in Alexis C.’s answer, partitioningBy sta andando nella giusta direzione, ma si può semplificare:

Map<Boolean,Map<KeyType,Animal>> tmp = animalMap.entrySet().stream() 
    .collect(Collectors.partitioningBy(pair -> pair.getValue() != null, 
       Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); 
Map<KeyType,Animal> myMap = tmp.get(true); 
List<KeyType> myList = new ArrayList<>(tmp.get(false).keySet()); 

La linea di fondo è, non dimenticare le operazioni di raccolta ordinaria, non c'è bisogno di fare tutto con la nuova API Stream.

+0

Holger, non sei d'accordo che la tua soluzione è sicuramente meno leggibile di quella accettata? –

+2

@Marco Altieri: dipende dalla domanda reale. chiesto all '* Stream API *, che la risposta accettata in realtà non risponde, come alla fine, 'forEach' è solo una sintassi alternativa per un ciclo' for'. Ad esempio, la variante 'Map.forEach (...)' non può essere eseguita in parallelo, la variante 'entrySet(). Stream(). ForEach (...)' si interromperà bruscamente quando viene eseguita in parallelo. Quando vuoi usare l'API Stream e capire come usarlo correttamente, devi andare con la risposta di Alexis C o la mia. Una volta capito, non ti sembrerà illeggibile ... – Holger

0

penso che sia possibile in Java 9: ​​

animalMap.entrySet().stream() 
       .forEach(
         pair -> Optional.ofNullable(pair.getValue()) 
           .ifPresentOrElse(v -> myMap.put(pair.getKey(), v), v -> myList.add(pair.getKey()))) 
       ); 

necessario l'ifPresentOrElse per farlo funzionare però. (Penso che un ciclo for abbia un aspetto migliore.)

Problemi correlati