Come interrompere Java Stream forEach

1. Panoramica

Come sviluppatori Java, spesso scriviamo codice che itera su un insieme di elementi ed esegue un'operazione su ciascuno di essi. La libreria di flussi Java 8 e il suo metodo forEach ci consentono di scrivere quel codice in modo pulito e dichiarativo.

Sebbene sia simile ai cicli, ci manca l'equivalente dell'istruzione break per interrompere l'iterazione . Un flusso può essere molto lungo o potenzialmente infinito e se non abbiamo motivo di continuare a elaborarlo, vorremmo interromperlo, piuttosto che attendere il suo ultimo elemento.

In questo tutorial, esamineremo alcuni meccanismi che ci consentono di simulare un'istruzione break su un'operazione Stream.forEach .

2. Stream.takeWhile () di Java 9

Supponiamo di avere un flusso di elementi String e di voler elaborare i suoi elementi purché le loro lunghezze siano dispari.

Proviamo il metodo Java 9 Stream.takeWhile :

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

Se eseguiamo questo, otteniamo l'output:

cat dog

Confrontiamo questo con il codice equivalente in Java semplice usando un ciclo for e un'istruzione break , per aiutarci a vedere come funziona:

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

Come possiamo vedere, il metodo takeWhile ci consente di ottenere esattamente ciò di cui abbiamo bisogno.

Ma cosa succede se non abbiamo ancora adottato Java 9? Come possiamo ottenere una cosa simile usando Java 8?

3. Un divisore personalizzato

Creiamo uno Spliterator personalizzato che funzionerà come decoratore per un Stream.spliterator . Possiamo fare in modo che questo Spliterator esegua la pausa per noi.

Per prima cosa, prenderemo lo Spliterator dal nostro stream, quindi lo decoreremo con il nostro CustomSpliterator e forniremo il Predicate per controllare l' operazione di interruzione . Infine, creeremo un nuovo flusso da CustomSpliterator:

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

Diamo un'occhiata a come creare 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; } }

Quindi, diamo un'occhiata al metodo tryAdvance . Possiamo vedere qui che lo Spliterator personalizzato elabora gli elementi dello Spliterator decorato . L'elaborazione viene eseguita fintanto che il nostro predicato è abbinato e il flusso iniziale ha ancora elementi. Quando una delle due condizioni diventa falsa , il nostro Spliterator "si interrompe" e l'operazione di streaming termina.

Testiamo il nostro nuovo metodo di supporto:

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

Come possiamo vedere, il flusso si è interrotto dopo che la condizione è stata soddisfatta. A scopo di test, abbiamo raccolto i risultati in un elenco, ma avremmo anche potuto usare una chiamata forEach o una qualsiasi delle altre funzioni di Stream .

4. A Custom forEach

Sebbene fornire un flusso con il meccanismo di interruzione incorporato possa essere utile, potrebbe essere più semplice concentrarsi solo sull'operazione forEach .

Usiamo Stream.spliterator direttamente senza un decoratore:

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

Come possiamo vedere, il nuovo metodo forEach personalizzato chiama un BiConsumer fornendo al nostro codice sia l'elemento successivo che un oggetto breaker che può utilizzare per interrompere il flusso.

Proviamolo in uno unit test:

@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. conclusione

In questo articolo, abbiamo esaminato i modi per fornire l'equivalente della chiamata di interruzione in un flusso. Abbiamo visto come takeWhile di Java 9 risolve la maggior parte del problema per noi e come fornirne una versione per Java 8.

Infine, abbiamo esaminato un metodo di utilità che può fornirci l'equivalente di un'operazione di interruzione durante l'iterazione su un flusso .

Come sempre, il codice di esempio può essere trovato su GitHub.