Copie um fluxo para evitar que "o fluxo já tenha sido operado ou fechado"


121

Eu gostaria de duplicar um fluxo do Java 8 para poder lidar com ele duas vezes. Eu posso collectcomo uma lista e obter novos fluxos a partir disso;

// doSomething() returns a stream
List<A> thing = doSomething().collect(toList());
thing.stream()... // do stuff
thing.stream()... // do other stuff

Mas acho que deveria haver uma maneira mais eficiente / elegante.

Existe uma maneira de copiar o fluxo sem transformá-lo em uma coleção?

Na verdade, estou trabalhando com um fluxo de Eithers, então, quero processar a projeção esquerda de uma maneira antes de passar para a projeção direita e lidar com essa outra maneira. Mais ou menos assim (com o qual até agora sou forçado a usar o toListtruque).

List<Either<Pair<A, Throwable>, A>> results = doSomething().collect(toList());

Stream<Pair<A, Throwable>> failures = results.stream().flatMap(either -> either.left());
failures.forEach(failure -> ... );

Stream<A> successes = results.stream().flatMap(either -> either.right());
successes.forEach(success -> ... );

Você poderia elaborar mais sobre "processar uma maneira" ... você está consumindo os objetos? Mapeando-os? partitionBy () e groupingBy () podem levá-lo diretamente para mais de 2 listas, mas você pode se beneficiar do mapeamento primeiro ou apenas com uma bifurcação de decisão em seu forEach ().
AjahnCharles

Em alguns casos, transformá-lo em uma coleção não poderia ser uma opção se estivermos lidando com fluxo infinito. Você pode encontrar uma alternativa para memorização aqui: dzone.com/articles/how-to-replay-java-streams
Miguel Gamboa

Respostas:


88

Eu acho que sua suposição sobre eficiência é meio atrasada. Você obtém esse enorme retorno de eficiência se quiser usar os dados apenas uma vez, porque não precisa armazená-los, e os fluxos oferecem otimizações poderosas de "fusão de loop" que permitem que você flua os dados inteiros de maneira eficiente pelo pipeline.

Se você deseja reutilizar os mesmos dados, por definição, é necessário gerá-los duas vezes (deterministicamente) ou armazená-los. Se já estiver em uma coleção, ótimo; iterá-lo duas vezes é barato.

Nós experimentamos o design com "fluxos bifurcados". O que descobrimos foi que apoiar isso tinha custos reais; sobrecarregou o caso comum (use uma vez) às custas do caso incomum. O grande problema foi lidar com "o que acontece quando os dois pipelines não consomem dados na mesma taxa". Agora você voltou ao buffer de qualquer maneira. Essa era uma característica que claramente não carregava seu peso.

Se você deseja operar os mesmos dados repetidamente, armazene-os ou estruture suas operações como Consumidores e faça o seguinte:

stream()...stuff....forEach(e -> { consumerA(e); consumerB(e); });

Você também pode procurar na biblioteca RxJava, pois seu modelo de processamento se presta melhor a esse tipo de "fluxo de bifurcação".


1
Talvez eu não devesse ter usado "eficiência", estou entendendo por que me incomodaria com fluxos (e não armazenaria nada) se tudo o que faço é armazenar imediatamente os dados ( toList) para poder processá-los (o Eithercaso sendo o exemplo)?
Toby

11
Os fluxos são expressivos e eficientes . Eles são expressivos na medida em que permitem configurar operações agregadas complexas sem muitos detalhes acidentais (por exemplo, resultados intermediários) na maneira de ler o código. Eles também são eficientes, na medida em que (geralmente) fazem uma única passagem nos dados e não preenchem os contêineres de resultados intermediários. Essas duas propriedades juntas os tornam um modelo de programação atraente para muitas situações. Obviamente, nem todos os modelos de programação atendem a todos os problemas; você ainda precisa decidir se está usando uma ferramenta apropriada para o trabalho.
Brian Goetz

1
Mas a incapacidade de reutilizar um fluxo causa situações em que o desenvolvedor é forçado a armazenar resultados intermediários (coleta) para processar um fluxo de duas maneiras diferentes. A implicação de que o fluxo é gerado mais de uma vez (a menos que você o colete) parece clara - porque, caso contrário, você não precisaria de um método de coleta.
Niall Connaughton

