Incluindo dois fluxos Java 8 ou um elemento extra em um fluxo

168

Posso adicionar fluxos ou elementos extras, como este:

Stream stream = Stream.concat(stream1, Stream.concat(stream2, Stream.of(element));

E posso adicionar coisas novas à medida que vou, assim:

Stream stream = Stream.concat(
                       Stream.concat(
                              stream1.filter(x -> x!=0), stream2)
                              .filter(x -> x!=1),
                                  Stream.of(element))
                                  .filter(x -> x!=2);

Mas isso é feio, porque concaté estático. Se concatfosse um método de instância, os exemplos acima seriam muito mais fáceis de ler:

 Stream stream = stream1.concat(stream2).concat(element);

E

 Stream stream = stream1
                 .filter(x -> x!=0)
                 .concat(stream2)
                 .filter(x -> x!=1)
                 .concat(element)
                 .filter(x -> x!=2);

Minha pergunta é:

1) Existe alguma boa razão para concat ser estático? Ou existe algum método de instância equivalente que estou faltando?

2) De qualquer forma, existe uma maneira melhor de fazer isso?

MarcG
fonte
4
Parece que as coisas nem sempre foram assim , mas não consigo encontrar o motivo.
Edwin Dalorzo 30/03

Respostas:

126

Se você adicionar importações estáticas para Stream.concat e Stream.of , o primeiro exemplo poderá ser escrito da seguinte maneira:

Stream<Foo> stream = concat(stream1, concat(stream2, of(element)));

A importação de métodos estáticos com nomes genéricos pode resultar em um código que fica difícil de ler e manter ( poluição do namespace ). Portanto, pode ser melhor criar seus próprios métodos estáticos com nomes mais significativos. No entanto, para demonstração, continuarei com esse nome.

public static <T> Stream<T> concat(Stream<? extends T> lhs, Stream<? extends T> rhs) {
    return Stream.concat(lhs, rhs);
}
public static <T> Stream<T> concat(Stream<? extends T> lhs, T rhs) {
    return Stream.concat(lhs, Stream.of(rhs));
}

Com esses dois métodos estáticos (opcionalmente em combinação com importações estáticas), os dois exemplos podem ser escritos da seguinte maneira:

Stream<Foo> stream = concat(stream1, concat(stream2, element));

Stream<Foo> stream = concat(
                         concat(stream1.filter(x -> x!=0), stream2).filter(x -> x!=1),
                         element)
                     .filter(x -> x!=2);

O código agora é significativamente mais curto. No entanto, concordo que a legibilidade não melhorou. Então, eu tenho outra solução.


Em muitas situações, os coletores podem ser usados ​​para estender a funcionalidade dos fluxos. Com os dois coletores na parte inferior, os dois exemplos podem ser escritos da seguinte maneira:

Stream<Foo> stream = stream1.collect(concat(stream2)).collect(concat(element));

Stream<Foo> stream = stream1
                     .filter(x -> x!=0)
                     .collect(concat(stream2))
                     .filter(x -> x!=1)
                     .collect(concat(element))
                     .filter(x -> x!=2);

A única diferença entre a sintaxe desejada e a sintaxe acima é que você deve substituir concat (...) por collect (concat (...)) . Os dois métodos estáticos podem ser implementados da seguinte forma (opcionalmente usado em combinação com importações estáticas):

private static <T,A,R,S> Collector<T,?,S> combine(Collector<T,A,R> collector, Function<? super R, ? extends S> function) {
    return Collector.of(
        collector.supplier(),
        collector.accumulator(),
        collector.combiner(),
        collector.finisher().andThen(function));
}
public static <T> Collector<T,?,Stream<T>> concat(Stream<? extends T> other) {
    return combine(Collectors.toList(),
        list -> Stream.concat(list.stream(), other));
}
public static <T> Collector<T,?,Stream<T>> concat(T element) {
    return concat(Stream.of(element));
}

