Puoi dividere un flusso in due flussi?


146

Ho un set di dati rappresentato da un flusso Java 8:

Stream<T> stream = ...;

Vedo come filtrarlo per ottenere un sottoinsieme casuale, ad esempio

Random r = new Random();
PrimitiveIterator.OfInt coin = r.ints(0, 2).iterator();   
Stream<T> heads = stream.filter((x) -> (coin.nextInt() == 0));

Posso anche vedere come potrei ridurre questo flusso per ottenere, ad esempio, due elenchi che rappresentano due metà casuali del set di dati e quindi trasformarli nuovamente in flussi. Ma esiste un modo diretto per generare due flussi da quello iniziale? Qualcosa di simile a

(heads, tails) = stream.[some kind of split based on filter]

Grazie per qualsiasi approfondimento.


La risposta di Mark è molto utile della risposta di Louis, ma devo dire che quella di Louis è più correlata alla domanda originale. La domanda è piuttosto incentrata sulla possibilità di convertire Streama più Streams senza conversione intermedia , anche se penso che le persone che hanno raggiunto questa domanda sono in realtà cercando il modo per raggiungere in modo indipendentemente da tale vincolo, che è la risposta di Marco. Ciò può essere dovuto al fatto che la domanda nel titolo non è uguale a quella nella descrizione .
Devildelta,

Risposte:


9

Non esattamente. Non puoi ottenere due Streams da uno; questo non ha senso: come faresti a scorrere su uno senza dover generare l'altro allo stesso tempo? Un flusso può essere gestito solo una volta.

Tuttavia, se vuoi scaricarli in un elenco o qualcosa del genere, puoi farlo

stream.forEach((x) -> ((x == 0) ? heads : tails).add(x));

65
Perché non ha senso? Dato che un flusso è una pipeline non c'è motivo per cui non sia possibile creare due produttori del flusso originale, ho potuto vedere che questo è gestito da un collezionista che fornisce due flussi.
Brett Ryan,

36
Non thread-safe. Cattivi consigli che provano ad aggiungere direttamente a una raccolta, ecco perché abbiamo il stream.collect(...)for con thread-safe predefinito Collectors, che funziona bene anche su raccolte non thread-safe (senza contesa di blocco sincronizzato). Migliore risposta di @MarkJeronimus.
YoYo

1
@JoD È sicuro per i thread se testate e code sono a thread sicuro. Inoltre, supponendo l'uso di flussi non paralleli, solo l'ordine non è garantito, quindi sono thread-safe. Spetta al programmatore risolvere i problemi di concorrenza, quindi questa risposta è perfettamente adatta se le raccolte sono thread-safe.
Nicolas,

1
@Nixon non è adatto in presenza di una soluzione migliore, che abbiamo qui. Avere un codice del genere può portare a un cattivo precedente, causando ad altri di usarlo in modo sbagliato. Anche se non vengono utilizzati flussi paralleli, è solo a un passo. Le buone pratiche di codifica richiedono di non mantenere lo stato durante le operazioni di streaming. La prossima cosa che facciamo è la codifica in un framework come Apache Spark, e le stesse pratiche porterebbero davvero a risultati inaspettati. Era una soluzione creativa, dico io, una che avrei potuto scrivere me stesso non molto tempo fa.
YoYo

1
@JoD Non è una soluzione migliore, è in realtà più inefficiente. Tale linea di pensiero alla fine finisce con la conclusione che tutte le Collezioni dovrebbero essere thread-safe di default per prevenire conseguenze indesiderate, il che è semplicemente sbagliato.
Nicolas,

301

Un collezionista può essere utilizzato per questo.

  • Per due categorie, utilizzare Collectors.partitioningBy()factory.

Ciò creerà un Mapda Booleana Liste inserirà gli elementi in uno o l'altro elenco in base a a Predicate.

