Qual é a diferença entre Collection.stream (). ForEach () e Collection.forEach ()?

286

Entendo que .stream(), com , posso usar operações em cadeia como .filter()ou usar fluxo paralelo. Mas qual é a diferença entre eles se eu precisar executar pequenas operações (por exemplo, imprimir os elementos da lista)?

collection.stream().forEach(System.out::println);
collection.forEach(System.out::println);
VladS
fonte

Respostas:

287

Para casos simples, como o ilustrado, eles são basicamente os mesmos. No entanto, existem várias diferenças sutis que podem ser significativas.

Um problema é com o pedido. Com Stream.forEach, o pedido é indefinido . É improvável que ocorra com fluxos sequenciais, ainda assim, está dentro da especificação para Stream.forEachexecução em alguma ordem arbitrária. Isso ocorre frequentemente em fluxos paralelos. Por outro lado, Iterable.forEaché sempre executado na ordem de iteração de Iterable, se um for especificado.

Outra questão é com efeitos colaterais. A ação especificada em Stream.forEaché necessária para não interferir . (Consulte o documento do pacote java.util.stream .) Iterable.forEachPossui potencialmente menos restrições. Para as coleções em java.util, Iterable.forEachgeralmente usará as coleções Iterator, a maioria projetada para ser à prova de falhas e que será lançada ConcurrentModificationExceptionse a coleção for estruturalmente modificada durante a iteração. No entanto, modificações que não são estruturais são permitidas durante a iteração. Por exemplo, a documentação da classe ArrayList diz que "apenas definir o valor de um elemento não é uma modificação estrutural". Assim, a ação paraArrayList.forEaché permitido definir valores no subjacente ArrayListsem problemas.

As coleções simultâneas são novamente diferentes. Em vez de serem rápidos, eles são projetados para serem fracamente consistentes . A definição completa está nesse link. Por um breve momento, considere ConcurrentLinkedDeque. A ação passou a seu forEachmétodo é permitido modificar o deque subjacente, mesmo estruturalmente, e ConcurrentModificationExceptionnunca é lançada. No entanto, a modificação que ocorre pode ou não ser visível nesta iteração. (Daí a consistência "fraca".)

Ainda outra diferença é visível se Iterable.forEachestiver repetindo uma coleção sincronizada. Nessa coleção, Iterable.forEach pega o bloqueio da coleção uma vez e o mantém em todas as chamadas para o método de ação. A Stream.forEachchamada usa o separador da coleção, que não é bloqueado e depende da regra predominante de não interferência. A coleção de backup do fluxo pode ser modificada durante a iteração e, se for, ConcurrentModificationExceptionpode resultar em um comportamento inconsistente.

Stuart Marks
fonte
Iterable.forEach takes the collection's lock. De onde é essa informação? Não consigo encontrar esse comportamento nas fontes JDK.
turbanoff
@Stuart, você pode falar sobre não interferir. Stream.forEach () também lançará ConcurrentModificationException (pelo menos para mim).
yuranos
1
@ yuranos87 Muitas coleções, como ArrayLista verificação bastante rigorosa de modificações simultâneas e, portanto, geralmente são lançadas ConcurrentModificationException. Mas isso não é garantido, principalmente para fluxos paralelos. Em vez do CME, você pode obter uma resposta inesperada. Considere também modificações não estruturais na origem do fluxo. Para fluxos paralelos, você não sabe qual thread processará um elemento específico, nem se foi processado no momento em que foi modificado. Isso define uma condição de corrida, na qual você pode obter resultados diferentes a cada corrida e nunca obter um CME.
Stuart Marcas
30

Essa resposta se preocupa com o desempenho das várias implementações dos loops. É apenas marginalmente relevante para loops que são chamados MUITAS VEZES (como milhões de chamadas). Na maioria dos casos, o conteúdo do loop será de longe o elemento mais caro. Para situações em que você faz um loop com muita frequência, isso ainda pode ser interessante.

Você deve repetir esses testes no sistema de destino, pois isso é específico da implementação ( código fonte completo ).

Executo o openjdk versão 1.8.0_111 em uma máquina Linux rápida.

Eu escrevi um teste que faz um loop 10 ^ 6 vezes em uma lista usando esse código com tamanhos variados para integers(10 ^ 0 -> 10 ^ 5 entradas).

Os resultados estão abaixo, o método mais rápido varia dependendo da quantidade de entradas na lista.

Mas, mesmo nas piores situações, o loop de 10 ^ 5 entradas 10 ^ 6 vezes levou 100 segundos para o pior desempenho; portanto, outras considerações são mais importantes em praticamente todas as situações.

public int outside = 0;

private void forCounter(List<Integer> integers) {
    for(int ii = 0; ii < integers.size(); ii++) {
        Integer next = integers.get(ii);
        outside = next*next;
    }
}

private void forEach(List<Integer> integers) {
    for(Integer next : integers) {
        outside = next * next;
    }
}

private void iteratorForEach(List<Integer> integers) {
    integers.forEach((ii) -> {
        outside = ii*ii;
    });
}
private void iteratorStream(List<Integer> integers) {
    integers.stream().forEach((ii) -> {
        outside = ii*ii;
    });
}

Aqui estão meus horários: milissegundos / função / número de entradas na lista. Cada execução é de 10 ^ 6 loops.

                           1    10    100    1000    10000
       iterator.forEach   27   116    959    8832    88958
               for:each   53   171   1262   11164   111005
         for with index   39   112    920    8577    89212
