Artigo no estilo: Curso

Por que eu devo ler este artigo:Continuando a análise de Expressões Lambda (EL) em Java, nesta segunda parte do artigo vamos verificar como as mesmas podem auxiliar no processamento de coleções de dados com a expressividade e simplicidade da programação funcional.

Vamos explorar alguns métodos da nova API de Streams e apresentaremos uma visão de alto nível das técnicas envolvidas para realização de processamento paralelo, as quais utilizam o estado da arte de programação concorrente baseadas nos artefatos do framework Fork/Join.

Deste modo, este artigo é útil para desenvolvedores que desejam entender como utilizar as EL juntamente com a nova API de Streams com o objetivo de processar massas de dados empregando um código mais conciso e elegante, assim como conferir como a nova biblioteca torna trivial a tarefa de paralelizar um processamento sequencial.

Na primeira parte do artigo avaliamos os componentes que foram criados para suportar a sintaxe e criação de EL no Java, as quais permitirão um estilo muito mais compacto e elegante de codificação, com nuances de programação funcional.

Nesta segunda parte vamos focar na API de Streams, criada especificamente para alavancar o uso de EL e que modificará completamente a maneira que estamos acostumados a lidar com coleções de dados.

A biblioteca de Streams chegou para suprir, definitivamente, a antiga necessidade dos desenvolvedores Java de possuírem em seu arsenal uma API de linguagem fluente e “universal” para processamento de dados.

Esta API concentra-se em disponibilizar o trio de operações Filter, Map e Reduce (FMR) através de funções de ordem superior, que nada mais são que métodos que recebem uma interface, tipicamente do pacote java.util.function, como parâmetro.

Além disso, veremos como a biblioteca de Streams promove a utilização do framework de processamento paralelo Fork/Join, entregue no JDK 7, porém ainda não tão popular quanto os ThreadPools do framework Executors, disponível desde o JDK 5.

Avaliação lazy, processamento em lote e o pacote java.util.stream

Enquanto o pacote java.util.function provê os insumos para construir uma biblioteca de programação funcional, o pacote java.util.stream faz uso desses ingredientes para criar de fato as receitas que os agregam para produzir algo muito maior que a soma de cada parte.

Como o próprio nome diz, este pacote é dedicado ao tratamento do fluxo de dados, que usualmente provém de coleções, porém as abstrações construídas neste pacote não assumem nada em relação à fonte dos dados, as quais podem ter como origem um arquivo ou tabela no banco de dados.

Embora as ideias para implementação do trio FMR já estivessem relativamente amadurecidas no início de sua implementação no JDK, o código que provia estas funcionalidades habitava o próprio pacote java.util e boa parte da implementação estava consolidada com métodos padrão na interface Iterable e suas extensões como List e Set.

Ao perceberem que as antigas interfaces estavam ficando extremamente poluídas devido ao abuso de métodos padrão, os desenvolvedores da JSR 335 decidiram criar a abstração contida no pacote java.util.stream, separando “fisicamente” as centenas de novas classes e criando alguns poucos métodos padrão nas classes do pacote java.util que servem como ponte do antigo mundo das coleções para o novo mundo dos Streams.

Na versão atual, a interface Iterable define apenas um método padrão utilizado para suportar as funcionalidades de FMR, spliterator(), cujo significado veremos logo adiante, e a interface Collection define os novos métodos stream() e parallelStream(), que permitem que qualquer coleção seja adaptada para um Stream.

A implementação de Filter-Map-Reduce na biblioteca de Streams segue três princípios básicos:

· As operações devem ser Lazy, sempre que possível;

· As operações devem poder ser encadeadas “à vontade”;

· Deve ser possível converter processamento sequencial para processamento paralelo de forma transparente.

O primeiro princípio, de avaliação Lazy, está relacionado a minimizar o footprint de memória ocasionado por operações do tipo Filter e Map. Isto quer dizer que dada uma massa de dados, contendo, por exemplo, 100.000 objetos, e a operação Filter selecionar apenas 20.000 objetos para serem posteriormente transformados por uma operação Map, nenhum objeto deverá ser criado em função da quantidade de objetos filtrados e/ou mapeados, se as funções de Filter e Map não provocarem a criação de novos objetos.

Para entender este conceito, consideremos, por exemplo, uma coleção de objetos do tipo Pessoa (Listagem 1). Em determinado momento, poderíamos ter de implementar a funcionalidade de selecionar apenas pessoas entre 20 e 30 anos, cujo nome inicia com a letra 'A' e calcular a soma, a média, o maior e o menor salário das mesmas.

Existem muitas maneiras de realizar esta filtragem e calcular esses valores atrelados ao salário. Talvez a mais intuitiva, como faríamos sem ter experiência em desenvolvimento, seria como mostrado na Listagem 2.

Listagem 1. Código da classe Pessoa.


  public class Pessoa {
   
   String nome;
   int idade;
   int salario;
   List<Imovel> imoveis;
   
   //gets e sets
  }
   
  public class Imovel {
   
    int valor;
    int area;
   
   //gets e sets
  }

Listagem 2. Código para filtrar uma coleção de Pessoas.


    public class TestFiltro {
   
   public void testFiltroComCopia() {
    Collection<Pessoa> pessoas = Arrays.asList(
        new Pessoa("Andre", 42,18000),
        new Pessoa("Joao", 20,2000),  
        new Pessoa("Alessandra", 21,500), 
        new Pessoa("Antônio", 28),11500)
    );
   
    Collection<Pessoa> filtrada = filtrarPorInicialEIdade(pessoas, 'A', 20,30);
   
    int numFiltrada = filtrada.size();
    int somaSalario = 0;
    int maxSalario = 0;
    int minSalario = 0;
    double mediaSalario=0;
   
    if(numFiltrada > 0){
     //consome a coleção filtrada: {[Alessandra,21,500],[Antônio,28,11500]}              
     for(Pessoa p : filtrada) {
      int sal = p.getSalario();
      
      if(sal > maxSalario){
       maxSalario = sal;
      }
   
      if(minSalario == 0 || sal < minSalario){
       minSalario = sal;
      }
      somaSalario += p.getSalario();
     }
   
     mediaSalario = somaSalario/numFiltrada;
    }    
    
   }
   
   private Collection<Pessoa> filtrarPorInicialEIdade(
     Collection<Pessoa> pessoas, char inicial, int idadeMin, int idadeMax) {
   
    Collection<Pessoa> result = new ArrayList<>();
   
    for (Pessoa p : pessoas) {
     if (aceita(p, inicial, idadeMin, idadeMax)) {
      result.add(p);
     }
    }
   
    return result;
   }
   
   private static boolean aceita(Pessoa p, char inicial, int idadeMin,
     int idadeMax) {
    return p.getNome().charAt(0) == inicial && //
      p.getIdade() >= idadeMin && //
      p.getIdade() <= idadeMax;
   }
  }

O código da Listagem 2 apresenta dois problemas:

1. Os objetos são copiados da coleção original para uma nova coleção, possivelmente sem necessidade. Se todos forem selecionados, teremos uma réplica da coleção original;

2. O código que realiza a filtrage ...

Quer ler esse conteúdo completo? Tenha acesso completo