Jak sprawdzić, czy strumień Java 8 jest pusty?

101

Jak mogę sprawdzić, czy a Streamjest puste i zgłosić wyjątek, jeśli tak nie jest, jako operacja nieterminalowa?

Zasadniczo szukam czegoś równoważnego z poniższym kodem, ale bez materializacji strumienia pomiędzy. W szczególności sprawdzenie nie powinno mieć miejsca przed faktycznym zużyciem strumienia przez operację terminala.

public Stream<Thing> getFilteredThings() {
    Stream<Thing> stream = getThings().stream()
                .filter(Thing::isFoo)
                .filter(Thing::isBar);
    return nonEmptyStream(stream, () -> {
        throw new RuntimeException("No foo bar things available")   
    });
}

private static <T> Stream<T> nonEmptyStream(Stream<T> stream, Supplier<T> defaultValue) {
    List<T> list = stream.collect(Collectors.toList());
    if (list.isEmpty()) list.add(defaultValue.get());
    return list.stream();
}
Głowonóg
źródło
23
Nie możesz mieć swojego ciasta i też go zjeść - i to całkiem dosłownie w tym kontekście. Musisz skonsumować strumień, aby dowiedzieć się, czy jest pusty. Na tym polega semantyka Stream (lenistwo).
Marko Topolnik
W końcu zostanie skonsumowany, w tym momencie powinno nastąpić sprawdzenie
głowonóg
12
Aby sprawdzić, czy strumień nie jest pusty, musisz spróbować zużyć co najmniej jeden element. W tym momencie strumień stracił swoje „dziewictwo” i nie może być ponownie skonsumowany od początku.
Marko Topolnik

Odpowiedzi:

24

Jeśli możesz żyć z ograniczonymi możliwościami równoległymi, zadziała następujące rozwiązanie:

private static <T> Stream<T> nonEmptyStream(
    Stream<T> stream, Supplier<RuntimeException> e) {

    Spliterator<T> it=stream.spliterator();
    return StreamSupport.stream(new Spliterator<T>() {
        boolean seen;
        public boolean tryAdvance(Consumer<? super T> action) {
            boolean r=it.tryAdvance(action);
            if(!seen && !r) throw e.get();
            seen=true;
            return r;
        }
        public Spliterator<T> trySplit() { return null; }
        public long estimateSize() { return it.estimateSize(); }
        public int characteristics() { return it.characteristics(); }
    }, false);
}

Oto przykładowy kod, który go używa:

List<String> l=Arrays.asList("hello", "world");
nonEmptyStream(l.stream(), ()->new RuntimeException("No strings available"))
  .forEach(System.out::println);
nonEmptyStream(l.stream().filter(s->s.startsWith("x")),
               ()->new RuntimeException("No strings available"))
  .forEach(System.out::println);

Problem z (wydajnym) wykonywaniem równoległym polega na tym, że obsługa dzielenia Spliteratorwymaga bezpiecznego wątkowo sposobu, aby zauważyć, czy którykolwiek z fragmentów widział jakąkolwiek wartość w sposób bezpieczny dla wątków. Następnie ostatni z wykonywanych fragmentów tryAdvancemusi zdać sobie sprawę, że jest ostatnim (i też nie mógł przejść dalej), który rzuci odpowiedni wyjątek. Więc nie dodałem tutaj obsługi dzielenia.

Holger
źródło
33

Pozostałe odpowiedzi i komentarze są poprawne, ponieważ aby zbadać zawartość strumienia, należy dodać operację terminalową, tym samym „konsumując” strumień. Można to jednak zrobić i zamienić wynik z powrotem w strumień, bez buforowania całej zawartości strumienia. Oto kilka przykładów:

static <T> Stream<T> throwIfEmpty(Stream<T> stream) {
    Iterator<T> iterator = stream.iterator();
    if (iterator.hasNext()) {
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false);
    } else {
        throw new NoSuchElementException("empty stream");
    }
}

static <T> Stream<T> defaultIfEmpty(Stream<T> stream, Supplier<T> supplier) {
    Iterator<T> iterator = stream.iterator();
    if (iterator.hasNext()) {
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false);
    } else {
        return Stream.of(supplier.get());
    }
}

Zasadniczo zamień strumień w strumień, Iteratoraby go wywołać hasNext(), a jeśli to prawda, zamień Iteratortył w plik Stream. Jest to nieefektywne, ponieważ wszystkie kolejne operacje na strumieniu będą przechodzić przez Iterator hasNext()i next()metody, co również oznacza, że ​​strumień jest skutecznie przetwarzany sekwencyjnie (nawet jeśli później zostanie włączony równolegle). Pozwala to jednak przetestować strumień bez buforowania wszystkich jego elementów.

Prawdopodobnie istnieje sposób, aby to zrobić, używając Spliteratorzamiast Iterator. Potencjalnie umożliwia to, aby zwrócony strumień miał takie same cechy jak strumień wejściowy, w tym działanie równoległe.

