Você pode dividir um fluxo em dois fluxos?

146

Eu tenho um conjunto de dados representado por um fluxo Java 8:

Stream<T> stream = ...;

Eu posso ver como filtrá-lo para obter um subconjunto aleatório - por exemplo

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

Também posso ver como reduzir esse fluxo para obter, por exemplo, duas listas representando duas metades aleatórias do conjunto de dados e depois transformá-las novamente em fluxos. Mas, existe uma maneira direta de gerar dois fluxos a partir do inicial? Algo como

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

Obrigado por qualquer insight.

user1148758
fonte
A resposta de Mark é muito útil que a resposta de Louis, mas devo dizer que a de Louis está mais relacionada à pergunta original. A pergunta é bastante focada na possibilidade de converter Streamem múltiplos Streams sem conversão intermediária , embora eu ache que as pessoas que chegaram a essa pergunta estão realmente procurando o caminho para alcançá-lo, independentemente de tal restrição, que é a resposta de Mark. Isso pode ser devido ao fato de a pergunta no título não ser a mesma da descrição .
devildelta 17/01

Respostas:

9

Não exatamente. Você não pode obter dois Streams de um; isso não faz sentido - como você iteraria uma sem precisar gerar a outra ao mesmo tempo? Um fluxo pode ser operado apenas uma vez.

No entanto, se você deseja despejá-los em uma lista ou algo assim, você pode fazer

stream.forEach((x) -> ((x == 0) ? heads : tails).add(x));
Louis Wasserman
fonte
65
Por que isso não faz sentido? Como um fluxo é um pipeline, não há razão para que ele não tenha criado dois produtores do fluxo original, eu pude ver isso sendo tratado por um coletor que fornece dois fluxos.
Brett Ryan
36
Não é seguro para discussão. Um mau conselho ao tentar adicionar diretamente a uma coleção, é por isso que temos o stream.collect(...)thread-safe predefinido Collectors, que funciona bem mesmo em coleções não thread-safe (sem contenção de bloqueio sincronizado). Melhor resposta por @MarkJeronimus.
YoYo
1
@JoD É seguro para roscas se cabeças e rabos são seguros para roscas. Além disso, assumindo o uso de fluxos não paralelos, apenas o pedido não é garantido, portanto, eles são seguros para threads. Cabe ao programador corrigir problemas de simultaneidade, portanto, essa resposta é perfeitamente adequada se as coleções forem seguras para threads.
Nicolas
1
@ Nixon não é adequado na presença de uma solução melhor, que temos aqui. Ter esse código pode levar a um mau precedente, fazendo com que outras pessoas o usem de maneira errada. Mesmo que nenhum fluxo paralelo seja usado, ele está a apenas um passo de distância. Boas práticas de codificação exigem que não mantenhamos o estado durante as operações de fluxo. A próxima coisa que fazemos é codificar em uma estrutura como o Apache spark, e as mesmas práticas realmente levariam a resultados inesperados. Foi uma solução criativa, eu acredito, uma que eu poderia ter me escrito há não muito tempo.
YoYo 16/02
1
@JoD Não é uma solução melhor, é efetivamente mais ineficiente. Essa linha de raciocínio acaba com a conclusão de que todas as coleções devem ser protegidas por thread por padrão para evitar consequências indesejadas, o que é simplesmente errado.
Nicolas
301

Um coletor pode ser usado para isso.

  • Para duas categorias, use Collectors.partitioningBy()factory.

Isso criará um Mapde Booleanpara Liste colocará itens em uma ou outra lista com base em a Predicate.

Nota: Como o fluxo precisa ser consumido inteiro, isso não pode funcionar em fluxos infinitos. E como o fluxo é consumido de qualquer maneira, esse método simplesmente os coloca em Listas, em vez de criar um novo fluxo com memória. Você sempre pode transmitir essas listas se precisar de fluxos como saída.

Além disso, não há necessidade do iterador, nem mesmo no exemplo somente de cabeçote que você forneceu.

  • A divisão binária é assim:
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());
  • Para mais categorias, use uma Collectors.groupingBy()fábrica.
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());

Caso os fluxos não sejam Stream, mas um dos fluxos primitivos IntStream, esse .collect(Collectors)método não estará disponível. Você terá que fazer isso da maneira manual sem uma fábrica de colecionadores. Sua implementação é assim:

[Exemplo 2.0 desde 16/04 2020]

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

Neste exemplo, inicializo o ArrayLists com o tamanho total da coleção inicial (se é que isso é conhecido). Isso evita eventos de redimensionamento, mesmo no pior cenário, mas pode potencialmente devorar 2 * N * T de espaço (N = número inicial de elementos, T = número de encadeamentos). Para trocar o espaço pela velocidade, você pode deixá-lo de fora ou usar seu palpite melhor, como o maior número esperado de elementos em uma partição (normalmente um pouco acima de N / 2 para uma divisão equilibrada).

Espero não ofender ninguém usando o método Java 9. Para a versão do Java 8, consulte o histórico de edições.

