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

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.


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.


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

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.


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

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


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

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.