sexta-feira, 8 de maio de 2015

Java Funcional com Lambdas - Parte 2 (Mudanças na API de Collections)

Parte 1 - Introdução às expressões Lambdas
Parte 2 - As interfaces funcionais e as novidades da API Collections
Parte 3 - A classe Stream e conclusão 

As interfaces funcionais e as novidades da API Collections



As novas interfaces funcionais do Java 8

Os especialistas nas expressões Lambdas adicionaram ao Java 8 um pacote de interfaces funcionais muito úteis quando focando no modelo de programação voltado a essas expressões. Com o uso de genéricos e interfaces funcionais, foram definidas diversas interfaces que podem ser usadas pelo programador quando criando seu próprio código ou em designers de APIs. No pacote java.util.function, você encontra diversas interfaces funcionais, entre elas destamos:
Predicate
É uma interface funcional para testes booleanos. O método test é usado para realizar um teste booleano com os atributos de um objeto passado como parâmetro. É possível utilizar os métodos a and e ou para combinar diversos resultados booleanos. A interface é parametrizada através de generics, sendo que o parâmetro vai determinar o tipo de objeto a ser recebido no método test.
Function
Executa a função sobre um objeto T retornando um objeto do tipo R. Essa interface contém somente o método apply, que recebe T para retornar R. É possível usar o método andThen para configurar uma função a ser aplicada após que aplicamos essa e o método compose para configurarmos uma função que será aplicada antes dessa.
Consumer
Simplesmente aplica uma ação sobre o objeto de tipo T através do método apply. Similarmente à interface Function, podemos usar um método andThen para configurar um Consumer que deve ser chamado após a execução desse.
BinaryOperator
Essa interface funcional recebe dois objetos do tipo T e retorna um objeto do tipo T. Podemos utilizar os métodos estáticos maxBy e minBy para que tenhamos um implementação dessa função que calcula o máximo e o mínimo de acordo com o Comparator passado para esses métodos.
Supplier
Cria um objeto do tipo T. O método get é chamado e deve retornar um objeto do tipo T. Importante deixar claro que a interface funcional em sí não necessariamente obriga quem está implementando a criar um objeto, mas sim retornar um do tipo T, ficando a cargo de quem implementa como deveria ser realizada a criação do objeto.

As interfaces funcionais são parametrizadas através do uso de generics, muitas vezes determinando os parâmetros que as funções recebem. O uso dessas funções fica mais evidente nos exemplos a seguir, onde apresentamos o uso de Lambdas na API Collections do Java.

As novidades da API Collections

As collections, ou coleções, sofreram mudanças significativas no decorrer dos anos: foram parametrizadas através de Generics, ganharam novas classes e uma sintaxe mais agradável para iterarmos sobre os elementos de uma das classes da API de collections.
No entanto, uma manipulação dos elementos de uma lista ainda acontece através da escrita de muito código e pouco intuitiva. Por exemplo, digamos que temos uma lista de objetos do tipo Produto com os atributos nome, data de criação, quantidade em estoque e marca. Há uma aplicação que lista esses produtos. Surge um requerimento para tratamento de produtos em excesso de uma determinada marca: produtos que tiverem mais que 5 unidades da marca Super e criados no ano de 2014 devem ser submetidos a um processamento através do método processaProdutoEmExcesso e também devemos somar a quantidade de todos produtos que estiverem nessa condição. Já produtos antigos, fabricados antes de 2000, devem ser processados usando o método processaProdutoAntigo, além de informar o mais antigo produto de todos. Utilizando a maneira convencional, temos o código na Listagem 7, onde fazemos uso de loops e verificação manual de cada elemento. Notem como o código é inflexível e nos leva ao uso de diversos comandos para atingir um objetivo simples.