iterable.stream.forEach  255   324   1030    8519    88419

Se você repetir o experimento, publiquei o código fonte completo . Edite esta resposta e adicione resultados com uma notação do sistema testado.


Usando um MacBook Pro, Intel Core i7 de 2,5 GHz, 16 GB, macOS 10.12.6:

                           1    10    100    1000    10000
       iterator.forEach   27   106   1047    8516    88044
               for:each   46   143   1182   10548   101925
         for with index   49   145    887    7614    81130
iterable.stream.forEach  393   397   1108    8908    88361

VM de ponto de acesso Java 8 - Intel Xeon de 3,4 GHz, 8 GB, Windows 10 Pro

                            1    10    100    1000    10000
        iterator.forEach   30   115    928    8384    85911
                for:each   40   125   1166   10804   108006
          for with index   30   120    956    8247    81116
 iterable.stream.forEach  260   237   1020    8401    84883

VM de ponto de acesso Java 11 - Intel Xeon de 3,4 GHz, 8 GB, Windows 10 Pro
(mesma máquina que acima, versão diferente do JDK)

                            1    10    100    1000    10000
        iterator.forEach   20   104    940    8350    88918
                for:each   50   140    991    8497    89873
          for with index   37   140    945    8646    90402
 iterable.stream.forEach  200   270   1054    8558    87449

Java 11 OpenJ9 VM - Intel Xeon de 3,4 GHz , 8 GB, Windows 10 Pro
(mesma máquina e versão JDK que a anterior, VM diferente)

                            1    10    100    1000    10000
        iterator.forEach  211   475   3499   33631   336108
                for:each  200   375   2793   27249   272590
          for with index  384   467   2718   26036   261408
 iterable.stream.forEach  515   714   3096   26320   262786

VM de ponto de acesso Java 8 - AMD de 2,8 GHz, 64 GB, Windows Server 2016

                            1    10    100    1000    10000
        iterator.forEach   95   192   2076   19269   198519
                for:each  157   224   2492   25466   248494
          for with index  140   368   2084   22294   207092
 iterable.stream.forEach  946   687   2206   21697   238457

VM de ponto de acesso Java 11 - AMD de 2,8 GHz, 64 GB, Windows Server 2016
(mesma máquina que acima, versão JDK diferente)

                            1    10    100    1000    10000
        iterator.forEach   72   269   1972   23157   229445
                for:each  192   376   2114   24389   233544
          for with index  165   424   2123   20853   220356
 iterable.stream.forEach  921   660   2194   23840   204817

Java 11 OpenJ9 VM - 2.8GHz AMD, 64 GB, Windows Server 2016
(mesma máquina e versão JDK que a anterior, VM diferente)

                            1    10    100    1000    10000
        iterator.forEach  592   914   7232   59062   529497
                for:each  477  1576  14706  129724  1190001
          for with index  893   838   7265   74045   842927
 iterable.stream.forEach 1359  1782  11869  104427   958584

A implementação da VM que você escolhe também faz a diferença Hotspot / OpenJ9 / etc.

Angelo Fuchs
fonte
3
Essa é uma resposta muito legal, obrigado! Mas, à primeira vista (e também à segunda), não está claro qual método corresponde a qual experimento.
torina 02/02
Eu sinto que esta resposta precisa de mais votos para o teste de código :).
Cory
para exemplos de testes +1
Centos
8

Não há diferença entre os dois que você mencionou, pelo menos conceitualmente, Collection.forEach()é apenas uma abreviação.

Internamente, a stream()versão possui um pouco mais de sobrecarga devido à criação do objeto, mas, observando o tempo de execução, ela também não possui uma sobrecarga.

Ambas as implementações acabam repetindo o collectionconteúdo uma vez e, durante a iteração, imprimem o elemento.

skiwi
fonte
A sobrecarga de criação de objeto mencionada, você está se referindo ao objeto Streamcriado ou aos objetos individuais? AFAIK, a Streamnão duplica os elementos.
Raffi Khatchadourian 7/03/15
30
Essa resposta parece contradizer a excelente resposta escrita pelo cavalheiro que desenvolve as principais bibliotecas Java da Oracle Corporation.
Dawood ibn Kareem
0

Collection.forEach () usa o iterador da coleção (se um for especificado). Isso significa que a ordem de processamento dos itens está definida. Por outro lado, a ordem de processamento de Collection.stream (). ForEach () é indefinida.

Na maioria dos casos, não faz diferença qual dos dois escolhemos. Fluxos paralelos nos permitem executar o fluxo em vários encadeamentos e, nessas situações, a ordem de execução é indefinida. Java requer apenas que todos os encadeamentos sejam concluídos antes que qualquer operação de terminal, como Collectors.toList (), seja chamada. Vejamos um exemplo em que chamamos primeiro forEach () diretamente na coleção e, em segundo lugar, em um fluxo paralelo:

list.forEach(System.out::print);
System.out.print(" ");
list.parallelStream().forEach(System.out::print);

Se executarmos o código várias vezes, veremos que list.forEach () processa os itens em ordem de inserção, enquanto list.parallelStream (). ForEach () produz um resultado diferente a cada execução. Uma saída possível é:

ABCD CDBA

Outro é:

ABCD DBCA
cpatel
fonte