2015-05-28 4 views
9

Próbuję zebrać strumień wyrzucać rzadko używane przedmioty jak w poniższym przykładzie:Collect strumień z działalności grupy, liczenia i filtrowania

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")); 
} 

Mam wrażenie, że jest to możliwe do zrobienia jest dużo prostsze - czy mam rację?

+1

Nie, to wygląda na najprostszy sposób na zrobienie tego. –

+0

@LouisWasserman Ale to jest tak brzydka konstrukcja z pośrednią "Mapą" w nim. – ytterrr

+0

Tak? Musi być coś w rodzaju mapy w implementacji, aby wykonać liczenie. Nie da się tego obejść. –

Odpowiedz

3

Jakiś czas temu wrote eksperymentalny distinct(atLeast) metoda dla mojej bibliotece:

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

Chodziło o to, aby używać go tak:

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

Wykonuje stanową filtrację, która wygląda trochę brzydko. Wątpiłem, czy taka funkcja jest przydatna, więc nie połączyłem jej z główną gałęzią. Mimo to wykonuje zadanie w trybie pojedynczego strumienia. Prawdopodobnie powinienem ją ożywić. Tymczasem można skopiować ten kod do metody statycznej i używać go tak:

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

Update (2015/05/31): dodałem metodę distinct(atLeast) do StreamEx 0.3.1. Zaimplementowano go za pomocą custom spliterator. Testy porównawcze wykazały, że ta implementacja jest znacznie szybsza w przypadku strumieni sekwencyjnych niż filtrowanie stanowe opisane powyżej, aw wielu przypadkach jest również szybsza niż inne rozwiązania proponowane w tym temacie. Działa również ładnie, jeśli napotkano w strumieniu null (kolekcjoner groupingBy nie obsługuje klasy null, więc rozwiązania oparte na groupingBy zawiedzie, jeśli napotkane zostanie null).

+3

Dlaczego nie 'Long :: sum'? – Holger

+1

@Holger, dzięki, [użyje] (https://github.com/amaembo/streamex/commit/8cc7f632c87affdd0701847df41a778abc79aef2) 'Long :: sum'. Chcę przetestować również wersję [custom spliterator] (https://github.com/amaembo/streamex/blob/8cc7f632c87affdd0701847df41a778abc79aef2/src/main/java/javax/util/streamex/DistinctSpliterator.java): jest to nieco dłuższa implementacja, ale dodaje możliwość użycia prostego 'HashMap' do sekwencyjnych strumieni (jeśli' trySplit' nie został wywołany), może działać szybciej. –

+1

Czy profilowałeś swoje rozwiązanie? Nie zgadłbym, które wyższe koszty, niezaplanowana synchronizacja operacji 'ConcurrentHashMap.merge' lub * unbox, unbox, sum, box * dla prawie każdego elementu. Tylko profiler może powiedzieć ... – Holger

6

Nie ma sposobu na stworzenie Map, chyba że chcesz zaakceptować bardzo wysoką złożoność procesora.

Można jednak usunąć operację sekundcollect:

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

pamiętać, że w Javie 8 HashSet nadal okłady HashMap, więc używając keySet() o HashMap, gdy chcesz Set w pierwsze miejsce, nie marnuje miejsca przy obecnej implementacji.

Oczywiście, można ukryć post-processing w Collector jeśli czuje się bardziej „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

Ściśle mówiąc możesz nadal marnować przestrzeń na przydzielone obiekty 'Long' (jeśli którekolwiek z twoich zliczeń przekracza' 127'). –

+1

Również jeśli masz dość rzadkich przedmiotów, które są odfiltrowane przez 'removeIf', możesz skończyć z bardzo dużą tablicą asocjacyjną, która nie zostanie zmniejszona automatycznie. –

+1

@Tagir Valeev: oba można rozwiązać za pomocą spersonalizowanej implementacji 'Map', jednak pytanie brzmi, w jaki sposób później używany jest' powszechnie używany' 'zestaw', może to wcale nie jest problemem. – Holger

0

ja osobiście wolę rozwiązania Holger'S (+1), ale zamiast usuwania elementów z mapy groupingBy, chciałbym filtr jego entrySet i mapa wynik do ustawionego w finalizatora (czuje się jeszcze bardziej streamy do mnie)

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