Listagem 7. Aplicando determinadas ações sob uma lista
List<Produto> produtos = todosProdutos();
     for (Produto p : produtos){
          if(p.qtdeEstoque > 5 && p.marca.equals("Super") && p.dataCriacao.get(ChronoField.YEAR) == 2014){
               processaProdutoEmExcesso(p);
               somaProdutosMarcaSuper+=p.qtdeEstoque;
          }
          if(p.dataCriacao.get(ChronoField.YEAR) < 2000){
               processaProdutoAntigo(p);
               if(p.dataCriacao.isBefore(maisAntigo.dataCriacao)) maisAntigo = p;
          }
}

Com o tempo, diversas necessidade de filtragem desse relatório aparecem e temos que flexibilizar o mesmo. Aí modificamos o método acima para flexibilizar a aplicação de uma condição e a execução de uma ação como mostrado na Listagem 8, onde fazemos uso das interfaces funcionais Predicate e Consumer, descritas anteriormente. A chamada desses métodos usando a forma tradicional, com classes internas é verbosa e pode ser vista na Listagem 9 e a forma melhorada, com Lambdas, está na Listagem 10.

Listagem 8. Melhorando o código anterior para flexibilizar a condição de filtragem da lista
public static void realizaProcessamentoSobCondicao(List<Produto> produtos, Predicate<Produto> condicao, Consumer<Produto> acao){
     for(Produto p : produtos){
          if(condicao.test(p)){
               acao.accept(p);
          }
     }
}

Listagem 9. Utilizando método realizaProcessamentoSobCondicao usando Classes Anônimas
realizaProcessamentoSobCondicao(produtos, 
     new Predicate<Produto>(){
         public boolean test(Produto p){
               return p.qtdeEstoque > 5 && p.marca.equals("Super") && p.dataCriacao.get(ChronoField.YEAR) == 2014;
     }
},
     new Consumer<Produto>(){
          public void accept(Produto p){
               processaProdutoEmExcesso(p);
               somaProdutosMarcaSuper+=p.qtdeEstoque;
          }
     }
);

realizaProcessamentoSobCondicao(produtos,
     new Predicate<Produto>(){
          public boolean test(Produto p){
               return p.dataCriacao.get(ChronoField.YEAR) < 2000;
          }
     },
     new Consumer<Produto>(){
          public void accept(Produto p){
               processaProdutoAntigo(p);
               if(p.dataCriacao.isBefore(maisAntigo.dataCriacao)) maisAntigo = p;
          }
     }
);

Listagem 10. Utilizando método realizaProcessamentoSobCondicao usando expressões Lambdas
realizaProcessamentoSobCondicao(produtos,
     p -> p.qtdeEstoque > 5 && p.marca.equals("Super") && p.dataCriacao.get(ChronoField.YEAR) == 2014,
     p ->{
          processaProdutoEmExcesso(p);
          somaProdutosMarcaSuper+=p.qtdeEstoque;
     }
);

realizaProcessamentoSobCondicao(produtos,
     p -> p.dataCriacao.get(ChronoField.YEAR) < 2000,
     p -> {
          processaProdutoAntigo(p);
          if(p.dataCriacao.isBefore(maisAntigo.dataCriacao)) maisAntigo = p;
     }
);

Nosso código, no entanto, ainda está muito verboso e falta flexibilidade, pois a cada programa que criarmos, temos que criar novos métodos semelhantes ao realizaProcessamentoSobCondicao, mostrado na Listagem 8. Não seria interessante já ter algo semelhante a isso disponível na API nativamente?
A API Collections da versão 8 do Java traz uma forma interessante de tratarmos listas fortemente baseado nas expressões Lambdas e com uso das interfaces funcionais já descritas. O código mostrado acima poderia ser reescrito em uma linha e ter a leitura do mesmo muito facilitada. A classe que possibilita o uso extensivo de Lambdas e potencializa o uso de coleções no Java é a classe Stream.

Até mais na parte 3 onde falamos da classe Stream...

Nenhum comentário:

Postar um comentário