Nota: poiché il flusso deve essere consumato intero, questo non può funzionare su flussi infiniti. E poiché lo stream viene comunque consumato, questo metodo li inserisce semplicemente negli elenchi invece di creare un nuovo stream con memoria. È sempre possibile eseguire lo streaming di tali elenchi se si richiedono flussi come output.

Inoltre, non è necessario l'iteratore, nemmeno nell'esempio solo testa che hai fornito.

  • La divisione binaria si presenta così:
Random r = new Random();

Map<Boolean, List<String>> groups = stream
    .collect(Collectors.partitioningBy(x -> r.nextBoolean()));

System.out.println(groups.get(false).size());
System.out.println(groups.get(true).size());
  • Per più categorie, utilizzare una Collectors.groupingBy()fabbrica.
Map<Object, List<String>> groups = stream
    .collect(Collectors.groupingBy(x -> r.nextInt(3)));
System.out.println(groups.get(0).size());
System.out.println(groups.get(1).size());
System.out.println(groups.get(2).size());

Nel caso in cui i flussi non lo siano Stream, ma piace uno dei flussi primitivi IntStream, questo .collect(Collectors)metodo non è disponibile. Dovrai farlo manualmente senza una fabbrica di collezionisti. La sua implementazione è simile alla seguente:

[Esempio 2.0 dal 2020-04-16]

    IntStream    intStream = IntStream.iterate(0, i -> i + 1).limit(100000).parallel();
    IntPredicate predicate = ignored -> r.nextBoolean();

    Map<Boolean, List<Integer>> groups = intStream.collect(
            () -> Map.of(false, new ArrayList<>(100000),
                         true , new ArrayList<>(100000)),
            (map, value) -> map.get(predicate.test(value)).add(value),
            (map1, map2) -> {
                map1.get(false).addAll(map2.get(false));
                map1.get(true ).addAll(map2.get(true ));
            });

In questo esempio, inizializzo gli ArrayLists con l'intera dimensione della raccolta iniziale (se questo è noto). Questo impedisce gli eventi di ridimensionamento anche nello scenario peggiore, ma può potenzialmente inghiottire 2 * N * T spazio (N = numero iniziale di elementi, T = numero di thread). Per compensare lo spazio per la velocità, puoi lasciarlo fuori o usare la tua ipotesi più istruita, come il numero più alto previsto di elementi in una partizione (in genere appena sopra N / 2 per una divisione bilanciata).

Spero di non offendere nessuno usando un metodo Java 9. Per la versione Java 8, guarda la cronologia delle modifiche.


2
Bellissimo. Tuttavia, l'ultima soluzione per IntStream non sarà thread-safe nel caso di un flusso parallelizzato. La soluzione è molto più semplice di quanto pensi che sia ... stream.boxed().collect(...);! Farà come pubblicizzato: converti la primitiva IntStreamnella Stream<Integer>versione in scatola .
YoYo

32
Questa dovrebbe essere la risposta accettata in quanto risolve direttamente la domanda del PO.
ejel,

27
Vorrei che Stack Overflow consentisse alla comunità di ignorare la risposta selezionata se ne viene trovata una migliore.
GuiSim,

Non sono sicuro che questo risponda alla domanda. La domanda richiede la suddivisione di un flusso in stream, non in elenchi.
AlikElzin-Kilaka,

1
La funzione accumulatore è inutilmente dettagliata. Invece di (map, x) -> { boolean partition = p.test(x); List<Integer> list = map.get(partition); list.add(x); }te puoi semplicemente usare (map, x) -> map.get(p.test(x)).add(x). Inoltre, non vedo alcun motivo per cui l' collectoperazione non dovrebbe essere thread-safe. Funziona esattamente come dovrebbe funzionare e molto vicino a come Collectors.partitioningBy(p)funzionerebbe. Ma userei un IntPredicateinvece di Predicate<Integer>quando non lo uso boxed(), per evitare la boxe due volte.
Holger,

21