Stuart Marks
źródło
1
Nie sądzę, aby istniało możliwe w utrzymaniu rozwiązanie, które wspierałoby wydajne przetwarzanie równoległe, ponieważ trudno jest wspierać dzielenie, jednak mając, estimatedSizea characteristicsnawet może poprawić wydajność jednowątkową. Tak się złożyło, że napisałem Spliteratorrozwiązanie, kiedy publikowałeś Iteratorrozwiązanie…
Holger
3
Możesz poprosić strumień o Spliterator, wywołać tryAdvance (lambda), gdzie twoja lambda przechwytuje wszystko, co zostało do niej przekazane, a następnie zwrócić Spliterator, który deleguje prawie wszystko do bazowego Spliteratora, z wyjątkiem tego, że przykleja pierwszy element z powrotem do pierwszego fragmentu ( i naprawia wynik estimateSize).
Brian Goetz
1
@BrianGoetz Tak, taka była moja myśl, po prostu jeszcze nie zadałem sobie trudu, aby przejść przez całą pracę związaną z obsługą tych wszystkich szczegółów.
Stuart Marks
3
@Brian Goetz: To właśnie miałem na myśli mówiąc „zbyt skomplikowane”. Wołanie tryAdvanceprzedtem Streamzamienia leniwą naturę Streamw „częściowo leniwy” strumień. Oznacza to również, że wyszukiwanie pierwszego elementu nie jest już operacją równoległą, ponieważ musisz najpierw podzielić się i jednocześnie wykonać tryAdvancena podzielonych częściach, aby wykonać prawdziwą operację równoległą, o ile rozumiem. Jeśli jedyna operacja terminalowa jest findAnylub podobna, spowoduje to zniszczenie całego parallel()żądania.
Holger
2
Tak więc, aby uzyskać pełną obsługę równoległą, nie możesz wywoływać tryAdvanceprzed wykonaniem strumienia i musisz zawijać każdą podzieloną część do serwera proxy i samodzielnie zbierać informacje „hasAny” wszystkich operacji współbieżnych i upewnić się, że ostatnia operacja współbieżna zgłosi żądany wyjątek strumień był pusty. Dużo rzeczy…
Holger,
26

W wielu przypadkach może to być wystarczające

stream.findAny().isPresent()
kenglxn
źródło
15

Aby zastosować którykolwiek z filtrów, musisz wykonać operację terminalową na strumieniu. Dlatego nie możesz wiedzieć, czy będzie pusty, dopóki go nie skonsumujesz.

Najlepsze, co możesz zrobić, to zakończyć strumień findAny()operacją terminalową, która zatrzyma się, gdy znajdzie dowolny element, ale jeśli nie ma żadnego, będzie musiał iterować po całej liście wejściowej, aby to sprawdzić.

Pomogłoby to tylko wtedy, gdy lista wejściowa zawiera wiele elementów, a jeden z kilku pierwszych przechodzi przez filtry, ponieważ tylko niewielki podzbiór listy musiałby zostać wykorzystany, zanim zorientujesz się, że strumień nie jest pusty.

Oczywiście nadal będziesz musiał utworzyć nowy strumień, aby utworzyć listę wyników.

Eran
źródło
7
Jest anyMatch(alwaysTrue()), myślę, że jest najbliżej hasAny.
Marko Topolnik
1
@MarkoTopolnik Właśnie sprawdziłem odniesienie - miałem na myśli findAny (), chociaż anyMatch () też by działało.
Eran
3
anyMatch(alwaysTrue())idealnie pasuje do zamierzonej semantyki twojej hasAny, dając ci booleanzamiast Optional<T>--- ale tutaj dzielimy włosy :)
Marko Topolnik
1
Uwaga alwaysTrueto predykat guawy.
Jean-François Savard
11
anyMatch(e -> true)następnie.
FBB
6

Myślę, że powinno wystarczyć do zmapowania wartości logicznej

W kodzie to jest:

boolean isEmpty = anyCollection.stream()
    .filter(p -> someFilter(p)) // Add my filter
    .map(p -> Boolean.TRUE) // For each element after filter, map to a TRUE
    .findAny() // Get any TRUE
    .orElse(Boolean.FALSE); // If there is no match return false
Luis Roberto
źródło
1
Jeśli to wszystko, czego potrzebujesz, odpowiedź kenglxn jest lepsza.
Dominykas Mostauskis
jest bezużyteczny, powiela Collection.isEmpty ()
Krzysiek
@Krzysiek nie jest bezużyteczne, jeśli chcesz przefiltrować kolekcję. Jednak zgadzam się z Dominykasem, że odpowiedź kenglxn jest lepsza
Hertzu
To dlatego, że też się powielaStream.anyMatch()
Krzysiek
4

Zgodnie z pomysłem Stuarta można to zrobić w następujący sposób Spliterator:

static <T> Stream<T> defaultIfEmpty(Stream<T> stream, Stream<T> defaultStream) {
    final Spliterator<T> spliterator = stream.spliterator();
    final AtomicReference<T> reference = new AtomicReference<>();
    if (spliterator.tryAdvance(reference::set)) {
        return Stream.concat(Stream.of(reference.get()), StreamSupport.stream(spliterator, stream.isParallel()));
    } else {
        return defaultStream;
    }
}

Myślę, że działa to z równoległymi strumieniami, ponieważ stream.spliterator()operacja zakończy strumień, a następnie przebuduje go zgodnie z wymaganiami

W moim przypadku potrzebowałem wartości domyślnej, Streama nie domyślnej. to jest dość łatwe do zmiany, jeśli nie tego potrzebujesz

phoenix7360
źródło
Nie wiem, czy miałoby to znaczący wpływ na wydajność przy równoległych strumieniach. Powinienem prawdopodobnie przetestować, jeśli jest to wymagane
phoenix7360
Przepraszam, że nie zdawałem sobie sprawy, że @Holger również miał rozwiązanie Spliteratori zastanawiam się, jak te dwa są porównane.
phoenix7360
0

Po prostu użyłbym:

stream.count()>0
daniel sp
źródło