Limitare un flusso di un predicato


187

Esiste un'operazione di flusso Java 8 che limita un (potenzialmente infinito) Stream fino a quando il primo elemento non riesce a corrispondere a un predicato?

In Java 9 possiamo usare takeWhilecome nell'esempio seguente per stampare tutti i numeri meno di 10.

IntStream
    .iterate(1, n -> n + 1)
    .takeWhile(n -> n < 10)
    .forEach(System.out::println);

Dato che non esiste alcuna operazione di questo tipo in Java 8, qual è il modo migliore per implementarla in modo generale?


1
Forse informazioni utili a: stackoverflow.com/q/19803058/248082
nobeh


Mi chiedo come gli architetti potrebbero mai superare il "cosa possiamo effettivamente usare per questo" senza imbattersi in questo caso d'uso. A partire da Java 8 Streams sono effettivamente utili solo per le strutture di dati esistenti: - /
Thorbjørn Ravn Andersen


Con Java 9, sarebbe più facile scrivereIntStream.iterate(1, n->n<10, n->n+1).forEach(System.out::print);
Marc Dzaebel l'

Risposte:


81

Tale operazione dovrebbe essere possibile con Java 8 Stream, ma non può essere necessariamente eseguita in modo efficiente - ad esempio, non è possibile parallelizzare necessariamente tale operazione, poiché è necessario esaminare gli elementi in ordine.

L'API non fornisce un modo semplice per farlo, ma quello che probabilmente è il modo più semplice è prendere Stream.iterator(), avvolgerlo Iteratorper avere un'implementazione "take-while", quindi tornare a a Spliteratore quindi a Stream. O, forse, avvolgere il Spliterator, anche se non può essere più suddiviso in questa implementazione.

Ecco un'implementazione non testata di takeWhilesu a Spliterator:

static <T> Spliterator<T> takeWhile(
    Spliterator<T> splitr, Predicate<? super T> predicate) {
  return new Spliterators.AbstractSpliterator<T>(splitr.estimateSize(), 0) {
    boolean stillGoing = true;
    @Override public boolean tryAdvance(Consumer<? super T> consumer) {
      if (stillGoing) {
        boolean hadNext = splitr.tryAdvance(elem -> {
          if (predicate.test(elem)) {
            consumer.accept(elem);
          } else {
            stillGoing = false;
          }
        });
        return hadNext && stillGoing;
      }
      return false;
    }
  };
}

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

8
In teoria, parallelizzare takeWhile con un predicato apolide è facile. Valuta la condizione in lotti paralleli (supponendo che il predicato non venga lanciato o abbia un effetto collaterale se eseguito un paio di volte in più). Il problema è farlo nel contesto della decomposizione ricorsiva (fork / join framework) utilizzata da Streams. Davvero, sono gli Stream che sono orribilmente inefficienti.
Aleksandr Dubinsky,

91
Gli stream sarebbero stati molto meglio se non fossero stati così preoccupati per il parallelismo automagico. Il parallelismo è necessario solo in una minima parte dei luoghi in cui è possibile utilizzare gli stream. Inoltre, se Oracle si preoccupasse così tanto delle prestazioni, avrebbe potuto rendere l'autovectorize JVM JIT e ottenere un incremento delle prestazioni molto più grande, senza disturbare gli sviluppatori. Questo è il parallelismo automagico fatto bene.
Aleksandr Dubinsky,

È necessario aggiornare questa risposta ora che Java 9 è stato rilasciato.
Radiodef,

4
No, @Radiodef. La domanda richiede specificamente una soluzione Java 8.
Renato Back,

146

Operazioni takeWhilee dropWhilesono stati aggiunti a JDK 9. Il tuo codice di esempio

IntStream
    .iterate(1, n -> n + 1)
    .takeWhile(n -> n < 10)
    .forEach(System.out::println);

si comporterà esattamente come previsto quando compilato ed eseguito con JDK 9.

JDK 9 è stato rilasciato. È disponibile per il download qui: http://jdk.java.net/9/


3
Link diretto ai documenti di anteprima per JDK9 Stream, con takeWhile/ dropWhile: download.java.net/jdk9/docs/api/java/util/stream/Stream.html
Miglia

