É muito comum utilizarmos estruturas de dados para nos ajudar a atender as necessidades requeridas em nossas aplicações. Uma dessas estruturas são as collections. Elas são tão comuns que remover até mesmo uma pequena quantidade de cerimônia para operá-las traria um grande ganho na redução de possíveis confusões que podem aparecer em nossos códigos.
Vamos explorar como podemos utilizar as Lambda Expressions
para manipular essas tais coleções. Com elas podemos, de uma forma declarativa, filtrar dados, realizar transformações, criar novas coleções, concatenar elementos, entre outras operações.
Iterando em Coleções
Iterar por meio de uma lista é uma operação básica nas coleções (collection
), e ao longo dos anos, essa operação também sofreu mudanças significativa dentro da linguagem Java.
Vamos começar com um exemplo - enumerando uma lista de nomes - utilizando uma abordagem mais antiga e seguir evoluindo até uma versão onde a escrita dessa operação apresente uma forma mais concisa e elegante.
Aqui criamos uma coleção imutável de lista de nomes:
final List<String> developers = List.of(
"Maximillian",
"Otavio Santana",
"Bruno Souza",
"Elder Moraes",
"Sérgio Lopes",
"Fernando Boaglio");
Abaixo, uma forma de iterar e escrever cada item no console:
for (int i = 0; i < developers.size(); i++) {
System.out.println(developers.get(i));
}
Provavelmente, alguma vez, durande a escrita de um laço for
conforme escrito acima, vc se deparou questionando: é i <
ou i <=
. Essa abordagem é bem verbosa e propensa a erros, e outra, ela só é útil se precisarmos manipular elementos em um particular índice na coleção.
Uma outra forma que o Java também oferece do que o bom e velho for
:
for (String developer: developers) {
System.out.println(developer);
}
Por baixo do capô, essa forma utiliza a interface Iterator
para iterar entre os itens, chamando o método hasNext
para saber quando parar de iterar, e o método next
para capturar o item na posição corrente.
Nesses dois casos, são utilizados iteradores externos (external iterators 1).
No primeiro exemplo, precisamos explicitamente controlar a iteração, indicando onde começar e onde parar; Já no segundo, essas mesmas operações acontecem por baixo dos panos utilizando os métodos da interface Iterator
. E mais, através desse controle explícito, podemos utilizar as declarações break
e continue
para gerenciar o fluxo de controle da execução da iteração.
Com external iterators, instruimos o programa COMO fazer a iteração para que, só então atingir o QUE queremos no final das contas.
Já no segundo exemplo, iteramos entre os elementos da coleção com menos cerimônia do que a primeira versão. Essa estrutura só é melhor do que a primeira quando não temos a intenção de acessar ou modificar a coleção baseada em índices e posições específicas , porém, ambas utilizam um estilo imperativo e nós podemos dispensar essa abordagem uma vez que podemos utilizar o estilo funcional.
Há boas razões a favor de mudar do estilo imperativo para o estilo funcional:
- Loops utilizando
for
são inerentemente sequenciais e são difíceis de paralelizar; - Tais loops são non-polymorphic, isto é, temos que passar a coleção na instrução do
for
ao invés de executar algum método (que pode usufruir do polimorfismo por baixo dos panos) na coleção para executar a tarefa. - No nível de design, o princípio "Tell, don't ask"2 cai por terra! Nós solicitamos a execução de uma específica iteração ao invés de deixar esses detalhes da iteração para a biblioteca de nível mais baixo.
Dito isso, vamos utilizar o estilo funcional no lugar do imperativo, e assim utilizar iteradores internos (internal iterators 1).
Com uma internal iterators, nós deixamos a maioria das instruções de COMO fazer tal iteração para a biblioteca de nível mais baixo e focamos no QUE queremos realizar durante a iteração.
A interface Iterable
foi melhorada no Java 8 com um método especial chamado forEach
, que aceita um parâmetro do tipo Consumer
. Como o próprio nome indica, uma instância do tipo Consumer
irá consumir o que for passado pra ele através do seu método accept
.
developers.forEach(new Consumer<String>() {
@Override
public void accept(final String developer) {
System.out.println(developer);
}
});
Ao trocar a utilização do velho for
pelo novo internal iterator 1 forEach
ganhamos o benefício de não necessitar focar em como iterar na coleção em questão e sim em no que fazer a cada iteração. O código aplica o princípio Tell, don't ask de maneira satisfatória.
Espere um pouco, essa interface
Consumer
não é uma interface funcional!
Exato! Com isso podemos utilizar Lambda Expressions ao invés de implementar uma classe anônima!
O método forEach
é um método que aplica o pilar higher-order function, onde nos permite oferecer uma Lambda Expression ou um bloco de código que irá executar dentro do contexto de cada elemento da lista. A variável developer
será vinculada a cada elemento da coleção durante sua chamada.
Assim, a implementação por baixo dos panos deste método terá o controle de como iterar e como executar o objeto de função recebido como argumento. Encapsular a implementação atrás de métodos como esse permitem que implementações como essas possam também poderá decidir vários aspectos interessantes, como se a execução deve ser ou não preguiçosa (lazy), ou definir a ordem dos itens durante a iteração, ou até explorar o paralelismo como achar melhor. Esse é o poder do encapsulamento.
developers.forEach((final String developer) ->
System.out.println(developer));
A sintaxe padrão de Lambda Expressions espera que os parâmetros estejam junto com seu tipo definido entre parênteses e separado por vírgulas, mas o compilador Java também oferece a inferência de tipos 3 4 5.
Baseado na assinatura do método da interface que a Lambda Expression está implementando, o compilador é capaz de detectar qual é o tipo do parâmetro em questão e efetuar sua inferência.
Vamos usufruir da inferência de tipos em nosso exemplo tirando a declaração:
developers.forEach((developer) ->
System.out.println(developer));
Assim, baseado no contexto do método, o compilador sabe determinar o tipo do parâmetro que está sendo fornecido.
Para casos onde há multiplos parâmetros, podemos seguir o mesmo princípio, não declarar o tipo de cada parâmetro, mas se precisarmos especificar o tipo de um parâmetro, precisaremos especificar o tipo de todos os parâmentros, isto é, ou declara nenhum ou declara todos os tipos de cada parâmetros.
Para casos onde só há um parâmetro, o compilador Java não exige que o parâmetro esteja dentro de parenteses.
developers.forEach(developer ->
System.out.println(developer));
Mas uma resalva: parâmetros inferidos são non-final. Em um dos exemplos anteriores, escrevemos uma Lambda Expression onde além de explicitamente definir o tipo do parâmetro, nós também definimos que o parâmetro deve ser final
. Isso instrui o compilador a nos alertar caso o parâmetro for modificado dentro da Lambda Expression. De modo geral, modificar parâmetros é algo ruim que pode conduzir a erros, então defini-los com final
é uma boa prática.
Infelizmente, quando favorecemos a inferência de tipos na declaração dos parâmetros em uma Lambda Expressions, temos que ter uma disciplina extra em não modificar os parâmetros, pois o compilador não poderá nos ajudar nesses casos.
Reduzindo código com Method References
Vimos até agora exemplos com Lambda Expressions, porém há mais um passo que podemos dar para deixar o codigo mais conciso:
developers.forEach(System.out::println);
No último código de exemplo nós usamos um Method Reference. O Java nos deixa, de maneira simples, substituir o corpo de código com um método nomeado de nossa escolha. Vamos olhar com mais detalhes sobre Method Reference em artigos futuros, no worries! 😉
Como não existe bala de prata, utilizar forEach
também tem suas limitações. Uma vez que começa o método, diferentemente das versões que utilizam for
, a iteração não podem ser interrompidas. Como consequência, esse estilo é útil em casos comuns onde nós queremos processar cada elemento de uma coleção.
No próximo artigo, vamos ver como Lambda Expressions
podem nos ajudar a lidar com a mutabilidade e deixar nosso código mais conciso durante operações de transformação com coleções...spoiler: Streams API 🚀 !!!
Obrigado a todos e até o próximo artigo!!!
Top comments (3)
Em relação à não interromper a iteração no forEach, tenho uma dúvida. Se eu tiver um método de validação por exemplo que esteja sendo chamado no laço do forEach e ele tiver um try/catch ou um if/else, enfim, qualquer estrutura condicional que se não for atendida lance uma exception do tipo RunTime. Me corrija se eu estiver errado, mas vejo que nesse caso a execução seria pausada e o laço não seria completo. Se isso estiver correto, seria esse um caso de mitigar a limitação de não interrupção explícita da iteração no forEach em relação ao for?
Olá Gus! Muito boa a sua pergunta! Vamos por parte:
É possível interromper a execução de um
forEach
sim, mas abruptamente, isso quer dizer, lançando uma exceção não checada (UncheckedException), como um RuntimeException, como nesse exemplo:mas isso exigirá que haja um tratamento try/catch em algum lugar de sua aplicação, ou sendo ao redor da chamada do forEach, como o exemplo abaixo...
...ou em algum tratador de exceção, como por exemplo quando utilizamos algum framework como o Spring.
A questão é que, quando utilizamos o
forEach
não há uma maneira elegante onde a própria API do Java forneça essa interrupção da iteração. Não podemos utilizarbreak
,continue
oureturn
dentro de uma lambda expression. E por quê?Por que o contexto de execução vive somente para cada item na iteração! Se fornecermos
return
no bloco de código, ele vai devolver a execução ao iterador interno e então seguir para o próximo item da coleção caso ele não participou da iteração!Espero ter esclarecido para você essa questão!
Muito boa a explicação!
Show de bola mesmo, muito obrigado, esclareceu sim!