Obviamente, há uma desvantagem nesta solução que deve ser mencionada. collect é uma operação final que consome todos os elementos do fluxo. Além disso, o coletor concat cria um ArrayList intermediário cada vez que é usado na cadeia. Ambas as operações podem ter um impacto significativo no comportamento do seu programa. No entanto, se a legibilidade for mais importante que o desempenho , ainda poderá ser uma abordagem muito útil.

nosid
fonte
1
Não acho o concatcolecionador muito legível. Parece estranho um método estático de parâmetro único chamado assim, e também usar collectpara concatenação.
Didier L
@nosid, talvez uma pergunta ligeiramente ortogonal a esta discussão, mas por que você reivindica It's a bad idea to import static methods with names? Estou realmente interessado - acho que torna o código mais conciso e legível, e muitas pessoas que eu pedi pensaram o mesmo. Gostaria de fornecer alguns exemplos de por que isso geralmente é ruim?
Quantum
1
@Quantum: Qual é o significado de compare(reverse(getType(42)), of(6 * 9).hashCode())? Observe que eu não disse que importações estáticas são uma má idéia, mas importações estáticas para nomes genéricos como ofe concatsão.
Nos
1
@nosid: Não passar o mouse sobre cada declaração em um IDE moderno rapidamente revelará o significado? De qualquer forma, acho que essa poderia ser uma declaração de preferência pessoal, na melhor das hipóteses, pois ainda não vejo nenhuma razão técnica para a importação estática de nomes "genéricos" ser ruim - a menos que você esteja usando o Bloco de Notas ou o VI (M) para programar. você tem problemas maiores.
Quantum
Não vou dizer que o Scala SDK é melhor, mas ... opa, eu disse.
eirirlar
165

Infelizmente, essa resposta provavelmente é de pouca ou nenhuma ajuda, mas fiz uma análise forense da lista de mala direta do Java Lambda para ver se encontrava a causa desse design. Foi isso que eu descobri.

No começo, havia um método de instância para Stream.concat (Stream)

Na lista de discussão, posso ver claramente que o método foi originalmente implementado como um método de instância, como você pode ler neste tópico de Paul Sandoz, sobre a operação concat.

Nele, discutem-se os problemas que poderiam surgir daqueles casos em que o fluxo poderia ser infinito e o que concatenação significaria nesses casos, mas não creio que esse tenha sido o motivo da modificação.

Você vê neste outro encadeamento que alguns usuários iniciais do JDK 8 questionaram sobre o comportamento do método da instância concat quando usado com argumentos nulos.

Esse outro segmento revela, no entanto, que o design do método concat estava em discussão.

Refatorado para Streams.concat (Stream, Stream)

Mas sem nenhuma explicação, de repente, os métodos foram alterados para métodos estáticos, como você pode ver neste tópico sobre a combinação de fluxos . Esse talvez seja o único segmento de correio que esclarece um pouco essa alteração, mas não estava claro o suficiente para eu determinar o motivo da refatoração. Mas podemos ver que eles fizeram um commit no qual sugeriram mover o concatmétodo para fora da Streamclasse auxiliarStreams .

Refatorado para Stream.concat (Stream, Stream)

Mais tarde, foi movido novamente de Streamspara Stream, mas novamente, nenhuma explicação para isso.

Portanto, o motivo do design não está totalmente claro para mim e não consegui encontrar uma boa explicação. Eu acho que você ainda pode fazer a pergunta na lista de discussão.

Algumas alternativas para a concatenação de fluxo

Este outro tópico de Michael Hixson discute / pergunta sobre outras maneiras de combinar / concat streams

  1. Para combinar dois fluxos, devo fazer o seguinte:

    Stream.concat(s1, s2)

    isso não:

    Stream.of(s1, s2).flatMap(x -> x)

    ... certo?

  2. Para combinar mais de dois fluxos, devo fazer o seguinte:

    Stream.of(s1, s2, s3, ...).flatMap(x -> x)

    isso não:

    Stream.of(s1, s2, s3, ...).reduce(Stream.empty(), Stream::concat)

    ... certo?