1
C'è qualche motivo per cui vengono chiamati takeWhilee dropWhilepiuttosto che limitWhilee skipWhile, per coerenza con l'API esistente?
Lukas Eder

10
@LukasEder takeWhilee dropWhilesono piuttosto diffusi, si verificano in Scala, Python, Groovy, Ruby, Haskell e Clojure. L'asimmetria con skiped limitè sfortunata. Forse skipe limitavrebbe dovuto essere chiamato drope take, ma quelli non sono così intuitivi a meno che tu non abbia già familiarità con Haskell.
Stuart segna l'

3
@StuartMarks: lo capisco dropXXXe takeXXXsono termini più popolari ma posso vivere personalmente con più SQL-esque limitXXXe skipXXX. Trovo che questa nuova asimmetria sia molto più confusa della scelta individuale dei termini ... :) (a proposito: anche Scala ha drop(int)e take(int))
Lukas Eder

1
Sì, lasciami aggiornare a Jdk 9 in produzione. Molti sviluppatori sono ancora su Jdk8, una funzionalità del genere avrebbe dovuto essere inclusa in Stream dall'inizio.
Wilmol,

50

allMatch()è una funzione di corto circuito, quindi puoi usarla per interrompere l'elaborazione. Lo svantaggio principale è che devi eseguire il test due volte: una volta per vedere se è necessario elaborarlo e di nuovo per vedere se continuare.

IntStream
    .iterate(1, n -> n + 1)
    .peek(n->{if (n<10) System.out.println(n);})
    .allMatch(n->n < 10);

5
Questo all'inizio non mi sembrò per niente intuitivo (dato il nome del metodo), ma i documenti confermano che si Stream.allMatch()tratta di un'operazione di corto circuito . Quindi questo si completerà anche su un flusso infinito come IntStream.iterate(). Naturalmente, a posteriori, questa è un'ottimizzazione sensata.
Bailey Parker,

3
Questo è pulito, ma non credo che comunichi molto bene che il suo intento è il corpo del peek. Se l'avessi incontrato il mese prossimo, mi sarei preso un minuto per chiedermi perché il programmatore prima di me avesse verificato se allMatche poi avesse ignorato la risposta.
Joshua Goldberg,

10
Lo svantaggio di questa soluzione è che restituisce un valore booleano, quindi non è possibile raccogliere i risultati del flusso come si farebbe normalmente.
NeXus,

35

Come seguito alla risposta di @StuartMarks . La mia libreria StreamEx ha l' takeWhileoperazione compatibile con l'attuale implementazione JDK-9. Quando viene eseguito con JDK-9, verrà semplicemente delegato all'implementazione JDK (tramite la MethodHandle.invokeExactquale è veramente veloce). Quando viene eseguito con JDK-8, verrà utilizzata l'implementazione "polyfill". Quindi usando la mia libreria il problema può essere risolto in questo modo:

IntStreamEx.iterate(1, n -> n + 1)
           .takeWhile(n -> n < 10)
           .forEach(System.out::println);

Perché non l'hai implementato per la classe StreamEx?
Someguy,

@Someguy L'ho implementato.
Tagir Valeev,

14

takeWhileè una delle funzioni fornite dalla libreria protonpack .

Stream<Integer> infiniteInts = Stream.iterate(0, i -> i + 1);
Stream<Integer> finiteInts = StreamUtils.takeWhile(infiniteInts, i -> i < 10);

assertThat(finiteInts.collect(Collectors.toList()),
           hasSize(10));

11

Aggiornamento: Java 9 Streamora viene fornito con un metodo takeWhile .

Non sono necessari hack o altre soluzioni. Usalo e basta!


Sono sicuro che questo può essere notevolmente migliorato su: (qualcuno potrebbe renderlo thread-safe forse)

Stream<Integer> stream = Stream.iterate(0, n -> n + 1);

TakeWhile.stream(stream, n -> n < 10000)
         .forEach(n -> System.out.print((n == 0 ? "" + n : "," + n)));

Un hack di sicuro ... Non elegante - ma funziona ~: D

class TakeWhile<T> implements Iterator<T> {

    private final Iterator<T> iterator;
    private final Predicate<T> predicate;
    private volatile T next;
    private volatile boolean keepGoing = true;

