Article image

DH

Douglas Holanda18/07/2024 16:03
Compartilhe

Tornando a manipulação de Collections eficiente com Stream API

  • #Java

Breve introdução

As Collections representam um conjunto de classes e interfaces para armazenamento e gerenciamento de objetos em Java, a fim de tornar eficiente a manipulação de estruturas de dados. Com o crescimento da complexidade dos sistemas computacionais e as demandas por paradigmas mais apropriados a essa complexidade, eis que a partir do Java 8 surge a Stream API, um grupo de recursos para manipulação de coleções de modo eficaz e simplificado, utilizando conceitos do paradigma funcional e das expressões lambda. A motivação da Stream API é permitir aos desenvolvedores maneiras concisas e competentes de implementar e estruturar controles de fluxo, deixando tal tarefa a cargo da API, com práticas de mapeamento, filtragem, redução, particionamento, entre outras.

Exemplos práticos

1) Analisando dados de congestionamento nas cidades mais engarrafadas do Brasil

Conforme dito anteriormente, usaremos coleções de dados para fazer boa utilização da Stream API.

Uma classe "Cidade" com nome e quantidade de tempo gasto no trânsito em 2023.

public class Cidade {
  private String nome;
  private int tempoGasto;

  public Cidade(String nome, int tempoGasto) {
      this.nome = nome;
      this.tempoGasto = tempoGasto;
  }
  
  public String getNome() {
      return nome;
  }
  
  public void setNome(String nome) {
      this.nome = nome;
  }
  
  public int getTempoGasto() {
      return tempoGasto;
  }
  
  public void setTempoGasto(int tempoGasto) {
      this.tempoGasto = tempoGasto;
  }
}

Uma classe "Engarrafamento" que usa o paradigma funcional com Stream para iterar sobre a coleção de "Cidade".

import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.text.*;

public class Engarrafamento {
  private List<Cidade> cidades;

  public Engarrafamento(List<Cidade> cidades) {
      this.cidades = cidades;
  }

  private double calculaMediaTempoTotal() {
      return cidades.stream()  
              .mapToDouble(Cidade::getTempoGasto)
              .average()
              .orElse(0.0);
  }
  
  public String retornaMedia() {
      DecimalFormat decimalFormat = new DecimalFormat("0.00");
      double media = calculaMediaTempoTotal();
      
      return "Média das cidades analisadas : " + decimalFormat.format(media);
  }
  
  public List<String> determinaCidadePorTempo() {
      double media = calculaMediaTempoTotal();
      return cidades.stream()
              .filter(cidade -> cidade.getTempoGasto() > media)
              .map(cidade -> cidade.getNome() + " - " + cidade.getTempoGasto())
              .collect(Collectors.toList());
  }
}

Primeiro, gera-se uma instância da coleção List<Cidade>, e em um método funcional, converte a instância em uma stream:

cidades.stream()  

A partir disso, realiza-se a chamada do método mapToDouble para mapear cada cidade e o tempo gasto e então determinar a média desse tempo, caso a coleção esteja vazia, retorna 0.0.

.mapToDouble(Cidade::getTempoGasto)
              .average()
              .orElse(0.0);

Após isso, o stream retorna novamente em outro método para realizar a filtragem das cidades com quantidade de tempo gasto no trânsito maior que a média de todas as cidades analisadas e ao final retorná-las no formato de String com o mapeamento de lista feito por Collectors.toList().

double media = calculaMediaTempoTotal();
  return cidades.stream()
      .filter(cidade -> cidade.getTempoGasto() > media)
      .map(cidade -> cidade.getNome() + " " + cidade.getTempoGasto())
      .collect(Collectors.toList());
 

Realizando-se a execução:

import java.util.*;


public class Principal {
  public static void main(String[] args) {

      var principal = new Principal();
      List<Cidade> cidades = principal.inicializaDadosCidades();
      var engarrafamento = new Engarrafamento(cidades);
      List<String> congestionamentos = engarrafamento.determinaCidadePorTempo();  
      
      System.out.println("As cidades com tempo gasto na média maior que a média nacional são: \n");
      
      System.out.println(" " + engarrafamento.retornaMedia());
      
      congestionamentos.forEach(System.out::println);    
  }

  public List<Cidade> inicializaDadosCidades() {
      return new ArrayList<>(Arrays.asList(
            new Cidade("São Paulo", 105),
            new Cidade("Fortaleza", 92),
            new Cidade("Curitiba", 94),
            new Cidade("Recife", 116),
            new Cidade("Belo Horizonte", 109),
            new Cidade("Rio de Janeiro", 81)
      ));
  }
}

/*
Saída esperada :

As cidades com quantidades de tempo gasto maiores que a média de todas as analisadas são:

Média das cidades analisadas : 99.50
São Paulo 105
Recife 116
Belo Horizonte 109
*/

2) Determinando alimentos por carga glicêmica

Com mais um exemplo, dessa vez sem tantos detalhamentos e fazendo uso de um record, recurso introduzido permanentemente na linguagem a partir do Java 16 e que reduz a verbosidade das classes com seus construtores e getters e setters.

O record "Nutriente" possui como atributos o nome, o peso, a massa de carboidratos (ambos em gramas) e o índice glicêmico.

public record Nutriente(
      String nome,
      double peso,
      double massaCarboidratos,
      double indiceGlicemico
) {}

A partir disso, introduzimos a classe "Glicemico" que usará o paradigma funcional para iterar sobre a stream gerada a partir da instância da coleção Set<Glicemico>.

import java.util.*;
import java.util.stream.Collectors;

public class Glicemico {
  private Set<Nutriente> nutrientes;

  public Glicemico(Set<Nutriente> nutrientes) {
      this.nutrientes = nutrientes;
  }

