6 de março de 2014

Mapeando e Reduzindo - Java 8

As vezes precisamos processar dados em um volume maior, onde é necessário filtrar a informação, selecionar ou alterar o que queremos para depois realizar o processamento. Como exemplo, vamos imaginar uma lista de alunos, onde cada instância tem seu nome e nota. Desta lista queremos a média geral, a média dos alunos aprovados (ou seja, quem tirou nota seis ou mais) e a média dos reprovados (nota menor que seis). Para isso, vamos primeiro visualizar nossa lista:

        List<Aluno>alunos = new ArrayList<>();

        Aluno aluno1 = new Aluno("Cebolinha", 8.0);
        Aluno aluno2 = new Aluno("Cascao", 6.5);
        Aluno aluno3 = new Aluno("Monica", 9.0);
        Aluno aluno4 = new Aluno("Magali", 3.0);

        alunos.addAll(Arrays.asList(aluno1, aluno2, aluno3, aluno4));

Apesar da simplicidade dos nossos dados, para poder separar a nossa lista de forma a realizar esta média, precisamos iterar sobre todos os seus elementos. Vamos fazer primeiro a média mais simples, sem nenhum critério:

        Double media = new Double("0");
        int quantidadeDeAlunos = 0;

        for (Aluno aluno : alunos) {
            media = Double.sum(media, aluno.getNota());
            quantidadeDeAlunos++;
        }

        System.out.println(media / quantidadeDeAlunos);


Repare no trabalho de declarar a variável e iterar sobre todos os seus elementos, ainda de maneira simples, pois ainda não temos nenhuma regra implementada, vamos agora colocar os outros dois trechos de código, implementando a média dos aprovados e reprovados:

        media = new Double("0");
        quantidadeDeAlunos = 0;

        for (Aluno aluno : alunos) {
            if (aluno.getNota() >= 6) {
                media = Double.sum(media, aluno.getNota());
                quantidadeDeAlunos++;
            }
        }

        System.out.println(media / quantidadeDeAlunos);

        media = new Double("0");
        quantidadeDeAlunos = 0;

        for (Aluno aluno : alunos) {
            if (aluno.getNota() < 6) {
                media = Double.sum(media, aluno.getNota());
                quantidadeDeAlunos++;
            }
        }

        System.out.println(media / quantidadeDeAlunos);
Da mesma forma que meu modem toda vez que a internet cai, precisei reiniciar as variáveis de média e rescrever os for's, mas eu sei que é possível fazer em apenas uma iteração, desta forma podemos economizar tempo, vamos tentar ver a versão reduzida deste código:
 
        Double mediaAprovados = new Double("0");
        Double mediaReprovados = new Double("0");

        int quantidadeDeAlunosAprovados = 0;
        int quantidadeDeAlunosReprovados = 0;

        for (Aluno aluno : alunos) {
            if(aluno.getNota() >= 6) {
                mediaAprovados = Double.sum(mediaAprovados, aluno.getNota());
                quantidadeDeAlunosAprovados++;
            }
            else {
                mediaReprovados = Double.sum(mediaReprovados, aluno.getNota());
                quantidadeDeAlunosReprovados++;
            }
        }

        System.out.println(
                (Double.sum(mediaAprovados, mediaReprovados)) 
                        / (quantidadeDeAlunosAprovados + quantidadeDeAlunosReprovados));
        System.out.println(mediaAprovados / quantidadeDeAlunosAprovados);
        System.out.println(mediaReprovados / quantidadeDeAlunosReprovados);
    }
Nesta versão, conseguimos reduzir bem o nosso código, centralizando tudo em um único foreach, porém o gerenciamento manual aumentou, pois tivemos que criar novas variáveis e ainda realizar o cálculo das médias dentro das chamadas das funções de saída.

