web-dev-qa-db-fra.com

Comment vérifier si un Java 8 Stream est vide?

Comment puis-je vérifier si un Stream est vide et émettre une exception si ce n'est pas le cas, en tant qu'opération non terminale?

En gros, je cherche quelque chose d’équivalent au code ci-dessous, mais sans matérialiser le flux intermédiaire. En particulier, le contrôle ne doit pas avoir lieu avant que le flux ne soit réellement consommé par une opération de terminal.

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

Si vous pouvez vivre avec des capacités parallèles limitées, la solution suivante fonctionnera:

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

Voici un exemple de code l’utilisant:

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

Le problème de l’exécution parallèle (efficace) est que, pour prendre en charge le fractionnement de Spliterator, il faut un moyen de thread-safe pour savoir si l’un ou l’autre des fragments a perçu une valeur de manière thread-safe. Ensuite, le dernier des fragments exécutant tryAdvance doit comprendre que c’est le dernier (et il ne peut pas non plus avancer) à lancer l’exception appropriée. Je n’ai donc pas ajouté de support pour la scission ici.

18
Holger

Les autres réponses et commentaires sont corrects en ce que pour examiner le contenu d'un flux, il faut ajouter une opération de terminal, "consommant" ainsi le flux. Cependant, cela peut être fait et transformer le résultat en un flux, sans mettre en tampon tout le contenu du flux. Voici quelques exemples:

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

En gros, convertissez le flux en Iterator pour y appeler hasNext(), et si true, reconvertissez Iterator en Stream. Ceci est inefficace en ce que toutes les opérations ultérieures sur le flux passeront par les méthodes hasNext() et next() de l'Iterator, ce qui implique également que le flux est effectivement traité de manière séquentielle (même s'il est ultérieurement basculé en parallèle). ). Toutefois, cela vous permet de tester le flux sans mettre en tampon tous ses éléments.

Il existe probablement un moyen de faire cela en utilisant un Spliterator au lieu d'un Iterator. Cela permet potentiellement au flux renvoyé d'avoir les mêmes caractéristiques que le flux d'entrée, notamment en parallèle.

30
Stuart Marks

Vous devez effectuer une opération de terminal sur le flux pour que l’un des filtres soit appliqué. Par conséquent, vous ne pouvez pas savoir s'il sera vide jusqu'à ce que vous le consommiez.

Le mieux que vous puissiez faire est de terminer le flux avec une opération de terminal findAny(), qui s’arrêtera lorsqu’il trouvera un élément, mais s’il n’y en a pas, il devra parcourir toute la liste d’entrée pour le découvrir.

Cela ne vous aiderait que si la liste d'entrée contient de nombreux éléments et que l'un des premiers passe les filtres, car seul un petit sous-ensemble de la liste devrait être consommé avant que vous sachiez que le flux n'est pas vide.

Bien sûr, vous devrez toujours créer un nouveau flux afin de produire la liste de sortie.

11
Eran

Suivant l’idée de Stuart, ceci pourrait être fait avec un Spliterator comme ceci:

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

Je pense que cela fonctionne avec des flux parallèles car l'opération stream.spliterator() met fin au flux, puis le reconstruit selon les besoins

Dans mon cas d'utilisation, j'avais besoin d'un Stream par défaut plutôt que d'une valeur par défaut. c'est assez facile à changer si ce n'est pas ce dont vous avez besoin

3
phoenix7360

Je pense devrait être suffisant pour cartographier un booléen

En code c'est:

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
0
Luis Roberto