  private double calcularCargaGlicemica(Nutriente nutriente) {
      return (nutriente.massaCarboidratos() * nutriente.indiceGlicemico()) / 100.00; /*Fonte da fórmula logo abaixo.*/
  }

  public double calcularCargaTotal() {
      return nutrientes.stream()
              .mapToDouble(nutriente -> (nutriente.massaCarboidratos() * nutriente.indiceGlicemico()) / 100.00)
              .sum();
  }

  public List<String> retornaAlimentosPorCG() {
      return nutrientes.stream()
              .sorted(Comparator.comparingDouble(this::calcularCargaGlicemica).reversed())
              .map(nutriente -> {
                      double carga = calcularCargaGlicemica(nutriente);
                        return  " " + nutriente.nome() + " -  " + carga;
                      }
              ).collect(Collectors.toList());
  }
}

A Stream é utilizada para retornar a soma total de carga glicêmica da coleção de alimentos e posteriormente, no último método, para ordenar de modo decrescente e retornar a lista em formato de String.

Executando:

import java.text.DecimalFormat;
import java.util.*;

public class Principal {
  public static void main(String[] args) {
      DecimalFormat decimalFormat = new DecimalFormat("0.00");
      var principal = new Principal();
      Set<Nutriente> nutrientes = principal.inicializaNutrientes();
      var glicemico = new Glicemico(nutrientes);
      List<String> glicemicos = glicemico.retornaAlimentosPorCG();

      System.out.println("Alimentos por carga glicêmica.\n");

      glicemicos.forEach(System.out::println);

      System.out.println(("\nCarga glicêmica total : " + decimalFormat.format(glicemico.calcularCargaTotal())));

  }
  public Set<Nutriente> inicializaNutrientes() {
      return new HashSet<>(Arrays.asList(
              new Nutriente("Arroz", 1000.00, 28.00, 82.00),
              new Nutriente("Pão integral", 100.00, 11.00, 70.00),
              new Nutriente("Milho", 250.00, 25.00, 83.00),
              new Nutriente("Feijão", 1000.00, 30.00, 39.00),
              new Nutriente("Batata inglesa", 300.00, 35.00, 104.00)
      ));
  }
}

/* Saída espera

Alimentos por carga glicêmica.

 Batata inglesa -  36.4
 Arroz -  22.96
 Milho -  20.75
 Feijão -  11.7
 Pão integral -  7.7

Carga glicêmica total : 99.51 

*/

Conclusão

Em resumo, a Stream API não só apresenta uma forma de escrever código de maneira mais concisa, bem como também simplifica muito a manipulação das Collections. Nos exemplos práticos, foram demonstrados suas utilizações em projetos simples, mas, além disso, em grandes projetos Backend e de APIs com o extraordinário framework Spring Boot e outros como Quarkus, a Stream fornece melhor legibilidade e facilidade de implementação de várias operações que são fundamentais em se trantando do controle apropriado de fluxo dos dados.

Fontes

Dados do trânsito : https://canalcomq.com.br/noticia/10292/9-cidades-com-o-maior-congestionamento-do-pais-voce-mora-em-uma-delas

Fórmula da carga glicêmica : https://endocrinoluperes.com.br/indice-glicemico-x-carga-glicemica

Compartilhe
Comentários (2)

DH

Douglas Holanda - 18/07/2024 20:22

Izairton Vasconcelos, obrigado, colega! Sinta-se à vontade para dar continuidade a este tema em seu artigo.

Izairton Vasconcelos
Izairton Vasconcelos - 18/07/2024 20:01

O código fornecido do primeiro exemplo utiliza a API Stream do Java para processar uma lista de cidades e determinar quais cidades têm um tempo gasto no trânsito maior do que a média de todas as cidades comprovadas.


Se vocês observarem com mais atenção, perceberão como é importante esse recurso da API Stream levantado pelo Douglas, os benefícios dessa API do Java no contexto da programação.


Observem, na lupa, o trecho do código que utiliza a API Stream: " double media = calculaMediaTempoTotal();

return cidades.stream()

  .filter(cidade -> cidade.getTempoGasto() > media)

  .map(cidade -> cidade.getNome() + " " + cidade.getTempoGasto())

  .collect(Collectors.toList()); "


Ao executar o código na classe Principal, a lista de cidades é inicializada, e o método determinaCidadePorTempoé chamado para filtrar e transformar os dados conforme a lógica descrita. As cidades com tempo gastaram no trânsito maior que a mídia nacional são impressas no console.

Essa abordagem torna o código mais expressivo e fácil de entender, destacando-se como uma das principais vantagens da API Stream.


Percebam a riqueza no significado de cada linha:

1. cidades.stream(): Converte uma lista de cidades em um Stream de objetos Cidade. Isso permite o uso das operações da API Stream.

2. filter(cidade -> cidade.getTempoGasto() > media): Filtrar as cidades, incluindo apenas aquelas cujo tempo gasto no trânsito é maior que a média calculada ( media). Aqui, cidade -> cidade.getTempoGasto() > mediaé uma expressão lambda que define a condição de filtragem.

3. map(cidade -> cidade.getNome() + " " + cidade.getTempoGasto()): Transforma cada objeto Cidadeem uma string que combina o nome da cidade e o tempo gasto no trânsito. A expressão lambda cidade -> cidade.getNome() + " " + cidade.getTempoGasto()define uma transformação.

4. collect(Collectors.toList()): Coleta os resultados do Stream em uma lista ( List<String>), que é o formato desejado de saída.


Parabéns pelo seu artigo, Douglas, você foi no ponto central dessa API Stream do Java.


Permita-me, continuar com seu tema em um novo artigo que escreverei.