Edwin Dalorzo
fonte
6
+1 Boa pesquisa. E eu vou usá-lo como meu Stream.concat, tendo varargs:public static <T> Stream<T> concat(Stream<T>... streams) { return Stream.of(streams).reduce(Stream.empty(), Stream::concat);}
MarcG 30/03
1
Hoje eu escrevi minha própria versão concat, e logo depois disso financiei esse tópico. A assinatura é um pouco diferente, mas, graças ao fato de ser mais genérica;), por exemplo, você pode mesclar o Stream <Integer> e o Stream <Double> ao Stream <Number>. @SafeVarargs private static <T> Stream<T> concat(Stream<? extends T>... streams) { return Stream.of(streams).reduce(Stream.empty(),Stream::concat).map(Function.identity());}
Kant
@kant Por que você precisa de um Function.identity()mapa? Afinal, ele retorna o mesmo argumento que recebe. Isso não deve ter efeito no fluxo resultante. Estou esquecendo de algo?
Edwin Dalorzo
1
Você já tentou digitar no seu IDE? Sem .map (identity ()), você receberá um erro de compilação. Eu quero retornar o fluxo <T>, mas a instrução: return Stream.of(streams).reduce(Stream.empty(),Stream::concat)retorna o fluxo <? estende T>. (Someting <T> é o subtipo de Algo <? estende T>, não o contrário, portanto não pode ser convertido) .map(identity())Elenco adicional <? estende T> para <T>. Isso acontece graças à combinação de 'tipos de destino' java 8 de argumentos de método e tipos de retorno e assinatura do método map (). Na verdade, é Function. <T> identity ().
Kant
1
@ kant Não vejo muito sentido em fazer ? extends T, já que você pode usar a conversão de captura . De qualquer forma, aqui está o meu trecho de código da lista principal Vamos continuar a discussão na lista principal.
Edwin Dalorzo 14/09
12

Minha biblioteca StreamEx estende a funcionalidade da API Stream. Em particular, oferece métodos como anexar e anexar que resolvem esse problema (eles usam internamente concat). Esses métodos podem aceitar outro fluxo ou coleção ou matriz de varargs. Usando minha biblioteca, seu problema pode ser resolvido desta maneira (observe que x != 0parece estranho para um fluxo não primitivo):

Stream<Integer> stream = StreamEx.of(stream1)
             .filter(x -> !x.equals(0))
             .append(stream2)
             .filter(x -> !x.equals(1))
             .append(element)
             .filter(x -> !x.equals(2));

A propósito, também há um atalho para sua filteroperação:

Stream<Integer> stream = StreamEx.of(stream1).without(0)
                                 .append(stream2).without(1)
                                 .append(element).without(2);
Tagir Valeev
fonte
9

Apenas faça:

Stream.of(stream1, stream2, Stream.of(element)).flatMap(identity());

onde identity()é uma importação estática de Function.identity().

Concatenar vários fluxos em um fluxo é o mesmo que achatar um fluxo.

No entanto, infelizmente, por algum motivo, não existe um flatten()método Stream, então você deve usar flatMap()com a função de identidade.

herman
fonte
1

Se você não se importa em usar as bibliotecas de terceiros, o cyclops-react possui um tipo de fluxo estendido que permitirá que você faça exatamente isso através dos operadores de acréscimo / pré-acréscimo.

Os valores individuais, matrizes, iterables, Streams ou reactive-streams Publishers podem ser anexados e anexados como métodos de instância.

Stream stream = ReactiveSeq.of(1,2)
                           .filter(x -> x!=0)
                           .append(ReactiveSeq.of(3,4))
                           .filter(x -> x!=1)
                           .append(5)
                           .filter(x -> x!=2);

[Divulgação Sou o principal desenvolvedor do cyclops-react]

John McClean
fonte
1

No final do dia, não estou interessado em combinar fluxos, mas em obter o resultado combinado do processamento de cada elemento de todos esses fluxos.