Mi sono imbattuto in questa domanda per me stesso e sento che un flusso biforcuto ha alcuni casi d'uso che potrebbero dimostrarsi validi. Ho scritto il codice qui sotto come consumatore in modo che non faccia nulla ma tu possa applicarlo a funzioni e qualsiasi altra cosa che potresti incontrare.

class PredicateSplitterConsumer<T> implements Consumer<T>
{
  private Predicate<T> predicate;
  private Consumer<T>  positiveConsumer;
  private Consumer<T>  negativeConsumer;

  public PredicateSplitterConsumer(Predicate<T> predicate, Consumer<T> positive, Consumer<T> negative)
  {
    this.predicate = predicate;
    this.positiveConsumer = positive;
    this.negativeConsumer = negative;
  }

  @Override
  public void accept(T t)
  {
    if (predicate.test(t))
    {
      positiveConsumer.accept(t);
    }
    else
    {
      negativeConsumer.accept(t);
    }
  }
}

Ora l'implementazione del codice potrebbe essere simile a questa:

personsArray.forEach(
        new PredicateSplitterConsumer<>(
            person -> person.getDateOfBirth().isPresent(),
            person -> System.out.println(person.getName()),
            person -> System.out.println(person.getName() + " does not have Date of birth")));

20

Sfortunatamente, ciò che chiedi è direttamente disapprovato nel JavaDoc di Stream :