Mark Jeronimus
fonte
2
Lindo. No entanto, a última solução para o IntStream não será segura para threads no caso de um fluxo paralelo. A solução é muito mais simples do que você pensa ... stream.boxed().collect(...);! Ele fará o que foi anunciado: converta o primitivo IntStreampara a Stream<Integer>versão em caixa .
YoYo
32
Essa deve ser a resposta aceita, pois resolve diretamente a questão do OP.
ejel
27
Desejo que o Stack Overflow permita que a comunidade substitua a resposta selecionada se for encontrada uma melhor.
precisa saber é
Não tenho certeza se isso responde à pergunta. A pergunta solicita a divisão de um fluxo em fluxos - não em Listas.
AlikElzin-Kilaka
1
A função acumulador é desnecessariamente detalhada. Em vez de (map, x) -> { boolean partition = p.test(x); List<Integer> list = map.get(partition); list.add(x); }você pode simplesmente usar (map, x) -> map.get(p.test(x)).add(x). Além disso, não vejo nenhum motivo para a collectoperação não ser segura para threads. Funciona exatamente como deveria funcionar e muito próximo de como Collectors.partitioningBy(p)funcionaria. Mas eu usaria um em IntPredicatevez de Predicate<Integer>quando não estiver usando boxed(), para evitar o boxe duas vezes.
Holger
21

Eu me deparei com essa pergunta e sinto que um fluxo bifurcado tem alguns casos de uso que podem ser válidos. Escrevi o código abaixo como consumidor para que ele não faça nada, mas você pode aplicá-lo a funções e qualquer outra coisa que possa encontrar.

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

Agora sua implementação de código pode ser algo como isto:

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")));
Ludger
fonte
20

Infelizmente, o que você pede é desaprovado diretamente no JavaDoc of Stream :

Um fluxo deve ser operado (invocando uma operação de fluxo intermediária ou terminal) apenas uma vez. Isso exclui, por exemplo, fluxos "bifurcados", nos quais a mesma fonte alimenta dois ou mais pipelines, ou vários percursos do mesmo fluxo.

Você pode solucionar esse problema usando peekoutros métodos, se realmente desejar esse tipo de comportamento. Nesse caso, o que você deve fazer é, em vez de tentar fazer backup de dois fluxos da mesma fonte original do fluxo com um filtro de bifurcação, você duplicaria o fluxo e filtraria cada uma das duplicatas adequadamente.

No entanto, convém reconsiderar se a Streamé a estrutura apropriada para seu caso de uso.

Trevor Freeman
fonte
6
A formulação javadoc não exclui o particionamento em várias correntes, desde que um único item fluxo vai apenas em uma delas
Thorbjørn Ravn Andersen
2
@ ThorbjørnRavnAndersen Não tenho certeza se a duplicação de um item de fluxo é o principal impedimento para um fluxo bifurcado. A questão principal é que a operação de bifurcação é essencialmente uma operação de terminal; portanto, quando você decide bifurcar, está basicamente criando uma coleção de algum tipo. Por exemplo, eu posso escrever um método, List<Stream> forkStream(Stream s)mas meus fluxos resultantes serão pelo menos parcialmente apoiados por coleções e não diretamente pelo fluxo subjacente, em vez de dizer filterque não é uma operação de fluxo terminal.
Trevor Freeman
7
Essa é uma das razões pelas quais sinto que os fluxos Java são um pouco medíocres em comparação com o github.com/ReactiveX/RxJava/wiki, porque o ponto do fluxo é aplicar operações em um conjunto potencialmente infinito de elementos e as operações do mundo real frequentemente exigem divisão , duplicando e mesclando fluxos.
precisa saber é o seguinte
8

Isso é contra o mecanismo geral do Stream. Digamos que você possa dividir o Stream S0 em Sa e Sb como desejar. Executar qualquer operação do terminal, digamos count(), no Sa, necessariamente "consumirá" todos os elementos em S0. Portanto, o Sb perdeu sua fonte de dados.

Anteriormente, tee()eu acho que o Stream tinha um método que duplicava um stream para dois. Foi removido agora.

O Stream possui um método peek (); talvez você possa usá-lo para atender aos seus requisitos.

ZhongYu
fonte
1
peeké exatamente o que costumava ser tee.
Louis Wasserman
5

não exatamente, mas você pode conseguir o que precisa invocando Collectors.groupingBy(). você cria uma nova coleção e pode instanciar fluxos nessa nova coleção.

aepurniet
fonte
2

Essa foi a resposta menos ruim que eu consegui encontrar.

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

}

Isso pega um fluxo de números inteiros e os divide em 5. Para aqueles com mais de 5, ele filtra apenas números pares e os coloca em uma lista. De resto, junta-os a |.

saídas:

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

Não é o ideal, pois reúne tudo em coleções intermediárias que quebram o fluxo (e tem muitos argumentos!)

Ian Jones
fonte
1

Eu me deparei com essa pergunta enquanto procurava uma maneira de filtrar certos elementos de um fluxo e registrá-los como erros. Portanto, eu realmente não precisava dividir o fluxo, mas anexar uma ação de encerramento prematura a um predicado com sintaxe discreta. Isto é o que eu vim com:

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);
    }
}
Sebastian Hans
fonte
0

Versão mais curta que usa 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);
    }
}
OneCricketeer
fonte
-3

E se:

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));
Mateus
fonte
1
Como o fornecedor é chamado duas vezes, você receberá duas coleções aleatórias diferentes. Eu acho que é a mente do OP para dividir as probabilidades dos nivela na mesma seqüência gerada
ΕΨΗΕΛΩΝ usr-local-