    public TakeWhile(Stream<T> s, Predicate<T> p) {
        this.iterator = s.iterator();
        this.predicate = p;
    }

    @Override
    public boolean hasNext() {
        if (!keepGoing) {
            return false;
        }
        if (next != null) {
            return true;
        }
        if (iterator.hasNext()) {
            next = iterator.next();
            keepGoing = predicate.test(next);
            if (!keepGoing) {
                next = null;
            }
        }
        return next != null;
    }

    @Override
    public T next() {
        if (next == null) {
            if (!hasNext()) {
                throw new NoSuchElementException("Sorry. Nothing for you.");
            }
        }
        T temp = next;
        next = null;
        return temp;
    }

    public static <T> Stream<T> stream(Stream<T> s, Predicate<T> p) {
        TakeWhile tw = new TakeWhile(s, p);
        Spliterator split = Spliterators.spliterator(tw, Integer.MAX_VALUE, Spliterator.ORDERED);
        return StreamSupport.stream(split, false);
    }

}

8

Puoi usare java8 + rxjava .

import java.util.stream.IntStream;
import rx.Observable;


// Example 1)
IntStream intStream  = IntStream.iterate(1, n -> n + 1);
Observable.from(() -> intStream.iterator())
    .takeWhile(n ->
          {
                System.out.println(n);
                return n < 10;
          }
    ).subscribe() ;


// Example 2
IntStream intStream  = IntStream.iterate(1, n -> n + 1);
Observable.from(() -> intStream.iterator())
    .takeWhile(n -> n < 10)
    .forEach( n -> System.out.println(n));

6

In realtà ci sono 2 modi per farlo in Java 8 senza librerie extra o usando Java 9.

Se vuoi stampare numeri da 2 a 20 sulla console, puoi fare questo:

IntStream.iterate(2, (i) -> i + 2).peek(System.out::println).allMatch(i -> i < 20);

o

IntStream.iterate(2, (i) -> i + 2).peek(System.out::println).anyMatch(i -> i >= 20);

L'output è in entrambi i casi:

2
4
6
8
10
12
14
16
18
20

Nessuno ha ancora menzionato alcunoMatch . Questo è il motivo di questo post.


5

Questa è la fonte copiata da JDK 9 java.util.stream.Stream.takeWhile (Predicato). Una piccola differenza per lavorare con JDK 8.

static <T> Stream<T> takeWhile(Stream<T> stream, Predicate<? super T> p) {
    class Taking extends Spliterators.AbstractSpliterator<T> implements Consumer<T> {
        private static final int CANCEL_CHECK_COUNT = 63;
        private final Spliterator<T> s;
        private int count;
        private T t;
        private final AtomicBoolean cancel = new AtomicBoolean();
        private boolean takeOrDrop = true;

        Taking(Spliterator<T> s) {
            super(s.estimateSize(), s.characteristics() & ~(Spliterator.SIZED | Spliterator.SUBSIZED));
            this.s = s;
        }

        @Override
        public boolean tryAdvance(Consumer<? super T> action) {
            boolean test = true;
            if (takeOrDrop &&               // If can take
                    (count != 0 || !cancel.get()) && // and if not cancelled
                    s.tryAdvance(this) &&   // and if advanced one element
                    (test = p.test(t))) {   // and test on element passes
                action.accept(t);           // then accept element
                return true;
            } else {
                // Taking is finished
                takeOrDrop = false;
                // Cancel all further traversal and splitting operations
                // only if test of element failed (short-circuited)
                if (!test)
                    cancel.set(true);
                return false;
            }
        }

        @Override
        public Comparator<? super T> getComparator() {
            return s.getComparator();
        }

        @Override
        public void accept(T t) {
            count = (count + 1) & CANCEL_CHECK_COUNT;
            this.t = t;
        }

        @Override
        public Spliterator<T> trySplit() {
            return null;
        }
    }
    return StreamSupport.stream(new Taking(stream.spliterator()), stream.isParallel()).onClose(stream::close);
}

4

Ecco una versione fatta su ints - come richiesto nella domanda.

Uso:

StreamUtil.takeWhile(IntStream.iterate(1, n -> n + 1), n -> n < 10);

Ecco il codice per StreamUtil:

import java.util.PrimitiveIterator;
import java.util.Spliterators;
import java.util.function.IntConsumer;
import java.util.function.IntPredicate;
import java.util.stream.IntStream;
import java.util.stream.StreamSupport;

public class StreamUtil
{
    public static IntStream takeWhile(IntStream stream, IntPredicate predicate)
    {
        return StreamSupport.intStream(new PredicateIntSpliterator(stream, predicate), false);
    }

    private static class PredicateIntSpliterator extends Spliterators.AbstractIntSpliterator
    {
        private final PrimitiveIterator.OfInt iterator;
        private final IntPredicate predicate;

        public PredicateIntSpliterator(IntStream stream, IntPredicate predicate)
        {
            super(Long.MAX_VALUE, IMMUTABLE);
            this.iterator = stream.iterator();
            this.predicate = predicate;
        }

        @Override
        public boolean tryAdvance(IntConsumer action)
        {
            if (iterator.hasNext()) {
                int value = iterator.nextInt();
                if (predicate.test(value)) {
                    action.accept(value);
                    return true;
                }
            }

            return false;
        }
    }
}

2

Vai a ottenere la libreria AbacusUtil . Fornisce l'API esatta desiderata e altro:

IntStream.iterate(1, n -> n + 1).takeWhile(n -> n < 10).forEach(System.out::println);

Dichiarazione : Sono lo sviluppatore di AbacusUtil.


0

Non è possibile interrompere un flusso se non mediante un'operazione di terminale in corto circuito, che lascerebbe alcuni valori del flusso non elaborati indipendentemente dal loro valore. Ma se vuoi solo evitare operazioni su uno stream puoi aggiungere una trasformazione e un filtro allo stream:

import java.util.Objects;

class ThingProcessor
{
    static Thing returnNullOnCondition(Thing thing)
    {    return( (*** is condition met ***)? null : thing);    }

    void processThings(Collection<Thing> thingsCollection)
    {
        thingsCollection.stream()
        *** regular stream processing ***
        .map(ThingProcessor::returnNullOnCondition)
        .filter(Objects::nonNull)
        *** continue stream processing ***
    }
} // class ThingProcessor

Ciò trasforma il flusso di cose in null quando le cose soddisfano una condizione, quindi filtra i null. Se sei disposto a indulgere in effetti collaterali, puoi impostare il valore della condizione su true una volta che si incontra qualcosa, quindi tutte le cose successive vengono filtrate indipendentemente dal loro valore. Ma anche se non puoi salvare un sacco di (se non del tutto) elaborazione filtrando i valori dallo stream che non vuoi elaborare.


È logico che un rater anonimo abbia ridimensionato la mia risposta senza dire il perché. Quindi né io né nessun altro lettore sappiamo cosa c'è di sbagliato nella mia risposta. In assenza della loro giustificazione considererò le loro critiche invalide e la mia risposta pubblicata come corretta.
Matteo

La tua risposta non risolve il problema dei PO, che ha a che fare con flussi infiniti. Sembra anche complicare inutilmente le cose poiché puoi scrivere la condizione nella chiamata filter () stessa, senza bisogno di map (). La domanda ha già un codice di esempio, prova solo ad applicare la tua risposta a quel codice e vedrai che il programma eseguirà un ciclo per sempre.
SenoCtar

0

Anche se avevo un requisito simile: invocare il servizio Web, se fallisce, riprovare 3 volte. Se fallisce anche dopo queste molte prove, invia una notifica via email. Dopo aver cercato su Google molto, è anyMatch()venuto come un salvatore. Il mio codice di esempio come segue. Nel seguente esempio, se il metodo webServiceCall restituisce true nella prima iterazione stessa, il flusso non viene ripetuto ulteriormente come abbiamo chiamato anyMatch(). Credo, questo è quello che stai cercando.

import java.util.stream.IntStream;

import io.netty.util.internal.ThreadLocalRandom;