@NiallConnaughton Não tenho certeza se o seu ponto é. Se você deseja atravessá-lo duas vezes, alguém precisa armazená-lo ou você precisa regenerá-lo. Você está sugerindo que a biblioteca a armazene em buffer caso alguém precise dela duas vezes? Isso seria bobagem.
Brian Goetz

Não sugerindo que a biblioteca o armazene em buffer, mas dizendo que, ao ter fluxos únicos, obriga as pessoas que desejam reutilizar um fluxo inicial (ou seja: compartilhando a lógica declarativa usada para defini-lo) a construir vários fluxos derivados para coletar o fluxo de sementes ou tenha acesso a uma fábrica de fornecedores que criará uma duplicata do fluxo de sementes. Ambas as opções têm seus pontos negativos. Esta resposta tem muito mais detalhes sobre o tópico: stackoverflow.com/a/28513908/114200 .
Niall Connaughton

73

Você pode usar uma variável local com a Supplierpara configurar partes comuns do pipeline de fluxo.

De http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/ :

Reutilizando fluxos

Os fluxos Java 8 não podem ser reutilizados. Assim que você chama qualquer operação do terminal, o fluxo é fechado:

Stream<String> stream = Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> s.startsWith("a"));
stream.anyMatch(s -> true);    // ok
stream.noneMatch(s -> true);   // exception

Calling `noneMatch` after `anyMatch` on the same stream results in the following exception:
java.lang.IllegalStateException: stream has already been operated upon or closed
at 
java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
at 
java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459)
at com.winterbe.java8.Streams5.test7(Streams5.java:38)
at com.winterbe.java8.Streams5.main(Streams5.java:28)

Para superar essa limitação, precisamos criar uma nova cadeia de fluxo para cada operação do terminal que desejamos executar; por exemplo, poderíamos criar um fornecedor de fluxo para construir um novo fluxo com todas as operações intermediárias já configuradas:

Supplier<Stream<String>> streamSupplier =
    () -> Stream.of("d2", "a2", "b1", "b3", "c")
            .filter(s -> s.startsWith("a"));

streamSupplier.get().anyMatch(s -> true);   // ok
streamSupplier.get().noneMatch(s -> true);  // ok

Cada chamada para get()constrói um novo fluxo no qual somos salvos para chamar a operação de terminal desejada.


2
solução agradável e elegante. muito mais java8-ish do que a solução mais votada.
precisa saber é o seguinte

Apenas uma observação sobre o uso Supplierse o modelo Streamfor construído de maneira "cara", você paga esse custo por cada chamada paraSupplier.get() . ou seja, se uma consulta ao banco de dados ... essa consulta é feita a cada vez
Julien

Parece que você não consegue seguir esse padrão depois de um mapTo usando um IntStream. Eu descobri que tinha que convertê-lo novamente em um Set<Integer>uso collect(Collectors.toSet())... e fazer algumas operações nisso. Eu queria max()e se um valor específico estivesse definido como duas operações ...filter(d -> d == -1).count() == 1;
JGFMK

16

Use a Supplierpara produzir o fluxo para cada operação de encerramento.

Supplier<Stream<Integer>> streamSupplier = () -> list.stream();

Sempre que você precisar de um fluxo dessa coleção, use streamSupplier.get()para obter um novo fluxo.

Exemplos:

  1. streamSupplier.get().anyMatch(predicate);
  2. streamSupplier.get().allMatch(predicate2);

Voto-lhe como você é o primeiro a apontar Fornecedores aqui.
EnzoBnl 03/02

9

Implementamos um duplicate()método para fluxos no jOOλ , uma biblioteca de código aberto que criamos para aprimorar os testes de integração para o jOOQ . Basicamente, você pode escrever:

Tuple2<Seq<A>, Seq<A>> duplicates = Seq.seq(doSomething()).duplicate();

Internamente, há um buffer armazenando todos os valores que foram consumidos de um fluxo, mas não do outro. Isso é provavelmente o mais eficiente possível, se seus dois fluxos forem consumidos aproximadamente na mesma taxa e se você puder viver com a falta de segurança de threads .

Veja como o algoritmo funciona:

static <T> Tuple2<Seq<T>, Seq<T>> duplicate(Stream<T> stream) {
    final List<T> gap = new LinkedList<>();
    final Iterator<T> it = stream.iterator();

    @SuppressWarnings("unchecked")
    final Iterator<T>[] ahead = new Iterator[] { null };

    class Duplicate implements Iterator<T> {
        @Override
        public boolean hasNext() {
            if (ahead[0] == null || ahead[0] == this)
                return it.hasNext();

            return !gap.isEmpty();
        }

        @Override
        public T next() {
            if (ahead[0] == null)
                ahead[0] = this;

            if (ahead[0] == this) {
                T value = it.next();
                gap.offer(value);
                return value;
            }

            return gap.poll();
        }
    }

    return tuple(seq(new Duplicate()), seq(new Duplicate()));
}

