Java 8 - Melhor maneira de transformar uma lista: mapa ou foreach?

188

Eu tenho uma lista na myListToParsequal desejo filtrar os elementos e aplicar um método em cada elemento e adicionar o resultado em outra lista myFinalList.

Com o Java 8, notei que posso fazer isso de duas maneiras diferentes. Gostaria de saber a maneira mais eficiente entre eles e entender por que um caminho é melhor que o outro.

Estou aberto a qualquer sugestão sobre uma terceira via.

Método 1:

myFinalList = new ArrayList<>();
myListToParse.stream()
        .filter(elt -> elt != null)
        .forEach(elt -> myFinalList.add(doSomething(elt)));

Método 2:

myFinalList = myListToParse.stream()
        .filter(elt -> elt != null)
        .map(elt -> doSomething(elt))
        .collect(Collectors.toList()); 
Emilien Brigand
fonte
55
O segundo. Uma função adequada não deve ter efeitos colaterais; em sua primeira implementação, você está modificando o mundo externo.
ThanksForAllTheFish
37
apenas uma questão de estilo, mas elt -> elt != nullpode ser substituído porObjects::nonNull
the8472
2
@ the8472 Melhor ainda seria garantir que não haja valores nulos na coleção e, Optional<T>em vez disso , use-os em combinação com flatMap.
Herman
2
@SzymonRoziewski, não exatamente. Para algo tão trivial quanto isso, o trabalho necessário para configurar a corrente paralela sob o capô tornará o uso dessa construção mudo.
MK
2
Observe que você pode escrever .map(this::doSomething)assumindo que este doSomethingé um método não estático. Se for estático, você pode substituir thispelo nome da classe.
Herman

Respostas:

153

Não se preocupe com diferenças de desempenho, elas serão mínimas neste caso normalmente.

O método 2 é preferível porque

  1. não requer a mutação de uma coleção que existe fora da expressão lambda,

  2. é mais legível porque as diferentes etapas executadas no pipeline de coleta são gravadas sequencialmente: primeiro uma operação de filtro, depois uma operação de mapa e, em seguida, coletando o resultado (para obter mais informações sobre os benefícios dos pipelines de coleta, consulte o excelente artigo de Martin Fowler ),

  3. você pode alterar facilmente a maneira como os valores são coletados, substituindo o Collectorque é usado. Em alguns casos, pode ser necessário escrever o seu Collector, mas o benefício é que você pode reutilizá-lo facilmente.

herman
fonte
43

Concordo com as respostas existentes de que a segunda forma é melhor porque não tem efeitos colaterais e é mais fácil paralelizar (basta usar um fluxo paralelo).

Em termos de desempenho, parece que eles são equivalentes até você começar a usar fluxos paralelos. Nesse caso, o mapa terá um desempenho muito melhor. Veja abaixo os resultados da micro benchmark :

Benchmark                         Mode  Samples    Score   Error  Units
SO28319064.forEach                avgt      100  187.310 ± 1.768  ms/op
SO28319064.map                    avgt      100  189.180 ± 1.692  ms/op
SO28319064.mapWithParallelStream  avgt      100   55,577 ± 0,782  ms/op

Você não pode impulsionar o primeiro exemplo da mesma maneira, porque forEach é um método terminal - ele retorna nulo -, então você é forçado a usar uma lambda com estado. Mas isso é realmente uma péssima idéia se você estiver usando fluxos paralelos .

Por fim, observe que seu segundo trecho pode ser escrito de maneira um pouco mais concisa com referências de método e importações estáticas:

myFinalList = myListToParse.stream()
    .filter(Objects::nonNull)
    .map(this::doSomething)
    .collect(toList()); 
assylias
fonte
1
Sobre o desempenho, no seu caso, "map" realmente vence "forEach" se você usar parallelStreams. Meus benchmaks em milissegundos: SO28319064.forEach: 187.310 ± 1.768 ms / op - SO28319064.map: 189.180 ± 1.692 ms / op --SO28319064.mapParallelStream: 55.577 ± 0.782 ms / op
Giuseppe Bertone
2
@ GiuseppeBertone, cabe a assylias, mas na minha opinião sua edição contradiz a intenção do autor original. Se você deseja adicionar sua própria resposta, é melhor adicioná-la em vez de editar tanto a existente. Agora também o link para o microbenchmark não é relevante para os resultados.
Tagir Valeev
5

Um dos principais benefícios do uso de fluxos é que ele oferece a capacidade de processar dados de maneira declarativa, ou seja, usando um estilo funcional de programação. Ele também oferece a capacidade de multiencadeamento para um significado gratuito, não sendo necessário escrever nenhum código multithread extra para tornar seu fluxo simultâneo.

Supondo que você esteja explorando esse estilo de programação é que deseja explorar esses benefícios, seu primeiro exemplo de código não é funcional, pois o foreachmétodo é classificado como terminal (o que significa que ele pode produzir efeitos colaterais).

