Come verificare se un Java 8 Stream è vuoto?


95

Come posso verificare se a Streamè vuoto e lanciare un'eccezione in caso contrario, come operazione non terminale?

Fondamentalmente, sto cercando qualcosa di equivalente al codice seguente, ma senza materializzare il flusso intermedio. In particolare, il controllo non dovrebbe avvenire prima che il flusso venga effettivamente consumato da un'operazione del terminale.

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
Non puoi avere la tua torta e mangiarla anche tu - e letteralmente è così, in questo contesto. Devi consumare il flusso per scoprire se è vuoto. Questo è il punto della semantica di Stream (pigrizia).
Marko Topolnik

Alla fine verrà consumato, a questo punto dovrebbe avvenire il controllo
Cephalopod

11
Per verificare che lo stream non sia vuoto devi provare a consumare almeno un elemento. A quel punto il torrente ha perso la sua "verginità" e non può essere più consumato dall'inizio.
Marko Topolnik

Risposte:


24

Se riesci a convivere con capacità parallele limitate, la seguente soluzione funzionerà:

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

Ecco un esempio di codice che lo utilizza:

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

Il problema con l'esecuzione parallela (efficiente) è che supportare la suddivisione di Spliteratorrichiede un modo thread-safe per notare se uno dei frammenti ha visto un valore in modo thread-safe. Quindi l'ultimo dei frammenti in esecuzione tryAdvancedeve rendersi conto che è l'ultimo (e nemmeno può avanzare) a lanciare l'eccezione appropriata. Quindi non ho aggiunto il supporto per la divisione qui.


33

Le altre risposte e commenti sono corretti in quanto per esaminare il contenuto di uno stream, è necessario aggiungere un'operazione da terminale, in tal modo "consumando" lo stream. Tuttavia, è possibile farlo e trasformare il risultato in un flusso, senza eseguire il buffering dell'intero contenuto del flusso. Ecco un paio di esempi:

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

Fondamentalmente trasforma il flusso in un Iteratorper richiamarlo hasNext()e, se vero, trasforma il Iteratorretro in un Stream. Ciò è inefficiente in quanto tutte le operazioni successive sullo stream passeranno attraverso i metodi hasNext()e di Iterator next(), il che implica anche che lo stream viene effettivamente elaborato in modo sequenziale (anche se successivamente viene trasformato in parallelo). Tuttavia, ciò ti consente di testare il flusso senza eseguire il buffering di tutti i suoi elementi.

Probabilmente c'è un modo per farlo usando a Spliteratorinvece di Iterator. Ciò consente potenzialmente al flusso restituito di avere le stesse caratteristiche del flusso di input, inclusa l'esecuzione in parallelo.


1
Non penso che esista una soluzione gestibile che supporti un'elaborazione parallela efficiente poiché è difficile supportare la suddivisione, tuttavia avendo estimatedSizee characteristicspotrebbe persino migliorare le prestazioni a thread singolo. È appena successo che ho scritto la Spliteratorsoluzione mentre stavi postando la Iteratorsoluzione ...
Holger

3
Puoi chiedere al flusso uno Spliterator, chiamare tryAdvance (lambda) dove il tuo lambda acquisisce tutto ciò che gli è passato, e quindi restituire uno Spliterator che delega quasi tutto allo Spliterator sottostante, tranne che incolla il primo elemento sul primo blocco ( e corregge il risultato di estimSize).
Brian Goetz

1
@BrianGoetz Sì, questo era il mio pensiero, solo che non mi sono ancora preso la briga di passare attraverso il lavoro di gamba della gestione di tutti quei dettagli.
Stuart Marks

3
@Brian Goetz: Questo è quello che intendevo con "troppo complicato". Chiamare tryAdvanceprima del Streamfa trasforma la natura pigra di Streamin un flusso "parzialmente pigro". Implica anche che la ricerca del primo elemento non sia più un'operazione parallela poiché devi prima dividere e fare contemporaneamente tryAdvancesulle parti divise per fare una vera operazione parallela, per quanto ho capito. Se l'unica operazione del terminale è findAnyo simile, l'intera parallel()richiesta verrà distrutta .
Holger

2
Quindi, per un supporto parallelo completo, non devi chiamare tryAdvanceprima dello stream e devi avvolgere ogni parte divisa in un proxy e raccogliere le informazioni "hasAny" di tutte le operazioni simultanee da solo e assicurarti che l'ultima operazione simultanea generi l'eccezione desiderata se il il flusso era vuoto. Un sacco di cose ...
Holger

19

Questo può essere sufficiente in molti casi

stream.findAny().isPresent()

15

È necessario eseguire un'operazione del terminale sullo Stream per applicare uno qualsiasi dei filtri. Quindi non puoi sapere se sarà vuoto finché non lo consumi.

La cosa migliore che puoi fare è terminare lo Stream con un'operazione da findAny()terminale, che si fermerà quando trova un elemento, ma se non ce ne sono, dovrà iterare su tutto l'elenco di input per scoprirlo.

Questo ti aiuterebbe solo se l'elenco di input ha molti elementi e uno dei primi passa i filtri, poiché solo un piccolo sottoinsieme dell'elenco dovrebbe essere consumato prima che tu sappia che lo Stream non è vuoto.

Ovviamente dovrai comunque creare un nuovo Stream per produrre l'elenco di output.


7
C'è anyMatch(alwaysTrue()), penso che sia il più vicino a hasAny.
Marko Topolnik

1
@MarkoTopolnik Ho appena controllato il riferimento: quello che avevo in mente era findAny (), sebbene anche anyMatch () avrebbe funzionato.
Eran

3
anyMatch(alwaysTrue())corrisponde perfettamente alla tua semantica prevista hasAny, dandoti un booleaninvece di Optional<T>--- ma qui stiamo dividendo i capelli :)
Marko Topolnik

1
La nota alwaysTrueè un predicato Guava.
Jean-François Savard

10
anyMatch(e -> true)poi.
FBB

5

Penso che dovrebbe essere sufficiente per mappare un booleano

Nel codice questo è:

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
Se questo è tutto ciò di cui hai bisogno, la risposta di kenglxn è migliore.
Dominykas Mostauskis,

è inutile, duplica Collection.isEmpty ()
Krzysiek

@Krzysiek non è inutile se devi filtrare la raccolta. Tuttavia, sono d'accordo con Dominykas sul fatto che la risposta di kenglxn sia migliore
Hertzu

È perché duplica ancheStream.anyMatch()
Krzysiek

4

Seguendo l'idea di Stuart, questo potrebbe essere fatto con un Spliteratorsimile:

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

Penso che funzioni con Stream paralleli poiché l' stream.spliterator()operazione terminerà il flusso e quindi lo ricostruirà come richiesto

Nel mio caso d'uso avevo bisogno di un valore predefinito Streampiuttosto che di un valore predefinito. è abbastanza facile da cambiare se non è ciò di cui hai bisogno


Non riesco a capire se ciò avrebbe un impatto significativo sulle prestazioni con flussi paralleli. Probabilmente dovrei testarlo se questo è un requisito
phoenix7360

Scusa, non mi ero reso conto che @Holger aveva anche una soluzione con SpliteratorMi chiedo come si confrontino i due.
phoenix7360
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.