Mais código fonte aqui

Tuple2é, provavelmente, como o seu Pairtipo, enquanto Seqé Streamcom algumas melhorias.


2
Esta solução não é segura para threads: você não pode passar um dos fluxos para outro thread. Realmente não vejo cenário em que ambos os fluxos possam ser consumidos na mesma taxa em um único thread e você realmente precise de dois fluxos distintos. Se você deseja produzir dois resultados do mesmo fluxo, seria muito melhor usar coletores combinados (que você já possui no JOOL).
Tagir Valeev

@TagirValeev: Você está certo sobre a segurança da rosca, bom ponto. Como isso pode ser feito com a combinação de colecionadores?
Lukas Eder

1
Quero dizer, se alguém quiser usar o mesmo fluxo duas vezes assim Tuple2<Seq<A>>, Seq<A>> t = duplicate(stream); long count = t.collect(counting()); List<A> list = t.collect(toList());, é melhor usar Tuple2<Long, List<A>> t = stream.collect(Tuple.collectors(counting(), toList()));. O uso de Collectors.mapping/reducingum pode expressar outras operações de fluxo como coletores e elementos de processo de maneira bastante diferente, criando uma tupla resultante única. Portanto, em geral, você pode fazer muitas coisas consumindo o fluxo uma vez sem duplicação e será compatível com paralelos.
Tagir Valeev

2
Nesse caso, você ainda reduzirá um fluxo após o outro. Portanto, não há motivo para tornar a vida mais difícil, introduzindo o iterador sofisticado, que de qualquer maneira coletará todo o fluxo da lista sob o capô. Você pode coletar explicitamente a lista e criar dois fluxos a partir dela, como o OP diz (é o mesmo número de linhas de código). Bem, você pode ter alguma melhoria se a primeira redução for um curto-circuito, mas não é o caso do OP.
Tagir Valeev

1
@ maaartinus: Obrigado, bom ponteiro. Eu criei um problema para o benchmark. Usei-o para o offer()/ poll()API, mas um ArrayDequepode fazer o mesmo.
Lukas Eder

7

Você pode criar um fluxo de executáveis ​​(por exemplo):

results.stream()
    .flatMap(either -> Stream.<Runnable> of(
            () -> failure(either.left()),
            () -> success(either.right())))
    .forEach(Runnable::run);

Onde failuree successas operações a serem aplicadas. No entanto, isso criará alguns objetos temporários e pode não ser mais eficiente do que iniciar uma coleção e transmiti-la / iterá-la duas vezes.


4

Outra maneira de lidar com os elementos várias vezes é usar Stream.peek (Consumer) :

doSomething().stream()
.peek(either -> handleFailure(either.left()))
.foreach(either -> handleSuccess(either.right()));

peek(Consumer) pode ser encadeado quantas vezes for necessário.

doSomething().stream()
.peek(element -> handleFoo(element.foo()))
.peek(element -> handleBar(element.bar()))
.peek(element -> handleBaz(element.baz()))
.foreach(element-> handleQux(element.qux()));

Parece que o peek não deve ser usado para isso (consulte softwareengineering.stackexchange.com/a/308979/195787 ) #
955 HectorJ

2
@HectorJ O outro segmento é sobre a modificação de elementos. Eu assumi que isso não é feito aqui.
Martin Martin

2

O cyclops-react , uma biblioteca na qual contribuo, possui um método estático que permite duplicar um fluxo (e retorna uma tupla de fluxos).

    Stream<Integer> stream = Stream.of(1,2,3);
    Tuple2<Stream<Integer>,Stream<Integer>> streams =  StreamUtils.duplicate(stream);

Veja os comentários, há uma penalidade de desempenho que será incorrida ao usar duplicado em um fluxo existente. Uma alternativa mais eficiente seria usar o Streamable: -

Há também uma classe Streamable (lenta) que pode ser construída a partir de um Stream, Iterable ou Array e reproduzida várias vezes.

    Streamable<Integer> streamable = Streamable.of(1,2,3);
    streamable.stream().forEach(System.out::println);
    streamable.stream().forEach(System.out::println);

