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

96

Comment puis-je vérifier si a Streamest vide et lever 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 entre les deux. En particulier, le contrôle ne doit pas avoir lieu avant que le flux ne soit effectivement 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();
}
Céphalopode
la source
23
Vous ne pouvez pas avoir votre gâteau et le manger aussi - et tout à fait littéralement, dans ce contexte. Vous devez consommer le flux pour savoir s'il est vide. C'est le point de la sémantique de Stream (la paresse).
Marko Topolnik
Il sera éventuellement consommé, à ce stade, le contrôle devrait avoir lieu
Céphalopode
11
Pour vérifier que le flux n'est pas vide, vous devez essayer de consommer au moins un élément. À ce stade, le flux a perdu sa «virginité» et ne peut plus être consommé depuis le début.
Marko Topolnik

Réponses:

24

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 qui l'utilise:

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 avec une exécution parallèle (efficace) est que la prise en charge de la division du Spliteratornécessite un moyen thread-safe pour savoir si l'un des fragments a vu une valeur d'une manière thread-safe. Ensuite, le dernier des fragments en cours d'exécution tryAdvancedoit se rendre compte que c'est le dernier (et il n'a pas non plus pu avancer) à lancer l'exception appropriée. Je n'ai donc pas ajouté de support pour le fractionnement ici.

Holger
la source
33

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, on peut le faire et transformer le résultat en un flux, sans mettre en mémoire 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, transformez le flux en un Iteratorpour l'appeler hasNext(), et si c'est vrai, transformez le Iteratordos en un Stream. Ceci est inefficace en ce que toutes les opérations ultérieures sur le flux passeront par les méthodes hasNext()et de l'itérateur next(), ce qui implique également que le flux est effectivement traité séquentiellement (même s'il est ensuite mis en parallèle). Cependant, cela vous permet de tester le flux sans mettre en mémoire tampon tous ses éléments.

Il existe probablement un moyen de le faire en utilisant un Spliteratorau lieu d'un Iterator. Cela permet potentiellement au flux renvoyé d'avoir les mêmes caractéristiques que le flux d'entrée, y compris une exécution en parallèle.

Marques Stuart
la source
1
Je ne pense pas qu'il existe une solution maintenable qui prendrait en charge un traitement parallèle efficace car il est difficile de prendre en charge le fractionnement, mais ayant estimatedSizeet characteristicspourrait même améliorer les performances à un seul thread. Il se trouve que j'ai écrit la Spliteratorsolution pendant que vous posiez la Iteratorsolution…
Holger
3
Vous pouvez demander au flux un Spliterator, appeler tryAdvance (lambda) où votre lambda capture tout ce qui lui est passé, puis renvoyer un Spliterator qui délègue presque tout au Spliterator sous-jacent, sauf qu'il recolle le premier élément sur le premier morceau ( et corrige le résultat d'estimationSize).
Brian Goetz
1
@BrianGoetz Oui, c'était ma pensée, je n'ai tout simplement pas encore pris la peine de passer par le travail d'étape consistant à gérer tous ces détails.
Stuart marque
3
@Brian Goetz: C'est ce que je voulais dire par «trop compliqué». Appeler tryAdvanceavant le Streamfait transforme la nature paresseuse du Streamen un flux «partiellement paresseux». Cela implique également que la recherche du premier élément n'est plus une opération parallèle, car vous devez d'abord vous séparer et le faire tryAdvancesur les parties fractionnées simultanément pour faire une véritable opération parallèle, pour autant que je sache. Si la seule opération de terminal est findAnyou similaire, cela détruirait la parallel()demande entière .
Holger
2
Donc, pour une prise en charge parallèle complète, vous ne devez pas appeler tryAdvanceavant le flux et devez envelopper chaque partie fractionnée dans un proxy et collecter les informations «hasAny» de toutes les opérations simultanées par vous-même et vous assurer que la dernière opération simultanée lève l'exception souhaitée si le le flux était vide. Beaucoup de trucs…
Holger
19

Cela peut être suffisant dans de nombreux cas

stream.findAny().isPresent()
kenglxn
la source
15

Vous devez effectuer une opération de terminal sur le Stream 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 Stream avec une findAny()opération de terminal, 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 de savoir que le Stream n'est pas vide.

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

Eran
la source
7
Il y a anyMatch(alwaysTrue()), je pense que c'est le plus proche hasAny.
Marko Topolnik
1
@MarkoTopolnik Je viens de vérifier la référence - ce que j'avais à l'esprit était findAny (), bien que anyMatch () fonctionnerait également.
Eran
3
anyMatch(alwaysTrue())correspond parfaitement à la sémantique prévue de votre hasAny, vous donnant un booleanau lieu de Optional<T>--- mais nous nous
fendons les
1
La note alwaysTrueest un prédicat Guava.
Jean-François Savard
10
anyMatch(e -> true)puis.
FBB
5

Je pense que cela devrait suffire à mapper un booléen

Dans le 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
Luis Roberto
la source
1
Si c'est tout ce dont vous avez besoin, la réponse de kenglxn est meilleure.
Dominykas Mostauskis le
c'est inutile, il duplique Collection.isEmpty ()
Krzysiek
@Krzysiek ce n'est pas inutile si vous avez besoin de filtrer la collection. Cependant, je suis d'accord avec Dominykas sur le fait que la réponse de kenglxn est meilleure
Hertzu
C'est parce qu'il duplique aussiStream.anyMatch()
Krzysiek
4

Suivant l'idée de Stuart, cela pourrait être fait avec un Spliteratorcomme 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' stream.spliterator()opération mettra fin au flux, puis le reconstruira si nécessaire

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

phénix7360
la source
Je ne peux pas savoir si cela aurait un impact significatif sur les performances avec des flux parallèles. Devrait probablement le tester si c'est une exigence
phoenix7360
Désolé, Spliteratorje n'ai pas réalisé que @Holger avait également une solution, je me demande comment les deux se comparent.
phoenix7360