Embora a combinação de fluxos possa ser complicada (portanto, este segmento), combinar seus resultados de processamento é bastante fácil.

A chave a resolver é criar seu próprio coletor e garantir que a função do fornecedor para o novo coletor retorne a mesma coleção sempre ( não uma nova ), o código abaixo ilustra essa abordagem.

package scratchpad;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collector;
import java.util.stream.Stream;

public class CombineStreams {
    public CombineStreams() {
        super();
    }

    public static void main(String[] args) {
        List<String> resultList = new ArrayList<>();
        Collector<String, List<String>, List<String>> collector = Collector.of(
                () -> resultList,
                (list, item) -> {
                    list.add(item);
                },
                (llist, rlist) -> {
                    llist.addAll(rlist);
                    return llist;
                }
        );
        String searchString = "Wil";

        System.out.println("After processing first stream\n"
                + createFirstStream().filter(name -> name.contains(searchString)).collect(collector));
        System.out.println();

        System.out.println("After processing second stream\n"
                + createSecondStream().filter(name -> name.contains(searchString)).collect(collector));
        System.out.println();

        System.out.println("After processing third stream\n"
                + createThirdStream().filter(name -> name.contains(searchString)).collect(collector));
        System.out.println();

    }

    private static Stream<String> createFirstStream() {
        return Arrays.asList(
                "William Shakespeare",
                "Emily Dickinson",
                "H. P. Lovecraft",
                "Arthur Conan Doyle",
                "Leo Tolstoy",
                "Edgar Allan Poe",
                "Robert Ervin Howard",
                "Rabindranath Tagore",
                "Rudyard Kipling",
                "Seneca",
                "John Donne",
                "Sarah Williams",
                "Oscar Wilde",
                "Catullus",
                "Alfred Tennyson",
                "William Blake",
                "Charles Dickens",
                "John Keats",
                "Theodor Herzl"
        ).stream();
    }

    private static Stream<String> createSecondStream() {
        return Arrays.asList(
                "Percy Bysshe Shelley",
                "Ernest Hemingway",
                "Barack Obama",
                "Anton Chekhov",
                "Henry Wadsworth Longfellow",
                "Arthur Schopenhauer",
                "Jacob De Haas",
                "George Gordon Byron",
                "Jack London",
                "Robert Frost",
                "Abraham Lincoln",
                "O. Henry",
                "Ovid",
                "Robert Louis Stevenson",
                "John Masefield",
                "James Joyce",
                "Clark Ashton Smith",
                "Aristotle",
                "William Wordsworth",
                "Jane Austen"
        ).stream();
    }

    private static Stream<String> createThirdStream() {
        return Arrays.asList(
                "Niccolò Machiavelli",
                "Lewis Carroll",
                "Robert Burns",
                "Edgar Rice Burroughs",
                "Plato",
                "John Milton",
                "Ralph Waldo Emerson",
                "Margaret Thatcher",
                "Sylvie d'Avigdor",
                "Marcus Tullius Cicero",
                "Banjo Paterson",
                "Woodrow Wilson",
                "Walt Whitman",
                "Theodore Roosevelt",
                "Agatha Christie",
                "Ambrose Bierce",
                "Nikola Tesla",
                "Franz Kafka"
        ).stream();
    }
}
Legna
fonte
0

Que tal escrever seu próprio método concat?

public static Stream<T> concat(Stream<? extends T> a, 
                               Stream<? extends T> b, 
                               Stream<? extends T> args)
{
    Stream<T> concatenated = Stream.concat(a, b);
    for (Stream<T> stream : args)
    {
        concatenated = Stream.concat(concatenated, stream);
    }
    return concatenated;
}

Isso pelo menos torna seu primeiro exemplo muito mais legível.

Felix S
fonte
1
Tenha cuidado ao construir fluxos a partir de concatenação repetida. O acesso a um elemento de um fluxo profundamente concatenado pode resultar em cadeias de chamadas profundas ou mesmo StackOverflowError.
Legna