Comment rompre avec Java Stream forEach

1. Vue d'ensemble

En tant que développeurs Java, nous écrivons souvent du code qui itère sur un ensemble d'éléments et effectue une opération sur chacun d'eux. La bibliothèque de flux Java 8 et sa méthode forEach nous permettent d'écrire ce code de manière propre et déclarative.

Bien que cela soit similaire aux boucles, il nous manque l'équivalent de l' instruction break pour abandonner l'itération . Un flux peut être très long, voire infini , et si nous n'avons aucune raison de continuer à le traiter, nous voudrions nous en séparer, plutôt que d'attendre son dernier élément.

Dans ce didacticiel, nous allons examiner certains mécanismes qui nous permettent de simuler une instruction break sur une opération Stream.forEach .

2. Stream.takeWhile de Java 9 ()

Supposons que nous ayons un flux d' éléments String et que nous souhaitons traiter ses éléments tant que leurs longueurs sont impaires.

Essayons la méthode Java 9 Stream.takeWhile :

Stream.of("cat", "dog", "elephant", "fox", "rabbit", "duck") .takeWhile(n -> n.length() % 2 != 0) .forEach(System.out::println);

Si nous exécutons ceci, nous obtenons la sortie:

cat dog

Comparons cela avec le code équivalent en Java brut en utilisant une boucle for et une instruction break , pour nous aider à voir comment cela fonctionne:

List list = asList("cat", "dog", "elephant", "fox", "rabbit", "duck"); for (int i = 0; i < list.size(); i++) { String item = list.get(i); if (item.length() % 2 == 0) { break; } System.out.println(item); } 

Comme nous pouvons le voir, la méthode takeWhile nous permet d'obtenir exactement ce dont nous avons besoin.

Mais que faire si nous n'avons pas encore adopté Java 9? Comment pouvons-nous réaliser une chose similaire en utilisant Java 8?

3. Un séparateur personnalisé

Créons un Spliterator personnalisé qui fonctionnera comme un décorateur pour un Stream.spliterator . Nous pouvons faire en sorte que ce Spliterator effectue la pause pour nous.

Tout d'abord, nous allons récupérer le Spliterator de notre flux, puis nous le décorerons avec notre CustomSpliterator et fournirons le Predicate pour contrôler l' opération de rupture . Enfin, nous allons créer un nouveau flux à partir du CustomSpliterator:

public static  Stream takeWhile(Stream stream, Predicate predicate) { CustomSpliterator customSpliterator = new CustomSpliterator(stream.spliterator(), predicate); return StreamSupport.stream(customSpliterator, false); }

Voyons comment créer le CustomSpliterator :

public class CustomSpliterator extends Spliterators.AbstractSpliterator { private Spliterator splitr; private Predicate predicate; private boolean isMatched = true; public CustomSpliterator(Spliterator splitr, Predicate predicate) { super(splitr.estimateSize(), 0); this.splitr = splitr; this.predicate = predicate; } @Override public synchronized boolean tryAdvance(Consumer consumer) { boolean hadNext = splitr.tryAdvance(elem -> { if (predicate.test(elem) && isMatched) { consumer.accept(elem); } else { isMatched = false; } }); return hadNext && isMatched; } }

Alors, jetons un œil à la méthode tryAdvance . Nous pouvons voir ici que le Spliterator personnalisé traite les éléments du Spliterator décoré . Le traitement est effectué tant que notre prédicat est mis en correspondance et que le flux initial contient encore des éléments. Lorsque l'une des conditions devient fausse , notre Spliterator «casse» et l'opération de diffusion en continu se termine.

Testons notre nouvelle méthode d'assistance:

@Test public void whenCustomTakeWhileIsCalled_ThenCorrectItemsAreReturned() { Stream initialStream = Stream.of("cat", "dog", "elephant", "fox", "rabbit", "duck"); List result = CustomTakeWhile.takeWhile(initialStream, x -> x.length() % 2 != 0) .collect(Collectors.toList()); assertEquals(asList("cat", "dog"), result); }

Comme nous pouvons le voir, le flux s'est arrêté une fois la condition remplie. À des fins de test, nous avons rassemblé les résultats dans une liste, mais nous aurions pu également utiliser un appel forEach ou l'une des autres fonctions de Stream .

4. Une coutume pour chaque

Bien que la fourniture d'un Stream avec le mécanisme de rupture intégré puisse être utile, il peut être plus simple de se concentrer uniquement sur l' opération forEach .

Utilisons le Stream.spliterator directement sans décorateur:

public class CustomForEach { public static class Breaker { private boolean shouldBreak = false; public void stop() { shouldBreak = true; } boolean get() { return shouldBreak; } } public static  void forEach(Stream stream, BiConsumer consumer) { Spliterator spliterator = stream.spliterator(); boolean hadNext = true; Breaker breaker = new Breaker(); while (hadNext && !breaker.get()) { hadNext = spliterator.tryAdvance(elem -> { consumer.accept(elem, breaker); }); } } }

Comme nous pouvons le voir, la nouvelle méthode personnalisée forEach appelle un BiConsumer fournissant à notre code à la fois l'élément suivant et un objet disjoncteur qu'il peut utiliser pour arrêter le flux.

Essayons ceci dans un test unitaire:

@Test public void whenCustomForEachIsCalled_ThenCorrectItemsAreReturned() { Stream initialStream = Stream.of("cat", "dog", "elephant", "fox", "rabbit", "duck"); List result = new ArrayList(); CustomForEach.forEach(initialStream, (elem, breaker) -> { if (elem.length() % 2 == 0) { breaker.stop(); } else { result.add(elem); } }); assertEquals(asList("cat", "dog"), result); }

5. Conclusion

Dans cet article, nous avons examiné des moyens de fournir l'équivalent d'appeler break sur un flux. Nous avons vu comment takeWhile de Java 9 résout la plupart des problèmes pour nous et comment en fournir une version pour Java 8.

Enfin, nous avons examiné une méthode utilitaire qui peut nous fournir l'équivalent d'une opération de rupture lors d'une itération sur un Stream .

Comme toujours, l'exemple de code peut être trouvé sur GitHub.