class TrialStreamMatch {

public static void main(String[] args) {        
    if(!IntStream.range(1,3).anyMatch(integ -> webServiceCall(integ))){
         //Code for sending email notifications
    }
}

public static boolean webServiceCall(int i){
    //For time being, I have written a code for generating boolean randomly
    //This whole piece needs to be replaced by actual web-service client code
    boolean bool = ThreadLocalRandom.current().nextBoolean();
    System.out.println("Iteration index :: "+i+" bool :: "+bool);

    //Return success status -- true or false
    return bool;
}

0

Se conosci l'esatta quantità di ripetizioni che verranno eseguite, puoi farlo

IntStream
          .iterate(1, n -> n + 1)
          .limit(10)
          .forEach(System.out::println);

1
Sebbene ciò possa rispondere alla domanda degli autori, manca alcune parole esplicative e collegamenti alla documentazione. Gli snippet di codice non elaborati non sono molto utili senza alcune frasi. Puoi anche trovare molto utile come scrivere una buona risposta . Modifica la tua risposta
hellow,

0
    IntStream.iterate(1, n -> n + 1)
    .peek(System.out::println) //it will be executed 9 times
    .filter(n->n>=9)
    .findAny();

invece di picco puoi usare mapToObj per restituire l'oggetto o il messaggio finale

    IntStream.iterate(1, n -> n + 1)
    .mapToObj(n->{   //it will be executed 9 times
            if(n<9)
                return "";
            return "Loop repeats " + n + " times";});
    .filter(message->!message.isEmpty())
    .findAny()
    .ifPresent(System.out::println);

-2

Se hai un problema diverso, potrebbe essere necessaria una soluzione diversa, ma per il tuo problema attuale, vorrei semplicemente andare con:

IntStream
    .iterate(1, n -> n + 1)
    .limit(10)
    .forEach(System.out::println);

-2

Potrebbe essere un po 'fuori tema, ma questo è quello che abbiamo List<T>piuttosto cheStream<T> .

Per prima cosa devi avere un takemetodo util. Questo metodo accetta i primi nelementi:

static <T> List<T> take(List<T> l, int n) {
    if (n <= 0) {
        return newArrayList();
    } else {
        int takeTo = Math.min(Math.max(n, 0), l.size());
        return l.subList(0, takeTo);
    }
}

funziona come scala.List.take

    assertEquals(newArrayList(1, 2, 3), take(newArrayList(1, 2, 3, 4, 5), 3));
    assertEquals(newArrayList(1, 2, 3), take(newArrayList(1, 2, 3), 5));

    assertEquals(newArrayList(), take(newArrayList(1, 2, 3), -1));
    assertEquals(newArrayList(), take(newArrayList(1, 2, 3), 0));

ora sarà abbastanza semplice scrivere un takeWhilemetodo basato sutake

static <T> List<T> takeWhile(List<T> l, Predicate<T> p) {
    return l.stream().
            filter(p.negate()).findFirst(). // find first element when p is false
            map(l::indexOf).        // find the index of that element
            map(i -> take(l, i)).   // take up to the index
            orElse(l);  // return full list if p is true for all elements
}

funziona così:

    assertEquals(newArrayList(1, 2, 3), takeWhile(newArrayList(1, 2, 3, 4, 3, 2, 1), i -> i < 4));

questa implementazione ripeterà parzialmente l'elenco per alcune volte ma non aggiungerà O(n^2)operazioni di aggiunta . Spero sia accettabile


-3

Ho un'altra soluzione rapida implementando questo (che è davvero impuro, ma hai l'idea):

public static void main(String[] args) {
    System.out.println(StreamUtil.iterate(1, o -> o + 1).terminateOn(15)
            .map(o -> o.toString()).collect(Collectors.joining(", ")));
}

static interface TerminatedStream<T> {
    Stream<T> terminateOn(T e);
}

static class StreamUtil {
    static <T> TerminatedStream<T> iterate(T seed, UnaryOperator<T> op) {
        return new TerminatedStream<T>() {
            public Stream<T> terminateOn(T e) {
                Builder<T> builder = Stream.<T> builder().add(seed);
                T current = seed;
                while (!current.equals(e)) {
                    current = op.apply(current);
                    builder.add(current);
                }
                return builder.build();
            }
        };
    }
}

2
Stai valutando l' intero flusso in anticipo! E se currentmai .equals(e), otterrai un ciclo infinito. Entrambi anche se successivamente si applica ad es .limit(1). È molto peggio di "impuro" .
charlie,

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.