A segunda maneira é preferida do ponto de vista da programação funcional, pois a função map pode aceitar funções lambda sem estado. Mais explicitamente, o lambda passado para a função map deve ser

  1. Não interfere, significando que a função não deve alterar a fonte do fluxo se não for concorrente (por exemplo ArrayList).
  2. Sem estado para evitar resultados inesperados ao executar o processamento paralelo (causado por diferenças de agendamento de threads).

Outro benefício com a segunda abordagem é que se o fluxo for paralelo e o coletor for simultâneo e não ordenado, essas características podem fornecer dicas úteis para a operação de redução para realizar a coleta simultaneamente.

MK
fonte
4

Se você usar o Eclipse Collections, poderá usar o collectIf()método

MutableList<Integer> source =
    Lists.mutable.with(1, null, 2, null, 3, null, 4, null, 5);

MutableList<String> result = source.collectIf(Objects::nonNull, String::valueOf);

Assert.assertEquals(Lists.immutable.with("1", "2", "3", "4", "5"), result);

Ele avalia avidamente e deve ser um pouco mais rápido do que usar um Stream.

Nota: Sou um colaborador das Coleções Eclipse.

Craig P. Motlin
fonte
1

Eu prefiro o segundo caminho.

Ao usar a primeira maneira, se você decidir usar um fluxo paralelo para melhorar o desempenho, não terá controle sobre a ordem em que os elementos serão adicionados à lista de saída por forEach.

Quando você usa toList, a API do Streams preservará a ordem, mesmo se você usar um fluxo paralelo.

Eran
fonte
Não tenho certeza se este é um conselho correto: ele poderia usar em forEachOrderedvez de forEachse quisesse usar um fluxo paralelo, mas ainda preservar a ordem. Mas, como a documentação para os forEachestados, preservar a ordem do encontro sacrifica o benefício do paralelismo. Eu suspeito que também é o caso com toListentão.
Herman
0

Existe uma terceira opção - using stream().toArray()- veja os comentários em por que o stream não possui um método toList . Ele é mais lento que forEach () ou collect () e menos expressivo. Pode ser otimizado em versões posteriores do JDK, adicionando-o aqui apenas por precaução.

assumindo List<String>

    myFinalList = Arrays.asList(
            myListToParse.stream()
                    .filter(Objects::nonNull)
                    .map(this::doSomething)
                    .toArray(String[]::new)
    );

com uma referência micro-micro, entradas de 1 milhão, 20% de nulos e transformação simples em doSomething ()

private LongSummaryStatistics benchmark(final String testName, final Runnable methodToTest, int samples) {
    long[] timing = new long[samples];
    for (int i = 0; i < samples; i++) {
        long start = System.currentTimeMillis();
        methodToTest.run();
        timing[i] = System.currentTimeMillis() - start;
    }
    final LongSummaryStatistics stats = Arrays.stream(timing).summaryStatistics();
    System.out.println(testName + ": " + stats);
    return stats;
}

os resultados são

paralelo:

toArray: LongSummaryStatistics{count=10, sum=3721, min=321, average=372,100000, max=535}
forEach: LongSummaryStatistics{count=10, sum=3502, min=249, average=350,200000, max=389}
collect: LongSummaryStatistics{count=10, sum=3325, min=265, average=332,500000, max=368}

sequencial:

toArray: LongSummaryStatistics{count=10, sum=5493, min=517, average=549,300000, max=569}
forEach: LongSummaryStatistics{count=10, sum=5316, min=427, average=531,600000, max=571}
collect: LongSummaryStatistics{count=10, sum=5380, min=444, average=538,000000, max=557}

paralelo sem nulos e filtro (portanto, o fluxo é SIZED): toArrays tem o melhor desempenho nesse caso e .forEach()falha com "indexOutOfBounds" no destinatário ArrayList, teve que substituir por.forEachOrdered()

toArray: LongSummaryStatistics{count=100, sum=75566, min=707, average=755,660000, max=1107}
forEach: LongSummaryStatistics{count=100, sum=115802, min=992, average=1158,020000, max=1254}
collect: LongSummaryStatistics{count=100, sum=88415, min=732, average=884,150000, max=1014}
harshtuna
fonte
0

Pode ser o método 3.

Eu sempre prefiro manter a lógica separada.

Predicate<Long> greaterThan100 = new Predicate<Long>() {
            @Override
            public boolean test(Long currentParameter) {
                return currentParameter > 100;
            }
        };

        List<Long> sourceLongList = Arrays.asList(1L, 10L, 50L, 80L, 100L, 120L, 133L, 333L);
        List<Long> resultList = sourceLongList.parallelStream().filter(greaterThan100).collect(Collectors.toList());
Kumar Abhishek
fonte
0

Se o uso de 3rd Pary Libaries estiver ok, cyclops- react define coleções estendidas preguiçosas com essa funcionalidade incorporada. Por exemplo, poderíamos simplesmente escrever

ListX myListToParse;

ListX myFinalList = myListToParse.filter (elt -> elt! = Nulo) .map (elt -> doSomething (elt));

myFinalList não é avaliado até o primeiro acesso (e depois que a lista materializada é armazenada em cache e reutilizada).

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

John McClean
fonte