AsStreamable.synchronizedFromStream (stream) - pode ser usado para criar um Streamable que preencherá lentamente sua coleção de backups, de maneira que possa ser compartilhada entre os threads. Streamable.fromStream (stream) não sofrerá nenhuma sobrecarga de sincronização.


2
E, é claro, deve-se notar que os fluxos resultantes têm sobrecarga significativa de CPU / memória e desempenho paralelo muito ruim. Além disso, esta solução não é segura para threads (você não pode passar um dos fluxos resultantes para outro thread e processá-lo com segurança em paralelo). Seria muito mais eficiente e seguro List<Integer> list = stream.collect(Collectors.toList()); streams = new Tuple2<>(list.stream(), list.stream())(como sugere a OP). Além disso, divulgue explicitamente na resposta que você é o autor do cyclop-streams. Leia isto .
Tagir Valeev

Atualizado para refletir que sou o autor. Também é um bom ponto para discutir as características de desempenho de cada um. Sua avaliação acima é muito boa para StreamUtils.duplicate. StreamUtils.duplicate funciona armazenando em buffer os dados de um fluxo para outro, incorrendo em uma sobrecarga da CPU e da memória (dependendo do caso de uso). No entanto, para Streamable.of (1,2,3), um novo Stream é criado diretamente a partir da matriz e as características de desempenho, incluindo desempenho paralelo, serão as mesmas do Stream normalmente criado.
John McClean

Além disso, existe uma classe AsStreamable que permite a criação de uma instância Streamable a partir de um Stream, mas sincroniza o acesso à coleção que faz o backup do Streamable conforme ele é criado (AsStreamable.synchronizedFromStream). Tornando-o mais adequado para uso em threads (se é isso que você precisa - eu imaginaria 99% do tempo em que os Streams são criados e reutilizados no mesmo thread).
John McClean

Oi Tagir - você também não deve divulgar em seu comentário que é o autor de uma biblioteca concorrente?
John McClean

1
Comentários não são respostas e eu não anuncio minha biblioteca aqui, pois minha biblioteca não possui um recurso para duplicar o fluxo (apenas porque acho que é inútil), para não competirmos aqui. É claro que quando proponho uma solução envolvendo minha biblioteca, sempre digo explicitamente que sou o autor.
Tagir Valeev

0

Para esse problema específico, você também pode usar o particionamento. Algo como

     // Partition Eighters into left and right
     List<Either<Pair<A, Throwable>, A>> results = doSomething();
     Map<Boolean, Object> passingFailing = results.collect(Collectors.partitioningBy(s -> s.isLeft()));
     passingFailing.get(true) <- here will be all passing (left values)
     passingFailing.get(false) <- here will be all failing (right values)

0

Podemos fazer uso do Stream Builder no momento da leitura ou iteração de um fluxo. Aqui está o documento de Stream Builder .

https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.Builder.html

Caso de uso

Digamos que temos fluxo de funcionários e precisamos usá-lo para gravar dados de funcionários no arquivo do Excel e, em seguida, atualizar a coleção / tabela de funcionários [Este é apenas um caso de uso para mostrar o uso do Stream Builder]:

Stream.Builder<Employee> builder = Stream.builder();

employee.forEach( emp -> {
   //store employee data to excel file 
   // and use the same object to build the stream.
   builder.add(emp);
});

//Now this stream can be used to update the employee collection
Stream<Employee> newStream = builder.build();

0

Eu tive um problema semelhante e pude pensar em três estruturas intermediárias diferentes para criar uma cópia do fluxo: a List, uma matriz e a Stream.Builder. Eu escrevi um pequeno programa de benchmark, que sugeria que, do ponto de vista do desempenho, Listera cerca de 30% mais lento que os outros dois, que eram bastante semelhantes.

A única desvantagem da conversão para uma matriz é que é complicado se o seu tipo de elemento for um tipo genérico (que no meu caso era); portanto, eu prefiro usar a Stream.Builder.

Acabei escrevendo uma pequena função que cria um Collector:

private static <T> Collector<T, Stream.Builder<T>, Stream<T>> copyCollector()
{
    return Collector.of(Stream::builder, Stream.Builder::add, (b1, b2) -> {
        b2.build().forEach(b1);
        return b1;
    }, Stream.Builder::build);
}

Posso então fazer uma cópia de qualquer fluxo str, fazendo o str.collect(copyCollector())que parece bastante com o uso idiomático dos fluxos.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.