Wie überprüfe ich, ob ein Java 8-Stream leer ist?

93

Wie kann ich überprüfen, ob a Streamleer ist, und eine Ausnahme auslösen, wenn dies nicht der Fall ist, als nicht-terminale Operation?

Grundsätzlich suche ich nach etwas, das dem folgenden Code entspricht, ohne jedoch den dazwischen liegenden Stream zu materialisieren. Insbesondere sollte die Überprüfung nicht erfolgen, bevor der Stream tatsächlich von einer Terminaloperation verbraucht wird.

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();
}
Kopffüßer
quelle
22
Sie können Ihren Kuchen nicht haben und ihn auch nicht essen - und das in diesem Zusammenhang buchstäblich. Sie müssen den Stream verbrauchen , um herauszufinden, ob er leer ist. Das ist der Punkt von Streams Semantik (Faulheit).
Marko Topolnik
Es wird irgendwann verbraucht, an diesem Punkt sollte die Überprüfung stattfinden
Kopffüßer
10
Um zu überprüfen, ob der Stream nicht leer ist, müssen Sie versuchen, mindestens ein Element zu verwenden. Zu diesem Zeitpunkt hat der Stream seine "Jungfräulichkeit" verloren und kann von Anfang an nicht mehr konsumiert werden.
Marko Topolnik

Antworten:

23

Wenn Sie mit eingeschränkten parallelen Funktionen leben können, funktioniert die folgende Lösung:

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

Hier ist ein Beispielcode, der ihn verwendet:

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

Das Problem bei der (effizienten) parallelen Ausführung besteht darin, dass für die Unterstützung der Aufteilung des SpliteratorThreads eine threadsichere Methode erforderlich ist, um festzustellen, ob eines der Fragmente einen threadsicheren Wert aufweist. Dann muss das letzte der ausgeführten Fragmente tryAdvanceerkennen, dass es das letzte ist (und es konnte auch nicht vorrücken), das die entsprechende Ausnahme auslöst. Daher habe ich hier keine Unterstützung für die Aufteilung hinzugefügt.

Holger
quelle
33

Die anderen Antworten und Kommentare sind insofern richtig, als man zum Untersuchen des Inhalts eines Streams eine Terminaloperation hinzufügen muss, wodurch der Stream "verbraucht" wird. Sie können dies jedoch tun und das Ergebnis wieder in einen Stream umwandeln, ohne den gesamten Inhalt des Streams zu puffern. Hier einige Beispiele:

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

Verwandeln Sie den Stream in einen Iterator, um ihn aufzurufen hasNext(), und wenn dies zutrifft, verwandeln Sie den IteratorRücken in einen Stream. Dies ist insofern ineffizient, als alle nachfolgenden Operationen auf dem Stream die Iteratoren hasNext()und next()Methoden durchlaufen , was auch impliziert, dass der Stream effektiv sequentiell verarbeitet wird (selbst wenn er später parallel gedreht wird). Auf diese Weise können Sie den Stream jedoch testen, ohne alle Elemente zu puffern.

Es gibt wahrscheinlich eine Möglichkeit, dies mit a Spliteratoranstelle von a zu tun Iterator. Dies ermöglicht möglicherweise, dass der zurückgegebene Stream dieselben Eigenschaften wie der Eingabestream aufweist, einschließlich der parallelen Ausführung.