Abstraindo a função do código, chegamos a algumas conclusões que podemos traduzir melhor, a primeira coisa que fazemos é filtrar com os requisitos que queremos, ou seja, as notas, depois de filtrar as entidades Aluno, devemos selecionar qual informação vamos processar (se necessário trata-la) e depois realizamos a agregação dessa informação, somando tudo em uma variável double (por exemplo, mediaAprovados).

No artigo passado, vimos como realizar a primeira tarefa utilizando o Java 8, onde aprendemos sobre os filtros, porém ainda precisamos realizar algumas outras operações, que é selecionar a informação que queremos (mapear) e depois agrega-la (reduzir). Para nossa sorte, a nova versão do Java também possui este tratamento, onde podemos trabalhar com métodos como map() (e suas variações) e reduce(), todas elas da classe Stream. Abaixo vemos o primeiro exemplo, da média geral:
 
        Double media = alunos.stream()
                .filter(a -> a.getNota() >= 0) //Filtrando o resultado
                .mapToDouble(aluno -> { return aluno.getNota(); } ) //mapeando para uma DoubleStream as notas
                .average().getAsDouble(); //Pegando a média e retornando o double

        System.out.println(media);
Chega a ser covarde a abordagem, todas as iterações estão encapsuladas e tudo é resolvido em apenas em uma linha, com uma sintaxe bem mais limpa, além de termos fornecidos métodos que realizam operações matemáticas, como average(), facilitando toda a operação. Agora vejamos como são todas as implementações, pegando a média dos aprovados e reprovados:
 
        Double media= alunos.stream()
                .filter(a -> a.getNota() >= 0)
                .mapToDouble(aluno -> { return aluno.getNota(); } )
                .average().getAsDouble();

        System.out.println(media);

        Double mediaAprovados = alunos.stream()
                .filter(a -> a.getNota() >= 6)
                .mapToDouble(aluno -> { return aluno.getNota(); } )
                .average().getAsDouble();

        System.out.println(mediaAprovados);

        Double mediaReprovados = alunos.stream()
                .filter(a -> a.getNota() < 6)
                .mapToDouble(aluno -> { return aluno.getNota(); } )
                .average().getAsDouble();

        System.out.println(mediaReprovados);
Vemos que a legibilidade do código aumenta, mas repare que os três códigos são iguais, mudando apenas o filtro, e como repetição de código é mais feio que jogar cachorro na estrada, vamos pensar numa maneira mais bonita de lidar com o código. Lembre-se que as expressões lambda, como o nosso filtro, são na verdade implementações dinâmicas da classe Filter, então podemos receber ela como parâmetro. Vamos criar um método para lidar com esse parâmetro, recebendo a lista de alunos e o filtro, retornando a sua média:
 
    public static Double media(List<Aluno> alunos, Predicate<Aluno> filtro) {
        return alunos.stream()
                .filter(filtro)
                .mapToDouble(aluno -> { return aluno.getNota(); } )
                .average().getAsDouble();
    }
Agora que temos os métodos podemos substituir o código por chamadas as funções, mas lembre-se que não é necessário criar classes anônimas, afinal as expressões lambda são convertidas automaticamente pela máquina virtual em implementações da interface:
 
        Double media = media( alunos, a-> a.getNota() > 0 );

        System.out.println(media);

        Double mediaAprovados = media(alunos, a -> a.getNota() >= 6 );

        System.out.println(mediaAprovados);

        Double mediaReprovados = media(alunos, a -> a.getNota() < 6 );

        System.out.println(mediaReprovados);
Repare como a nossa classe ficou muito mais simples, com um código simples de se compreender e dar manutenção, podendo ampliar facilmente a função. Fica muito simples processar informações desta forma, onde podemos não apenas selecionar as informações e reduzi-la, mas podemos transforma-la antes. Poderíamos converter para String's todas as notas e concatena-las, ou mais mil e uma funções. Isso é inclusive a princípio de alguns banco de dados para processamento de grandes massas de dados, como o Hadoop, onde estas informações são utilizadas para análises de Business Inteligence e em sistemas de apoio a decisão.

Nenhum comentário:

Postar um comentário