web-dev-qa-db-fra.com

Sélection d'éléments d'une liste jusqu'à ce que la condition soit remplie avec Java 8 Lambdas

J'essaie de changer d'idée pour penser de manière fonctionnelle et j'ai récemment fait face à une situation dans laquelle je devais extraire des éléments d'une liste jusqu'à ce qu'une condition soit remplie et que je ne trouve pas de moyen naturel et facile d'y parvenir. Évidemment, j'apprends encore.

Dis que j'ai cette liste:

List<String> tokens = Arrays.asList("pick me", "Pick me", "pick Me",
    "PICK ME", "pick me and STOP", "pick me", "pick me and Stop", "pick me");

// In a non lambdas was you would do it like below
List<String> myTokens = new ArrayList<>();
for (String token : tokens) {
    myTokens.add(token);
    if (token.toUpperCase().endsWith("STOP")) {
        break;
    }
}

Merci d'avance pour vos contributions

NOTE: Avant de publier ceci, je lis Limite un flux par un prédicat mais je ne voyais pas comment je pourrais adapter cette réponse à mon problème. Toute aide serait appréciée Merci.

15
Julian

Une option utilise un collecteur nécessitant deux fonctions, une qui ajoute des chaînes à des listes et une autre qui combine des listes précédemment créées potentiellement en parallèle. Pour chacun, il ajoute la chaîne ou la liste complète uniquement si la sortie partielle précédente ne se termine pas par un élément qui se termine par STOP:

tokens.stream().collect(() -> new ArrayList<String>(), (l, e) -> {
    if(l.isEmpty() || !l.get(l.size()-1).toUpperCase().endsWith("STOP"))
        l.add(e);
}, (l1, l2) -> {
    if(l1.isEmpty() || !l1.get(l1.size()-1).toUpperCase().endsWith("STOP"))
        l1.addAll(l2);
});
2
WillShackleford

Dans JDK9, il y aura une nouvelle opération Stream appelée takeWhile qui fera ce qui est similaire à ce dont vous avez besoin. J'ai rétroporté cette opération dans ma bibliothèque StreamEx , afin que vous puissiez l'utiliser même en Java-8:

List<String> list = StreamEx.of(tokens)
                            .takeWhile(t -> !t.toUpperCase().endsWith("STOP"))
                            .toList();

Malheureusement, l'élément "STOP" n'est pas pris lui-même. La deuxième passe est donc nécessaire pour l'ajouter manuellement:

list.add(StreamEx.of(tokens).findFirst(t -> t.toUpperCase().endsWith("STOP")).get());

Notez que takeWhile et findFirst sont tous deux des opérations de court-circuit (ils ne traiteront pas l'intégralité du flux d'entrée si cela n'est pas nécessaire). Vous pouvez donc les utiliser avec des flux très longs, voire infinis.

Cependant, en utilisant StreamEx, vous pouvez le résoudre en un seul passage en utilisant l’astuce avec groupRuns . La méthode groupRuns regroupe les éléments Stream adjacents à la List en fonction du prédicat fourni qui indique si deux éléments adjacents donnés doivent être groupés ou non. Nous pouvons considérer que le groupe se termine par l'élément contenant "STOP". Ensuite, il suffit de prendre le premier groupe:

List<String> list = StreamEx.of(tokens)
                            .groupRuns((a, b) -> !a.toUpperCase().endsWith("STOP"))
                            .findFirst().get();

Cette solution ne fera pas non plus de travail supplémentaire lorsque le premier groupe est terminé.

13
Tagir Valeev

Si vous devez vraiment utiliser l'API Streams, restez simple et utilisez un flux d'index:

int lastIdx = IntStream.range(0, tokens.size())
        .filter(i -> tokens.get(i).toUpperCase().endsWith("STOP"))
        .findFirst()
        .orElse(-1);

List<String> myTokens = tokens.subList(0, lastIdx + 1);

Ou créez une nouvelle variable List de la sous-liste si vous souhaitez une copie indépendante qui n'est pas sauvegardée par la liste d'origine.

9
Misha

En utilisant uniquement l'API Java 8:

public static <R> Stream<? extends R> takeUntil(Iterator<R> iterator, Predicate<? super R> stopFilter) {

    return StreamSupport.stream(Spliterators.spliteratorUnknownSize(new Iterator<R>() {
        private R next = null;
        private boolean hasTaken = true;
        private boolean stopIteration = !iterator.hasNext();
        @Override
        public boolean hasNext() {
            if (stopIteration) {
                return false;
            }

            if (!hasTaken) {
                return true;
            }

            if (!iterator.hasNext()) {
                stopIteration = true;
                return false;
            }

            next = iterator.next();
            stopIteration = stopFilter.test(next);
            hasTaken = stopIteration;
            return !stopIteration;
        }

        @Override
        public R next() {
            if (!hasNext()) {
                throw new NoSuchElementException("There are no more items to consume");
            }
            hasTaken = true;
            return next;
        }
    }, 0), false);
}

Vous pouvez ensuite le spécialiser des manières suivantes:

Pour les ruisseaux

public static <R> Stream<? extends R> takeUntil(Stream<R> stream, Predicate<? super R> stopFilter) {
    return takeUntil(stream.iterator(), stopFilter);
}

Pour les collections

public static <R> Stream<? extends R> takeUntil(Collection<R> col, Predicate<? super R> stopFilter) {
    return takeUntil(col.iterator(), stopFilter);
}
1
smac89

Bien que les réponses ci-dessus soient parfaitement valables, elles nécessitent de collecter et/ou pré-extraire les éléments avant de les traiter (les deux peuvent poser problème si le flux est très long).

Pour mes besoins , j'ai donc adapté la réponse de Louis à la question indiquée par Julian et l'a adaptée pour conserver l'élément stop/break. Voir le paramètre keepBreak ::

public static <T> Spliterator<T> takeWhile(final Spliterator<T> splitr, final Predicate<? super T> predicate, final boolean keepBreak) {
    return new Spliterators.AbstractSpliterator<T>(splitr.estimateSize(), 0) {
        boolean stillGoing = true;

        @Override
        public boolean tryAdvance(final Consumer<? super T> consumer) {
            if (stillGoing) {
                final boolean hadNext = splitr.tryAdvance(elem -> {
                    if (predicate.test(elem)) {
                        consumer.accept(elem);
                    } else {
                        if (keepBreak) {
                            consumer.accept(elem);
                        }
                        stillGoing = false;
                    }
                });
                return hadNext && (stillGoing || keepBreak);
            }
            return false;
        }
    };
}

public static <T> Stream<T> takeWhile(final Stream<T> stream, final Predicate<? super T> predicate, final boolean keepBreak) {
    return StreamSupport.stream(takeWhile(stream.spliterator(), predicate, keepBreak), false);
}

Utilisation:

public List<String> values = Arrays.asList("some", "words", "before", "BREAK", "AFTER");

    @Test
    public void testStopAfter() {
        Stream<String> stream = values.stream();
        //how to filter stream to stop at the first BREAK
        stream = stream.filter(makeUntil(s -> "BREAK".equals(s)));
        final List<String> actual = stream.collect(Collectors.toList());

        final List<String> expected = Arrays.asList("some", "words", "before", "BREAK");
        assertEquals(expected, actual);
    }

Disclaimer: Je ne suis pas sûr à 100% que cela fonctionnera sur des flux parallèles (le nouveau Stream n'est certainement pas parallèle) ou non séquentiels. Veuillez commenter/modifier si vous avez des indices à ce sujet.

1
Benoît