Un flusso deve essere gestito (invocando un'operazione di flusso intermedio o terminale) una sola volta. Questo esclude, ad esempio, flussi "biforcati", in cui la stessa sorgente alimenta due o più condotte o più attraversamenti dello stesso flusso.

Puoi aggirare questo usando peeko altri metodi se desideri veramente quel tipo di comportamento. In questo caso, ciò che dovresti fare è invece di provare a eseguire il backup di due stream dalla stessa sorgente Stream originale con un filtro di fork, duplicando il flusso e filtrando ciascuno dei duplicati in modo appropriato.

Tuttavia, potresti voler riconsiderare se a Streamè la struttura appropriata per il tuo caso d'uso.


6
La formulazione javadoc non esclude il partizionamento in diversi flussi, purché un singolo elemento di flusso vada solo in uno di questi
Thorbjørn Ravn Andersen,

2
@ ThorbjørnRavnAndersen Non sono sicuro che la duplicazione di un elemento stream sia il principale impedimento per un flusso biforcuto. Il problema principale è che l'operazione di fork è essenzialmente un'operazione terminal, quindi quando decidi di fare fork stai fondamentalmente creando una raccolta di qualche tipo. Ad esempio, posso scrivere un metodo, List<Stream> forkStream(Stream s)ma i miei flussi risultanti saranno almeno parzialmente supportati da raccolte e non direttamente dal flusso sottostante, al contrario di dire filterche non è un'operazione di flusso terminale.
Trevor Freeman,

7
Questo è uno dei motivi per cui ritengo che i flussi Java siano un po 'per metà paragonati a github.com/ReactiveX/RxJava/wiki perché lo scopo del flusso è applicare operazioni su un insieme potenzialmente infinito di elementi e le operazioni del mondo reale spesso richiedono una divisione , duplicazione e fusione di flussi.
Usman Ismail,

8

Questo è contro il meccanismo generale di Stream. Supponi di poter dividere Stream S0 in Sa e Sb come desideri. Eseguire qualsiasi operazione terminale, diciamo count(), su Sa necessariamente "consumerà" tutti gli elementi in S0. Pertanto Sb ha perso la sua fonte di dati.

In precedenza, Stream aveva un tee()metodo, credo, che duplicava uno stream in due. È stato rimosso ora.

Stream ha un metodo peek (), tuttavia potresti essere in grado di usarlo per raggiungere i tuoi requisiti.


1
peekè esattamente quello che era tee.
Louis Wasserman,

5

non esattamente, ma potresti essere in grado di realizzare ciò di cui hai bisogno invocando Collectors.groupingBy(). crei una nuova raccolta e puoi quindi istanziare i flussi su quella nuova raccolta.


2

Questa è stata la risposta meno negativa che ho potuto trovare.

import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;

public class Test {

    public static <T, L, R> Pair<L, R> splitStream(Stream<T> inputStream, Predicate<T> predicate,
            Function<Stream<T>, L> trueStreamProcessor, Function<Stream<T>, R> falseStreamProcessor) {

        Map<Boolean, List<T>> partitioned = inputStream.collect(Collectors.partitioningBy(predicate));
        L trueResult = trueStreamProcessor.apply(partitioned.get(Boolean.TRUE).stream());
        R falseResult = falseStreamProcessor.apply(partitioned.get(Boolean.FALSE).stream());

        return new ImmutablePair<L, R>(trueResult, falseResult);
    }

    public static void main(String[] args) {

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

        Pair<List<Integer>, String> results = splitStream(stream,
                n -> n > 5,
                s -> s.filter(n -> n % 2 == 0).collect(Collectors.toList()),
                s -> s.map(n -> n.toString()).collect(Collectors.joining("|")));

        System.out.println(results);
    }

}

Questo prende un flusso di numeri interi e li divide in 5. Per quelli maggiori di 5 filtra solo i numeri pari e li mette in un elenco. Per il resto li unisce a |.

uscite:

 ([6, 8],0|1|2|3|4|5)

Non è l'ideale in quanto raccoglie tutto in raccolte intermedie che interrompono il flusso (e ha troppi argomenti!)


1

Mi sono imbattuto in questa domanda mentre cercavo un modo per filtrare determinati elementi da un flusso e registrarli come errori. Quindi non avevo davvero bisogno di dividere lo stream tanto quanto collegare un'azione di terminazione prematura a un predicato con sintassi discreta. Questo è quello che mi è venuto in mente:

public class MyProcess {
    /* Return a Predicate that performs a bail-out action on non-matching items. */
    private static <T> Predicate<T> withAltAction(Predicate<T> pred, Consumer<T> altAction) {
    return x -> {
        if (pred.test(x)) {
            return true;
        }
        altAction.accept(x);
        return false;
    };

    /* Example usage in non-trivial pipeline */
    public void processItems(Stream<Item> stream) {
        stream.filter(Objects::nonNull)
              .peek(this::logItem)
              .map(Item::getSubItems)
              .filter(withAltAction(SubItem::isValid,
                                    i -> logError(i, "Invalid")))
              .peek(this::logSubItem)
              .filter(withAltAction(i -> i.size() > 10,
                                    i -> logError(i, "Too large")))
              .map(SubItem::toDisplayItem)
              .forEach(this::display);
    }
}

0

Versione più breve che utilizza Lombok

import java.util.function.Consumer;
import java.util.function.Predicate;

import lombok.RequiredArgsConstructor;

/**
 * Forks a Stream using a Predicate into postive and negative outcomes.
 */
@RequiredArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PROTECTED)
public class StreamForkerUtil<T> implements Consumer<T> {
    Predicate<T> predicate;
    Consumer<T> positiveConsumer;
    Consumer<T> negativeConsumer;

    @Override
    public void accept(T t) {
        (predicate.test(t) ? positiveConsumer : negativeConsumer).accept(t);
    }
}

-3

Che ne dite di:

Supplier<Stream<Integer>> randomIntsStreamSupplier =
    () -> (new Random()).ints(0, 2).boxed();

Stream<Integer> tails =
    randomIntsStreamSupplier.get().filter(x->x.equals(0));
Stream<Integer> heads =
    randomIntsStreamSupplier.get().filter(x->x.equals(1));

1
Dal momento che il fornitore viene chiamato due volte, otterrai due diverse raccolte casuali. Penso che sia la mente dell'OP a dividere le probabilità dai pari nella stessa sequenza generata
usr-local-ΕΨΗΕΛΩΝ
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.