Qual das seguintes opções é a melhor prática no Java 8?
Java 8:
joins.forEach(join -> mIrc.join(mSession, join));
Java 7:
for (String join : joins) {
mIrc.join(mSession, join);
}
Eu tenho muitos loops que poderiam ser "simplificados" com lambdas, mas há realmente alguma vantagem em usá-los? Melhoraria seu desempenho e legibilidade?
EDITAR
Também estenderei essa pergunta a métodos mais longos. Eu sei que você não pode retornar ou interromper a função pai de um lambda e isso também deve ser levado em consideração ao compará-los, mas há mais alguma coisa a ser considerada?
java
for-loop
java-8
java-stream
Nebkat
fonte
fonte
joins.forEach((join) -> mIrc.join(mSession, join));
realmente uma "simplificação" defor (String join : joins) { mIrc.join(mSession, join); }
? Você aumentou a contagem de pontuação de 9 para 12, para o benefício de ocultar o tipo dejoin
. O que você realmente fez é colocar duas declarações em uma linha.Respostas:
A melhor prática é usar
for-each
. Além de violar o princípio Keep It Simple, Stupid , o novo fangledforEach()
tem pelo menos as seguintes deficiências:Não é possível usar variáveis não finais . Portanto, código como o seguinte não pode ser transformado em um lambda forEach:
Não é possível lidar com exceções verificadas . Os lambdas não são realmente proibidos de lançar exceções verificadas, mas interfaces funcionais comuns como
Consumer
não declaram nenhuma. Portanto, qualquer código que lança exceções verificadas deve colocá-las emtry-catch
ouThrowables.propagate()
. Mas mesmo se você fizer isso, nem sempre é claro o que acontece com a exceção lançada. Poderia ser engolido em algum lugar nas entranhas deforEach()
Controle de fluxo limitado . A
return
em um lambda é igual acontinue
em um para cada, mas não há equivalente a abreak
. Também é difícil fazer coisas como valores de retorno, curto-circuito ou sinalizadores de configuração (o que teria aliviado um pouco as coisas, se não fosse uma violação da regra de não variáveis não finais ). "Isso não é apenas uma otimização, mas é crítico quando você considera que algumas seqüências (como a leitura das linhas de um arquivo) podem ter efeitos colaterais ou uma sequência infinita".Pode ser executado em paralelo , o que é uma coisa horrível para todos, menos os 0,1% do seu código que precisam ser otimizados. Qualquer código paralelo precisa ser pensado (mesmo que não use bloqueios, voláteis e outros aspectos particularmente desagradáveis da execução multiencadeada tradicional). Qualquer bug será difícil de encontrar.
Pode prejudicar o desempenho , porque o JIT não pode otimizar forEach () + lambda na mesma extensão que os loops simples, especialmente agora que as lambdas são novas. Por "otimização", não quero dizer a sobrecarga de chamar lambdas (que é pequena), mas a análise e transformação sofisticadas que o compilador JIT moderno executa na execução de código.
Se você precisar de paralelismo, provavelmente é muito mais rápido e não muito mais difícil usar um ExecutorService . Os fluxos são ambos auto-mágicos (leia-se: não sabem muito sobre o seu problema) e usam uma estratégia de paralelização especializada (leia-se: ineficiente para o caso geral) ( decomposição recursiva de junção de forquilha ).
Torna a depuração mais confusa , devido à hierarquia de chamada aninhada e, Deus permita, execução paralela. O depurador pode ter problemas para exibir variáveis do código circundante, e coisas como passo a passo podem não funcionar conforme o esperado.
Os fluxos em geral são mais difíceis de codificar, ler e depurar . Na verdade, isso é verdade para APIs " fluentes " complexas em geral. A combinação de instruções únicas complexas, o uso pesado de genéricos e a falta de variáveis intermediárias conspiram para produzir mensagens de erro confusas e frustrar a depuração. Em vez de "esse método não tem uma sobrecarga para o tipo X", você recebe uma mensagem de erro mais próxima de "em algum lugar que você estragou os tipos, mas não sabemos onde ou como". Da mesma forma, você não pode examinar e examinar as coisas em um depurador tão facilmente quanto quando o código é dividido em várias instruções, e os valores intermediários são salvos nas variáveis. Por fim, a leitura do código e a compreensão dos tipos e comportamento em cada estágio da execução pode não ser trivial.
Destaca-se como um polegar dolorido . A linguagem Java já possui a instrução for-each. Por que substituí-lo por uma chamada de função? Por que incentivar a ocultação de efeitos colaterais em algum lugar das expressões? Por que incentivar linhas de mão difíceis? Misturar regularmente para cada um e o novo para cada um é um estilo ruim. O código deve falar em expressões idiomáticas (padrões que são rápidos de compreender devido à sua repetição), e quanto menos expressões idiomáticas forem usadas, mais claro será o código e menos tempo será gasto decidindo qual linguagem usar (uma grande perda de tempo para perfeccionistas como eu! )
Como você pode ver, eu não sou muito fã do forEach (), exceto nos casos em que faz sentido.
Particularmente ofensivo para mim é o fato de que
Stream
ele não é implementadoIterable
(apesar de ter um métodoiterator
) e não pode ser usado em um for-each, apenas com um forEach (). Eu recomendo converter Streams em Iterables com(Iterable<T>)stream::iterator
. Uma alternativa melhor é usar o StreamEx, que corrige vários problemas da API do Stream, incluindo a implementaçãoIterable
.Dito isto,
forEach()
é útil para o seguinte:Iterando atomicamente sobre uma lista sincronizada . Antes disso, uma lista gerada com
Collections.synchronizedList()
era atômica em relação a coisas como get ou set, mas não era segura para threads durante a iteração.Execução paralela (usando um fluxo paralelo apropriado) . Isso poupa algumas linhas de código versus o uso de um ExecutorService, se o seu problema corresponder às premissas de desempenho incorporadas ao Streams e Spliterators.
Contêineres específicos que , como a lista sincronizada, se beneficiam de estar no controle da iteração (embora isso seja amplamente teórico, a menos que as pessoas possam trazer mais exemplos)
Chamar uma única função de maneira mais limpa usando
forEach()
um argumento de referência de método (ou seja,list.forEach (obj::someMethod)
). No entanto, lembre-se dos pontos sobre exceções verificadas, depuração mais difícil e redução do número de expressões usadas ao escrever código.Artigos que usei para referência:
EDIT: Parece que algumas das propostas originais para lambdas (como http://www.javac.info/closures-v06a.html ) resolveram alguns dos problemas que mencionei (ao mesmo tempo em que adicionaram suas próprias complicações, é claro).
fonte
forEach
existe para incentivar o estilo funcional, ou seja, usar expressões sem efeitos colaterais. Se você encontrar a situação,forEach
ele não funciona bem com seus efeitos colaterais, deve ter a sensação de que não está usando a ferramenta certa para o trabalho. Então a resposta simples é: isso é porque você está certo, então fique no loop for-each para isso. O clássicofor
circuito não se tornou obsoleto ...forEach
ser usado sem efeitos colaterais?forEach
é a única operação de fluxo destinada a efeitos colaterais, mas não a efeitos colaterais como o código de exemplo, a contagem é umareduce
operação típica . Eu sugeriria, como regra geral, manter todas as operações que manipulam variáveis locais ou devem influenciar o fluxo de controle (incluindo manipulação de exceção) em umfor
loop clássico . Em relação à pergunta original, penso, o problema decorre do fato de alguém usar um fluxo em que umfor
loop simples sobre a fonte do fluxo seria suficiente. Use um riacho ondeforEach()
funciona apenasforEach
seria apropriado?for
loops.A vantagem é levada em consideração quando as operações podem ser executadas em paralelo. (Consulte http://java.dzone.com/articles/devoxx-2012-java-8-lambda-and - a seção sobre iteração interna e externa)
A principal vantagem do meu ponto de vista é que a implementação do que deve ser feito dentro do loop pode ser definida sem a necessidade de decidir se será executado em paralelo ou sequencialmente.
Se você quiser que seu loop seja executado em paralelo, você pode simplesmente escrever
Você precisará escrever um código extra para o manuseio de threads, etc.
Nota: Para minha resposta, assumi junções implementando a
java.util.Stream
interface. Se joins implementa apenas ajava.util.Iterable
interface, isso não é mais verdade.fonte
map
&fold
que não estão realmente relacionados a lambdas.Stream#forEach
eIterable#forEach
não são a mesma coisa. O OP está perguntandoIterable#forEach
.joins
estiver implementando emIterable
vez deStream
? A partir de um par de coisas que eu li, OP deve ser capaz de fazerjoins.stream().forEach((join) -> mIrc.join(mSession, join));
ejoins.parallelStream().forEach((join) -> mIrc.join(mSession, join));
sejoins
implementaIterable
Ao ler esta pergunta, pode-se ter a impressão de que,
Iterable#forEach
em combinação com expressões lambda, é um atalho / substituição para escrever um loop tradicional para cada um. Isto simplesmente não é verdade. Este código do OP:é não pretende ser um atalho para a escrita
e certamente não deve ser usado dessa maneira. Em vez disso, destina-se como um atalho (embora seja não exatamente o mesmo) para a escrita
E é um substituto para o seguinte código Java 7:
Substituir o corpo de um loop por uma interface funcional, como nos exemplos acima, torna seu código mais explícito: Você está dizendo que (1) o corpo do loop não afeta o código circundante e o fluxo de controle e (2) o O corpo do loop pode ser substituído por uma implementação diferente da função, sem afetar o código circundante. Não poder acessar variáveis não finais do escopo externo não é um déficit de funções / lambdas, é um recurso que distingue a semântica da
Iterable#forEach
semântica de um loop tradicional para cada loop. Uma vez acostumado com a sintaxe deIterable#forEach
, torna o código mais legível, porque você obtém imediatamente essas informações adicionais sobre o código.Os loops tradicionais para cada certamente continuarão sendo boas práticas (para evitar o termo usado em excesso " melhores práticas ") em Java. Mas isso não significa que isso
Iterable#forEach
deva ser considerado uma prática ruim ou um estilo ruim. É sempre uma boa prática usar a ferramenta certa para fazer o trabalho, e isso inclui misturar os loops tradicionais para cada umIterable#forEach
, onde faz sentido.Como as desvantagens de
Iterable#forEach
já foram discutidas neste segmento, aqui estão alguns motivos pelos quais você provavelmente deseja usarIterable#forEach
:Para tornar seu código mais explícito: Como descrito acima, você
Iterable#forEach
pode torná-lo mais explícito e legível em algumas situações.Para tornar seu código mais extensível e sustentável: O uso de uma função como corpo de um loop permite substituir essa função por diferentes implementações (consulte Padrão de estratégia ). Você pode, por exemplo, substituir facilmente a expressão lambda por uma chamada de método, que pode ser substituída por subclasses:
Em seguida, você pode fornecer estratégias padrão usando uma enumeração, que implementa a interface funcional. Isso não apenas torna seu código mais extensível, como também aumenta a capacidade de manutenção, porque desacopla a implementação do loop da declaração do loop.
Para tornar seu código mais depurável: Separar a implementação do loop da declaração também pode facilitar a depuração, porque você pode ter uma implementação de depuração especializada, que imprime mensagens de depuração, sem a necessidade de desorganizar o código principal
if(DEBUG)System.out.println()
. A implementação de depuração poderia, por exemplo, ser um delegado , que decora a implementação real da função.Para otimizar o código crítico de desempenho: Ao contrário de algumas das asserções deste encadeamento,
Iterable#forEach
ele já oferece melhor desempenho do que um loop tradicional para cada loop, pelo menos ao usar ArrayList e executar Hotspot no modo "-client". Embora esse aumento de desempenho seja pequeno e insignificante para a maioria dos casos de uso, há situações em que esse desempenho extra pode fazer a diferença. Por exemplo, os mantenedores de bibliotecas certamente quererão avaliar se algumas de suas implementações de loop existentes devem ser substituídas porIterable#forEach
.Para apoiar esta afirmação com fatos, fiz alguns micro-benchmarks com o Caliper . Aqui está o código de teste (é necessário o último Caliper do git):
E aqui estão os resultados:
Ao executar com "-client",
Iterable#forEach
supera o loop for tradicional sobre um ArrayList, mas ainda é mais lento que a iteração direta sobre um array. Ao executar com "-server", o desempenho de todas as abordagens é praticamente o mesmo.Para fornecer suporte opcional à execução paralela: Já foi dito aqui que a possibilidade de executar a interface funcional
Iterable#forEach
em paralelo usando fluxos é certamente um aspecto importante. ComoCollection#parallelStream()
não garante que o loop seja realmente executado em paralelo, deve-se considerar isso um recurso opcional . Ao iterar sua lista comlist.parallelStream().forEach(...);
, você diz explicitamente: Esse loop suporta execução paralela, mas não depende disso. Novamente, esse é um recurso e não um déficit!Ao afastar a decisão de execução paralela da implementação real do loop, você permite a otimização opcional do seu código, sem afetar o próprio código, o que é uma coisa boa. Além disso, se a implementação padrão do fluxo paralelo não atender às suas necessidades, ninguém o impedirá de fornecer sua própria implementação. Você pode, por exemplo, fornecer uma coleção otimizada, dependendo do sistema operacional subjacente, do tamanho da coleção, do número de núcleos e de algumas configurações de preferência:
O bom aqui é que sua implementação de loop não precisa saber ou se preocupar com esses detalhes.
fonte
forEach
e comfor-each
base em alguns critérios relacionados à natureza do corpo do loop. Sabedoria e disciplina para seguir essas regras são a marca registrada de um bom programador. Essas regras também são uma desgraça, porque as pessoas ao seu redor não as seguem ou discordam. Por exemplo, usando exceções marcadas vs não marcadas. Essa situação parece ainda mais sutil. Mas, se o corpo "não afeta o código surround ou o controle de fluxo", não é melhor considerar isso como uma função?But, if the body "does not affect surround code or flow control," isn't factoring it out as a function better?
. Sim, isso geralmente acontece na minha opinião - fatorar esses loops como funções é uma consequência natural.Iterable#forEach
anteriores ao Java 8 apenas por causa do aumento de desempenho. O projeto em questão possui um loop principal semelhante a um loop de jogo, com um número indefinido de sub-loops aninhados, onde os clientes podem conectar participantes do loop como funções. Essa estrutura de software se beneficia bastanteIteable#forEach
.forEach()
pode ser implementado para ser mais rápido do que para cada loop, porque o iterável sabe a melhor maneira de iterar seus elementos, em oposição à maneira do iterador padrão. Portanto, a diferença é loop interno ou externo.Por exemplo,
ArrayList.forEach(action)
pode ser simplesmente implementado comoem oposição ao loop for-each, que requer muitos andaimes
No entanto, também precisamos contabilizar dois custos indiretos usando
forEach()
: um está criando o objeto lambda e o outro está invocando o método lambda. Eles provavelmente não são significativos.consulte também http://journal.stuffwithstuff.com/2013/01/13/iteration-inside-and-out/ para comparar iterações internas / externas para diferentes casos de uso.
fonte
void forEach(Consumer<T> v){leftTree.forEach(v);v.accept(rootElem);rightTree.forEach(v);}
, este é mais elegante do que a iteração externa, e você pode decidir sobre a melhor forma de sincronizarString.join
métodos (ok, junção incorreta) é "Número de elementos que provavelmente não valem Arrays.stream overhead". então eles usam um laço for fino.TL; DR :
List.stream().forEach()
foi o mais rápido.Eu senti que deveria adicionar meus resultados da iteração de benchmarking. Adotei uma abordagem muito simples (sem estruturas de benchmarking) e avaliei 5 métodos diferentes:
for
List.forEach()
List.stream().forEach()
List.parallelStream().forEach
o procedimento de teste e parâmetros
A lista nesta classe deve ser repetida e
doIt(Integer i)
aplicada a todos os seus membros, a cada vez através de um método diferente. na classe Main, eu executo o método testado três vezes para aquecer a JVM. Em seguida, executo o método de teste 1000 vezes, somando o tempo necessário para cada método de iteração (usandoSystem.nanoTime()
). Depois disso, divido essa soma por 1000 e esse é o resultado, o tempo médio. exemplo:Eu executei isso em uma CPU i5 de 4 núcleos, com a versão java 1.8.0_05
clássico
for
tempo de execução: 4,21 ms
foreach clássico
tempo de execução: 5,95 ms
List.forEach()
tempo de execução: 3,11 ms
List.stream().forEach()
tempo de execução: 2,79 ms
List.parallelStream().forEach
tempo de execução: 3,6 ms
fonte
System.out.println
exibir esses dados de forma ingênua, todos os resultados serão inúteis.System.nanoTime()
. Se você ler a resposta, verá como foi feita. Eu não acho que isso torne inútil, visto que esta é uma pergunta relativa . Não me importo com o desempenho de um determinado método, mas com o desempenho de outros métodos.Sinto que preciso estender um pouco meu comentário ...
Sobre paradigma \ estilo
Esse é provavelmente o aspecto mais notável. O FP se tornou popular devido ao que você pode obter evitando os efeitos colaterais. Não vou me aprofundar em quais prós e contras você pode obter disso, pois isso não está relacionado à questão.
No entanto, direi que a iteração usando Iterable.forEach é inspirada no FP e, em vez disso, resulta em trazer mais FP para Java (ironicamente, eu diria que não há muito uso do forEach no FP puro, pois ele não faz nada, exceto introduzir efeitos colaterais).
No final, eu diria que é uma questão de gosto, estilo e paradigma em que você está escrevendo.
Sobre paralelismo.
Do ponto de vista do desempenho, não há benefícios notáveis prometidos com o uso de Iterable.forEach over foreach (...).
De acordo com os documentos oficiais do Iterable.forEach :
... ou seja, documenta claramente que não haverá paralelismo implícito. Adicionar um seria violação do LSP.
Agora, existem "coleções paralelas" que são prometidas no Java 8, mas para trabalhar com aquelas que você precisa para mim, é mais explícito e se preocupa mais com elas (veja a resposta de mschenk74, por exemplo).
BTW: nesse caso, Stream.forEach será usado e não garante que o trabalho real seja realizado em paralelo (depende da coleção subjacente).
ATUALIZAÇÃO: pode não ser tão óbvio e um pouco esticado à primeira vista, mas há outra faceta de estilo e perspectiva de legibilidade.
Primeiro de tudo - as planícies antigas são planas e antigas. Todo mundo já os conhece.
Segundo, e mais importante - você provavelmente deseja usar o Iterable.forEach apenas com lambdas de uma linha. Se o "corpo" ficar mais pesado - eles tendem a não ser tão legíveis. Você tem duas opções a partir daqui - use classes internas (eca) ou use forloop antigo simples. As pessoas geralmente ficam aborrecidas quando vêem as mesmas coisas (iterando sobre coleções) sendo executadas de várias formas / estilos na mesma base de código, e esse parece ser o caso.
Novamente, isso pode ou não ser um problema. Depende das pessoas que trabalham no código.
fonte
collection.forEach(MyClass::loopBody);
Uma das limitações mais funcionais
forEach
da funcionalidade é a falta de suporte a exceções verificadas.Uma solução possível é substituir o terminal
forEach
por um loop foreach antigo simples:Aqui está uma lista das perguntas mais populares com outras soluções alternativas no tratamento de exceções verificadas dentro de lambdas e fluxos:
Função Java 8 Lambda que gera exceção?
Java 8: Lambda-Streams, filtrar por método com exceção
Como posso lançar exceções CHECKED de dentro dos fluxos do Java 8?
Java 8: Tratamento de exceções verificadas obrigatórias em expressões lambda. Por que obrigatório, não opcional?
fonte
A vantagem do método Java 1.8 forEach sobre o 1.7 Enhanced for loop é que, ao escrever o código, você pode se concentrar apenas na lógica de negócios.
O método forEach usa o objeto java.util.function.Consumer como argumento, portanto, ajuda a ter nossa lógica de negócios em um local separado para que você possa reutilizá-lo a qualquer momento.
Veja o snippet abaixo,
Aqui eu criei uma nova classe que substituirá o método de classe de aceitação da classe de consumidor, onde você pode adicionar funcionalidade adicional, mais do que iteração .. !!!!!!
fonte