2015-05-28 9 views
9

Sto cercando di raccogliere flusso di buttare via gli oggetti usati raramente come in questo esempio:flusso Raccogliere con le operazioni di raggruppamento, di conteggio e filtraggio

import java.util.*; 
import java.util.function.Function; 
import static java.util.stream.Collectors.*; 
import static org.hamcrest.MatcherAssert.assertThat; 
import static org.hamcrest.Matchers.containsInAnyOrder; 
import org.junit.Test; 

@Test 
public void shouldFilterCommonlyUsedWords() { 
    // given 
    List<String> allWords = Arrays.asList(
     "call", "feel", "call", "very", "call", "very", "feel", "very", "any"); 

    // when 
    Set<String> commonlyUsed = allWords.stream() 
      .collect(groupingBy(Function.identity(), counting())) 
      .entrySet().stream().filter(e -> e.getValue() > 2) 
      .map(Map.Entry::getKey).collect(toSet()); 

    // then 
    assertThat(commonlyUsed, containsInAnyOrder("call", "very")); 
} 

ho la sensazione che sia possibile farlo molto più semplice - ho ragione?

+1

No, sembra il modo più semplice per farlo. –

+0

@LouisWasserman Ma è una costruzione così brutta con la 'Mappa' intermedia in esso. – ytterrr

+0

Quindi? Dovrà essere qualcosa come una mappa nell'implementazione per fare il conteggio. Non c'è modo di aggirare questo. –

risposta

3

Qualche tempo fa ho wrote un distinct(atLeast) metodo sperimentale per la mia biblioteca:

public StreamEx<T> distinct(long atLeast) { 
    if (atLeast <= 1) 
     return distinct(); 
    AtomicLong nullCount = new AtomicLong(); 
    ConcurrentHashMap<T, Long> map = new ConcurrentHashMap<>(); 
    return filter(t -> { 
     if (t == null) { 
      return nullCount.incrementAndGet() == atLeast; 
     } 
     return map.merge(t, 1L, (u, v) -> (u + v)) == atLeast; 
    }); 
} 

Quindi l'idea era quella di utilizzare in questo modo:

Set<String> commonlyUsed = StreamEx.of(allWords).distinct(3).toSet(); 

Questo esegue una filtrazione stateful, che sembra un po 'brutta. Dubitavo che tale funzionalità fosse utile, quindi non l'ho unita al master branch. Ciononostante fa il lavoro in pass di single stream. Probabilmente dovrei farlo rivivere. Nel frattempo è possibile copiare questo codice nel metodo statico e usarlo in questo modo:

Set<String> commonlyUsed = distinct(allWords.stream(), 3).collect(Collectors.toSet()); 

Aggiornamento (2015/05/31): ho aggiunto il metodo distinct(atLeast) al StreamEx 0.3.1. È implementato utilizzando custom spliterator. I benchmark hanno mostrato che questa implementazione è significativamente più veloce per i flussi sequenziali rispetto al filtro stateful descritto sopra e in molti casi è anche più veloce di altre soluzioni proposte in questo argomento. Funziona anche se lo stream è null (il collector groupingBy non supporta null come classe, quindi le soluzioni basate su groupingBy falliranno se si incontra null).

+3

Perché non 'Long :: sum'? – Holger

+1

@Holger, grazie, [userà] (https://github.com/amaembo/streamex/commit/8cc7f632c87affdd0701847df41a778abc79aef2) 'Long :: sum'. Voglio testare anche [custom spliterator] (https://github.com/amaembo/streamex/blob/8cc7f632c87affdd0701847df41a778abc79aef2/src/main/java/javax/util/streamex/DistinctSpliterator.java) versione: è un po 'più lunga implementazione, ma aggiunge la possibilità di usare semplicemente 'HashMap' per gli stream sequenziali (se' trySplit' non è stato chiamato), quindi potrebbe funzionare più velocemente. –

+1

Hai profilato la tua soluzione? Non vorrei fare un'ipotesi su quali costi di più, la sincronizzazione incontrastata di 'ConcurrentHashMap.merge' o l'operazione * unbox, unbox, sum, box * fatta per quasi ogni elemento. Solo un profiler può dire ... – Holger

6

Non c'è modo di creare un Map, a meno che non si desideri accettare una complessità della CPU molto elevata.

Tuttavia, è possibile rimuovere l'operazione secondocollect:

Map<String,Long> map = allWords.stream() 
    .collect(groupingBy(Function.identity(), HashMap::new, counting())); 
map.values().removeIf(l -> l<=2); 
Set<String> commonlyUsed=map.keySet(); 

Si noti che in Java 8, HashSet avvolge ancora un HashMap, in modo da utilizzare il keySet() di un HashMap, quando si desidera un Set nel il primo posto, non spreca spazio vista l'attuale implementazione.

Naturalmente, è possibile nascondere il post-processing in un Collector, se ci si sente più “streamy”:

Set<String> commonlyUsed = allWords.stream() 
    .collect(collectingAndThen(
     groupingBy(Function.identity(), HashMap::new, counting()), 
     map-> { map.values().removeIf(l -> l<=2); return map.keySet(); })); 
+1

A rigor di termini si può ancora perdere spazio per gli oggetti 'Long' assegnati (se uno dei tuoi conteggi supera' 127'). –

+1

Anche se hai molti oggetti rari che vengono filtrati con 'removeIf', potresti finire con una tabella hash molto grande che non verrà ridotta automaticamente. –

+1

@Tagir Valeev: entrambi potrebbero essere risolti con un'implementazione di 'Map' personalizzata, tuttavia, la domanda è come in seguito viene usato' comunementeUsato' Set', forse questo non è affatto un problema. – Holger

0

Io personalmente preferisco la soluzione di Holger (+1), ma, invece di rimuovere elementi dalla mappa groupingBy, vorrei filtro sua entrySet e mappa il risultato di un set nel finalizzatore (si sente ancora più streamy)

Set<String> commonlyUsed = allWords.stream().collect(
      collectingAndThen(
       groupingBy(identity(), counting()), 
       (map) -> map.entrySet().stream(). 
          filter(e -> e.getValue() > 2). 
          map(e -> e.getKey()). 
          collect(Collectors.toSet()))); 
Problemi correlati