DEV Community

Maximillian Arruda
Maximillian Arruda

Posted on

[PT-BR] Collections, forEach, Lambda Expressions - o que são external ou internal iterators?

É 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");
Enter fullscreen mode Exit fullscreen mode

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));
    }
Enter fullscreen mode Exit fullscreen mode

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);
    }
Enter fullscreen mode Exit fullscreen mode

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);
      }
  });
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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!!!

Source dos exemplos 6:

Referências:


  1. Livro:"Functional Programming in Java: Harnessing the Power of Java 8 Lambda Expression" by Venkat Subramaniam 

  2. Tell-Don't-Ask Principle by Martin Fowler 

  3. JEP 323: Local-Variable Syntax for Lambda Parameters 

  4. JEP 286: Local-Variable Type Inference 

  5. Arquivo: Java 9 na prática: Inferência de tipos 

  6. JBang 

Top comments (3)

Collapse
 
gusoliveiira profile image
gus

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?

Collapse
 
dearrudam profile image
Maximillian Arruda

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:

 developers.forEach(developer -> {
                     if ("Max".equals(developer)){
                         throw new RuntimeException("Não quero aceitar o 'Max'");
                     }
                     System.out.println(developer);
                     });
Enter fullscreen mode Exit fullscreen mode

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...

 try{
 developers.forEach(developer -> {
                     if ("Max".equals(developer)){
                         throw new RuntimeException("Não quero aceitar o 'Max'");
                     }
                     System.out.println(developer);
                     });
}catch(RuntimeException ex){
    //ops!
    ex.printStacktrace();
}
Enter fullscreen mode Exit fullscreen mode

...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 utilizar break, continue ou return 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!

Collapse
 
gusoliveiira profile image
gus

Muito boa a explicação!
Show de bola mesmo, muito obrigado, esclareceu sim!