Java 8 Iterable.forEach () vs foreach loop
Qual das seguintes é a melhor prática em Java 8?
Java 8:
joins.forEach(join -> mIrc.join(mSession, join));
Java 7:
for (String join : joins) {
mIrc.join(mSession, join);
}
Tenho muitos para loops que poderiam ser "simplificados" com lambdas, mas há realmente alguma vantagem de usá-los? Melhoraria o seu desempenho e legibilidade?
EDITAR
Também vou alargar esta questão a métodos mais longos. Eu sei que você não pode voltar ou quebrar a função de pai de um lambda e isso também deve ser levado para consideração ao compará-los, mas há algo mais a ser considerado?8 answers
A vantagem é tida em conta quando as operações podem ser executadas em paralelo. (Ver http://java.dzone.com/articles/devoxx-2012-java-8-lambda-and - a secção sobre iteração interna e externa
A principal vantagem, do meu ponto de vista, é que a implementação do que deve ser feito no âmbito do lacete pode ser definida sem ter de decidir se será executado em paralelo ou sequencial
-
Se quiser que o seu loop seja executado em paralelo você poderia simplesmente escrever
joins.parallelStream().forEach(join -> mIrc.join(mSession, join));
Terá de escrever algum código extra para o tratamento de tópicos, etc.
Nota: para a minha resposta, presumi que me juntaria a implementar a interface java.util.Stream
. Se o joins implementa apenas a interface java.util.Iterable
isto já não é verdade.
A melhor prática é usar for-each
. Além de violar o princípio manter simples e estúpido , o recém-criado forEach()
tem pelo menos as seguintes deficiências:
-
Não é possível utilizar variáveis não finais. Então, um código como o seguinte não pode ser transformado em um lambda forEach:
Object prev = null; for(Object curr : list) { if( prev != null ) foo(prev, curr); prev = curr; }
Não consigo lidar com as excepções verificadas. Lambdas não são proibidos de abrir excepções controladas, mas são comuns. interfaces funcionais como
Consumer
não declaram nenhuma. Portanto, qualquer código que lance excepções assinaladas deve envolvê-las emtry-catch
ouThrowables.propagate()
. Mas mesmo que faças isso, nem sempre é claro o que acontece à excepção. Pode ser engolido algures nas entranhas deforEach()
Controlo limitado do fluxo. A
return
em um lambda é igual acontinue
em A para-cada, mas não há equivalente abreak
. Também é difícil fazer coisas como valores de retorno, curto-circuito, ou definir bandeiras (o que teria aliviado um pouco as coisas, se não fosse uma violação da regra sem variáveis não finais). " isto não é apenas uma optimização, mas é crítico quando se considera que algumas sequências (como ler as linhas de um ficheiro) podem ter efeitos secundários, ou pode ter uma sequência infinita."Pode ser executado em paralelo, o que é uma coisa horrível, horrível para todos, menos para os 0,1% do seu código que precisa ser otimizado. As o código paralelo tem de ser pensado (mesmo que não use Fechaduras, voláteis e outros aspectos particularmente desagradáveis da execução multi-roscada tradicional). Qualquer insecto será difícil de encontrar.
Pode prejudicar o desempenho , porque o JIT não pode otimizar forEach ()+lambda na mesma medida que loops simples, especialmente agora que lambdas são novos. Por "otimização" eu não quero dizer a sobrecarga de chamar lambdas (que é pequeno), mas para a análise sofisticada e transformação que o compilador JIT moderno executa em execução de código.
Se você precisa de paralelismo, provavelmente é muito mais rápido e não muito mais difícil usar um ExecutorService . Os fluxos são ambos automágicos (Leia: não sei muito sobre o seu problema) e usam uma estratégia de paralelização especializada (leia: ineficiente para o caso geral) (} fork-join decomposição recursiva).
Torna a depuração mais confusa , por causa da hierarquia de chamadas aninhada e, Deus nos Livre, execução paralela. O depurador pode ter problemas mostrando variáveis do Código circundante, e coisas como step-through podem não funcionar como esperado.
Os fluxos em geral são mais difíceis de codificar, ler e depurar . Na verdade, isto é verdade para o complexo "fluente" APIs em geral. A combinação de afirmações simples complexas, uso pesado de genéricos, e falta de variáveis intermediárias conspire para produzir mensagens de erro confusas e frustrar a depuração. Em vez de" this method doesn't have an overload for type X", você recebe uma mensagem de erro mais perto de " somewhere you messed up the types, but we don't know where or how."Da mesma forma, você não pode avançar e examinar as coisas em um depurador tão facilmente como quando o código é dividido em múltiplas declarações, e valores intermediários são salvos para variáveis. Finalmente, ler o código e compreender os tipos e o comportamento em cada fase de a execução pode não ser trivial.
Destaca-se como um polegar dorido. A linguagem Java já tem a declaração para-cada. Porquê substituí-lo por uma chamada? Porquê incentivar a esconder efeitos secundários algures nas expressões? Porquê encorajar uníssono inflexível? Misturar regular para-cada e novo forEach willy-nilly é um mau estilo. Código deve falar em idiomas( padrões que são rápidos de compreender devido à sua repetição), e quanto menos idiomas são usados, mais claro o código é e menos tempo é gasto decidindo qual idioma usar (uma grande perda de tempo para perfeccionistas como eu!).
Stream
não implementa Iterable
(apesar de realmente ter Método iterator
) e não pode ser usado em um para-cada, apenas com um forEach(). Recomendo lançar correntes em Iterables com (Iterable<T>)stream::iterator
. Uma melhor alternativa é usar StreamEx que fixa uma série de problemas de API Stream, incluindo a implementação Iterable
.
Dito isto, forEach()
é útil para o seguinte:
Atomicamente a iterar numa 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 de fio quando iterava.Execução paralela (utilizando um fluxo paralelo apropriado). Isto poupa-lhe algumas linhas de código vs usando um ExecutorService, se o seu problema corresponder aos pressupostos de desempenho incorporados em fluxos e separadores.
Contentores específicos que, tal como a lista sincronizada, beneficiam de estar no controlo da iteração (embora isto seja em grande medida teórico, a menos que as pessoas possam apresentar mais exemplos)
Chamando uma única função mais limpa usando
forEach()
e um argumento de referência do método (ie,list.forEach (obj::someMethod)
). No entanto, tenha em mente os pontos sobre exceções assinaladas, depuração mais difícil e redução do número de expressões idiomáticas que você usa ao escrever código.
Artigos que utilizei para referência:
- tudo sobre Java 8
- iteração por dentro e por fora (como indicado por outro cartaz)
Editar: parece-se com algumas das propostas originais para lambdas (como http://www.javac.info/closures-v06a.html ([48]} questões que mencionei (ao adicionar suas próprias complicações, é claro).
Ao ler esta pergunta pode-se ter a impressão de que {[[7]} em combinação com expressões lambda é um atalho/substituto para escrever um tradicional para cada laço. Isso não é verdade. Este código do OP:
joins.forEach(join -> mIrc.join(mSession, join));
Não édestinado como um atalho para escrita
for (String join : joins) {
mIrc.join(mSession, join);
}
E certamente não deve ser usado desta forma. Em vez disso, pretende - se ser um atalho (embora não seja exactamente o mesmo) para escrever
joins.forEach(new Consumer<T>() {
@Override
public void accept(T join) {
mIrc.join(mSession, join);
}
});
E é como um substituto para o seguinte código Java 7:
final Consumer<T> c = new Consumer<T>() {
@Override
public void accept(T join) {
mIrc.join(mSession, join);
}
};
for (T t : joins) {
c.accept(t);
}
Substituindo o corpo de um loop com uma interface funcional, como nos exemplos acima, torna o seu código mais explícito: Você está dizendo que (1) o corpo do loop não afeta o código circundante e fluxo de controle, e (2) o corpo do loop pode ser substituído com uma implementação diferente da função, sem afetar o código circundante. Não ser capaz de acessar variáveis não finais do escopo exterior não é um deficit of functions / lambdas, it is a feature that distinguishes the semantics of Iterable#forEach
from the semantics of a traditional for-each loop. Uma vez que se acostuma com a sintaxe de Iterable#forEach
, torna o código mais legível, porque você imediatamente recebe esta informação adicional sobre o código.
Tradicional para cada loops certamente permanecerá boa prática (para evitar o termo " boas práticas") em Java. Mas isso não significa que ... considerado má prática ou mau estilo. É sempre uma boa prática, usar a ferramenta certa para fazer o trabalho, e isso inclui misturar tradicional para cada loops com Iterable#forEach
, onde faz sentido.
Uma vez que as desvantagens de Iterable#forEach
já foram discutidas neste tópico, aqui estão algumas razões, porque você provavelmente pode querer usar Iterable#forEach
:
Para tornar o código mais explícito:, Como descrito acima,
Iterable#forEach
pode tornar seu código mais explícito e legível em algumas situações.-
Para tornar o seu código mais extensível e sustentável: usando uma função como corpo de um laço permite-lhe substituir esta função por diferentes implementações (ver padrão de estratégia). Você poderia, por exemplo, substituir facilmente a expressão lambda por uma chamada de método, que pode ser substituída por sub-classes:
joins.forEach(getJoinStrategy());
Então você pode fornecer estratégias padrão usando um enum, que implementa a interface funcional. Isto não só torna o seu código mais extensível, ele também aumenta a manutenção, porque ele descupla a implementação do loop a partir da declaração de loop.
Para tornar o código mais debugável (debuggable): Seperating o ciclo de implementação da declaração também pode fazer a depuração mais fácil, porque você poderia ter um especializados de depuração de implementação, que imprime mensagens de debug, sem a necessidade de sobrecarregar o código principal com
if(DEBUG)System.out.println()
. A implementação de depuração pode, por exemplo, ser um delegar , que decore a implementação efectiva da função.-
Para otimizar o código crítico de desempenho: Ao contrário de algumas das afirmações neste tópico,
Iterable#forEach
o {[31] } já oferece um desempenho melhor do que um tradicional para cada ciclo, pelo menos ao usar o ArrayList e ao executar o Hotspot no modo "-cliente". Enquanto este impulso de desempenho é pequeno e insignificante para a maioria dos casos de uso, há situações, onde este desempenho extra pode fazer a diferença. Por exemplo, mantenedores de bibliotecas certamente vão querer avaliar, se algumas de suas implementações de loop existentes devem ser substituídas porIterable#forEach
.Para confirmar esta afirmação com factos, fiz alguns micro-benchmarks comPaquímetro . Aqui está o código de teste (o último Paquímetro do git é necessário):
E aqui estão os resultados:@VmOptions("-server") public class Java8IterationBenchmarks { public static class TestObject { public int result; } public @Param({"100", "10000"}) int elementCount; ArrayList<TestObject> list; TestObject[] array; @BeforeExperiment public void setup(){ list = new ArrayList<>(elementCount); for (int i = 0; i < elementCount; i++) { list.add(new TestObject()); } array = list.toArray(new TestObject[list.size()]); } @Benchmark public void timeTraditionalForEach(int reps){ for (int i = 0; i < reps; i++) { for (TestObject t : list) { t.result++; } } return; } @Benchmark public void timeForEachAnonymousClass(int reps){ for (int i = 0; i < reps; i++) { list.forEach(new Consumer<TestObject>() { @Override public void accept(TestObject t) { t.result++; } }); } return; } @Benchmark public void timeForEachLambda(int reps){ for (int i = 0; i < reps; i++) { list.forEach(t -> t.result++); } return; } @Benchmark public void timeForEachOverArray(int reps){ for (int i = 0; i < reps; i++) { for (TestObject t : array) { t.result++; } } } }
Quando executar com "- client",
Iterable#forEach
supera o tradicional para o loop sobre um ArrayList, mas ainda é mais lento do que iterar diretamente sobre um array. Ao executar com "- server", o desempenho de todas as abordagens é aproximadamente o mesmo. -
Para fornecer suporte opcional para a execução em paralelo: já foi dito aqui, que a possibilidade de executar a interface funcional de
Ao mover a decisão de execução paralela para longe da implementação real do loop, você permite otimização opcional do seu código, sem afetar o código o que é bom. Além disso, se a implementação padrão de fluxo paralelo não se adequa às suas necessidades, ninguém está impedindo você de fornecer sua própria implementação. Poderá, por exemplo, fornecer uma colecção optimizada, dependendo do sistema operativo subjacente, do tamanho da colecção, do número de núcleos e de algumas configurações de preferência:Iterable#forEach
em paralelo, usando fluxos, é, certamente, um aspecto importante. DesdeCollection#parallelStream()
Não garante, que o laço é realmente executado em paralelo, deve-se considerar esta uma funcionalidade opcional. Ao passar por cima da sua lista comlist.parallelStream().forEach(...);
, Você diz explicitamente: este loop suporta execução paralela, mas não depende dele. Mais uma vez, trata-se de uma característica e não de um défice!
O que é bom aqui é que a implementação do loop não precisa de saber ou de se preocupar com isto. informacao.public abstract class MyOptimizedCollection<E> implements Collection<E>{ private enum OperatingSystem{ LINUX, WINDOWS, ANDROID } private OperatingSystem operatingSystem = OperatingSystem.WINDOWS; private int numberOfCores = Runtime.getRuntime().availableProcessors(); private Collection<E> delegate; @Override public Stream<E> parallelStream() { if (!System.getProperty("parallelSupport").equals("true")) { return this.delegate.stream(); } switch (operatingSystem) { case WINDOWS: if (numberOfCores > 3 && delegate.size() > 10000) { return this.delegate.parallelStream(); }else{ return this.delegate.stream(); } case LINUX: return SomeVerySpecialStreamImplementation.stream(this.delegate.spliterator()); case ANDROID: default: return this.delegate.stream(); } } }
forEach()
pode ser implementado para ser mais rápido do que para-cada loop, porque o iterable sabe a melhor maneira de iterar seus elementos, em oposição à maneira iterator padrão. Então a diferença é laço interno ou laço externo.
Por exemplo ArrayList.forEach(action)
pode ser simplesmente implementado como
for(int i=0; i<size; i++)
action.accept(elements[i])
Em oposição ao laço para cada laço que requer um monte de andaimes
Iterator iter = list.iterator();
while(iter.hasNext())
Object next = iter.next();
do something with `next`
No entanto, também precisamos de ter em conta dois custos gerais, usando forEach()
, Um está a fazer o lambda. o outro é invocar o método lambda. Eles provavelmente não são significativos.
Ver também http://journal.stuffwithstuff.com/2013/01/13/iteration-inside-and-out / para comparar iterações internas / externas para diferentes casos de Utilização.
TL; DR: List.stream().forEach()
era o mais rápido.
Senti que devia adicionar os meus resultados da iteração de benchmarking. Adoptei uma abordagem muito simples (sem quadros de benchmarking) e avaliei 5 métodos diferentes:
- clássico
for
- clássico foreach
List.forEach()
List.stream().forEach()
List.parallelStream().forEach
O procedimento de ensaio e os parâmetros
private List<Integer> list;
private final int size = 1_000_000;
public MyClass(){
list = new ArrayList<>();
Random rand = new Random();
for (int i = 0; i < size; ++i) {
list.add(rand.nextInt(size * 50));
}
}
private void doIt(Integer i) {
i *= 2; //so it won't get JITed out
}
A lista desta classe deve ser iterada e ter algum doIt(Integer i)
aplicado a todos são membros, cada vez através de um método diferente.
na classe principal eu executei o método testado três vezes para aquecer a JVM. Executo então o método de ensaio 1000 vezes somando o tempo necessário para cada método de iteração (utilizando System.nanoTime()
). Depois disso, divido essa soma por 1000 e esse é o resultado, tempo médio.
exemplo:
myClass.fored();
myClass.fored();
myClass.fored();
for (int i = 0; i < reps; ++i) {
begin = System.nanoTime();
myClass.fored();
end = System.nanoTime();
nanoSum += end - begin;
}
System.out.println(nanoSum / reps);
Fiz isto numa CPU principal i5 4, com a versão java 1.8.0_05
Clássico for
for(int i = 0, l = list.size(); i < l; ++i) {
doIt(list.get(i));
}
Tempo de execução: 4,21 ms
Clássico foreachfor(Integer i : list) {
doIt(i);
}
Tempo de execução: 5,95 ms
List.forEach()
list.forEach((i) -> doIt(i));
Tempo de execução: 3.11 ms
List.stream().forEach()
list.stream().forEach((i) -> doIt(i));
Tempo de execução: 2.79 ms
List.parallelStream().forEach
list.parallelStream().forEach((i) -> doIt(i));
Tempo de execução: 3.6 ms
sobre o estilo do paradigma\
Esse é provavelmente o aspecto mais notável. FP tornou-se popular devido ao que você pode obter evitando efeitos colaterais. Não vou aprofundar o que prós\contras você pode obter a partir deste, uma vez que isso não está relacionado com a pergunta. No entanto, direi que a iteração usando iterável.forEach é inspirado pelo FP e é o resultado de trazer mais FP para Java (ironicamente, eu diria que há não há muito uso para o forEach em PF puro, uma vez que ele não faz nada exceto introduzir efeitos colaterais). No final eu diria que é uma questão de gosto\estilo\paradigma que você está escrevendo atualmente.sobre paralelismo.
Do ponto de vista do desempenho, não há benefícios notáveis prometidos do uso Iterable.forEach over foreach (...).De acordo com os documentos oficiais sobre Iterable.forEach :
Executa a acção indicada na conteúdo do iterável, os elementos de ordem ocorrem ao iterar, até que todos os elementos tenham sido processado ou a ação lança uma exceção.
... isto é, os documentos deixam bem claro que não haverá paralelismo implícito. Adicionar um seria violação da LSP.
Agora, existem" coleções parallells " que são prometidas em Java 8, mas para trabalhar com aqueles que você precisa para mim mais explícito e colocar algum cuidado extra para usá-los (veja a resposta de mschenk74 para exemplo).BTW: neste caso Stream.forEach será usado, e não garante que o trabalho real será feito em Paralell (depende da coleção subjacente).
Atualização: pode não ser tão óbvio e um pouco esticado de relance, mas há outra faceta de estilo e perspectiva de legibilidade.
Em primeiro lugar, as forloops velhas e simples são planas e velhas. Todos já os conhecem.Segundo, e mais importante - você provavelmente quer use Iterable.forEach apenas com um liner lambdas. Se o" corpo " ficar mais pesado - eles tendem a não ser-assim legível. Você tem 2 opções a partir daqui-use classes internas (yuck) ou use forloop velho simples. As pessoas muitas vezes ficam irritadas quando vêem as mesmas coisas (iteratins sobre coleções) sendo feitas vários vays/estilos na mesma base de código, e este parece ser o caso.
Mais uma vez, isto pode ou não ser um problema. Depende das pessoas que trabalham em código.Uma das limitações mais funcionais forEach
é a falta de suporte de excepções verificadas.
Um possível solução é substituir o terminal forEach
por um simples laço foreach:
Stream<String> stream = Stream.of("", "1", "2", "3").filter(s -> !s.isEmpty());
Iterable<String> iterable = stream::iterator;
for (String s : iterable) {
fileWriter.append(s);
}
Aqui está a lista das perguntas mais populares com outros trabalhos sobre o tratamento de excepções verificado em lambdas e streams:
Função Lambda Java 8 que abre excepções?
Java 8: fluxos Lambda, Filtrar por método com Excepção
Como posso lançar exceções verificadas de dentro de Java 8 streams?
A vantagem do método Java 1.8 forEach sobre 1.7 melhorado para o loop é que ao escrever o código você pode se concentrar apenas na lógica de negócios.
ForEach method takes java.util.funcao.Objeto do consumidor como um argumento, então Ajuda em ter a nossa lógica de negócio em um local separado que você pode reutilizá-lo a qualquer momento.
Veja abaixo o excerto,
-
Aqui criei uma nova classe que irá sobrepor o método accept da classe Consumer.,
onde você pode adicionar funcionalidade adicional, mais do que iteração..!!!!!!
class MyConsumer implements Consumer<Integer>{ @Override public void accept(Integer o) { System.out.println("Here you can also add your business logic that will work with Iteration and you can reuse it."+o); } } public class ForEachConsumer { public static void main(String[] args) { // Creating simple ArrayList. ArrayList<Integer> aList = new ArrayList<>(); for(int i=1;i<=10;i++) aList.add(i); //Calling forEach with customized Iterator. MyConsumer consumer = new MyConsumer(); aList.forEach(consumer); // Using Lambda Expression for Consumer. (Functional Interface) Consumer<Integer> lambda = (Integer o) ->{ System.out.println("Using Lambda Expression to iterate and do something else(BI).. "+o); }; aList.forEach(lambda); // Using Anonymous Inner Class. aList.forEach(new Consumer<Integer>(){ @Override public void accept(Integer o) { System.out.println("Calling with Anonymous Inner Class "+o); } }); } }