Stuart Marks
quelle
1
Ich glaube nicht , dass es eine wartbare Lösung, die eine effiziente Parallelverarbeitung unterstützen würde , wie es schwer ist , Spaltung zu unterstützen, jedoch mit estimatedSizeund characteristicskönnte sogar Single-Thread - Performance verbessern. Es ist einfach passiert, dass ich die SpliteratorLösung geschrieben habe, während Sie die IteratorLösung veröffentlicht haben…
Holger
3
Sie können den Stream nach einem Spliterator fragen, tryAdvance (Lambda) aufrufen, in dem Ihr Lambda alles erfasst, was an ihn übergeben wurde, und dann einen Spliterator zurückgeben, der fast alles an den zugrunde liegenden Spliterator delegiert, außer dass das erste Element wieder auf den ersten Block geklebt wird ( und korrigiert das Ergebnis von EstimateSize).
Brian Goetz
1
@BrianGoetz Ja, das war mein Gedanke, ich habe mich nur noch nicht darum gekümmert, all diese Details zu verarbeiten.
Stuart Marks
3
@ Brian Goetz: Das habe ich mit "zu kompliziert" gemeint. Wenn Sie tryAdvancevor dem StreamDo anrufen , wird die Faulheit des Streamzu einem „teilweise faulen“ Stream. Dies bedeutet auch, dass die Suche nach dem ersten Element keine parallele Operation mehr ist, da Sie zuerst teilen und tryAdvancedie geteilten Teile gleichzeitig ausführen müssen, um eine echte parallele Operation durchzuführen, soweit ich verstanden habe. Wenn der einzige Terminalbetrieb findAnyoder ähnlich ist, würde dies die gesamte parallel()Anforderung zerstören .
Holger
2
Für eine vollständige parallele Unterstützung müssen Sie also nicht tryAdvancevor dem Stream aufrufen und müssen jeden geteilten Teil in einen Proxy packen und die "hasAny" -Informationen aller gleichzeitigen Vorgänge selbst sammeln und sicherstellen, dass der letzte gleichzeitige Vorgang die gewünschte Ausnahme auslöst, wenn der Strom war leer. Viele Sachen…
Holger
15

Sie müssen eine Terminaloperation für den Stream ausführen, damit einer der Filter angewendet wird. Daher können Sie nicht wissen, ob es leer sein wird, bis Sie es verbrauchen.

Das Beste, was Sie tun können, ist, den Stream mit einer findAny()Terminaloperation zu beenden , die beendet wird, wenn ein Element gefunden wird. Wenn jedoch keines vorhanden ist, muss die gesamte Eingabeliste durchlaufen werden, um dies herauszufinden.

Dies würde Ihnen nur helfen, wenn die Eingabeliste viele Elemente enthält und einer der ersten die Filter durchläuft, da nur eine kleine Teilmenge der Liste verbraucht werden müsste, bevor Sie wissen, dass der Stream nicht leer ist.

Natürlich müssen Sie noch einen neuen Stream erstellen, um die Ausgabeliste zu erstellen.

Eran
quelle
7
Es gibt anyMatch(alwaysTrue()), ich denke , das ist in der Nähe von hasAny.
Marko Topolnik
1
@MarkoTopolnik Ich habe gerade die Referenz überprüft - was ich im Sinn hatte, war findAny (), obwohl anyMatch () auch funktionieren würde.
Eran
3
anyMatch(alwaysTrue())passt perfekt zu der beabsichtigten Semantik von dir hasAnyund gibt dir ein booleanstatt Optional<T>--- aber wir spalten hier die Haare :)
Marko Topolnik
1
Hinweis alwaysTrueist ein Guaven-Prädikat.
Jean-François Savard
10
anyMatch(e -> true)dann.
FBB
15

Dies kann in vielen Fällen ausreichend sein

stream.findAny().isPresent()
Kenglxn
quelle
5

Ich denke, sollte ausreichen, um einen Booleschen Wert abzubilden

Im Code ist dies:

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
quelle
1
Wenn dies alles ist, was Sie brauchen, ist Kenglxns Antwort besser.
Dominykas Mostauskis
es ist nutzlos, es dupliziert Collection.isEmpty ()
Krzysiek
@Krzysiek Es ist nicht nutzlos, wenn Sie die Sammlung filtern müssen. Ich stimme Dominykas jedoch darin zu, dass die Antwort von
Kenglxn
Es ist, weil es auch dupliziertStream.anyMatch()
Krzysiek
4

Nach Stuarts Idee könnte dies folgendermaßen geschehen 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;
    }
}

Ich denke, dies funktioniert mit parallelen Streams, da der stream.spliterator()Vorgang den Stream beendet und ihn dann nach Bedarf neu erstellt

In meinem Anwendungsfall benötigte ich einen Standardwert Streamanstelle eines Standardwerts. Das ist ziemlich einfach zu ändern, wenn dies nicht das ist, was Sie brauchen

phoenix7360
quelle
Ich kann nicht herausfinden, ob dies die Leistung bei parallelen Streams erheblich beeinträchtigen würde. Sollte es wahrscheinlich testen, wenn dies eine Anforderung ist
phoenix7360
Tut mir leid, ich wusste nicht, dass @Holger auch eine Lösung hatte. SpliteratorIch frage mich, wie die beiden sich vergleichen.
Phoenix 7360