DEV Community

Kauê Gatto
Kauê Gatto

Posted on

Streams em JAVA: Tudo que você precisa saber

Map, Filter, Reduce

Vamos começar com um exemplo?

* Given a list of people
* We need to compute the average of the age of those people
* For the people older than 20
Enter fullscreen mode Exit fullscreen mode
  1. Nesse caso, é meio claro, começamos com um objeto pessoa, mas trabalharemos / transformaremos o dado de uma maneira que consigamos a idade (map). O map pega um objeto, e mapeia para outro, geralmente de tipo diferente

Map, um objeto entra, é transformado (seta) e sai outro

  1. Com isso, vamos filtrar o dado age, para que ele só compute a média de maiores de 20 anos. Isso é um filter. O filter literalmente filtra os dados, não os transforma (diferente de map) e decide se deve manter ou não o dado. 2.1. Filters em código recebem uma predicate como parâmetro, ela representa uma função que recebe um argumento e retorna um valor booleano.

Filter, entra idade, passa pelo filter (seta) e voltam apenas idades >20

  1. Por fim, vamos calcular a média desse dado, já transformado de pessoas e filtado para que a idade seja >20. Essa operação avg é o reduce, nesse caso, vamos pensar nela a princípio como uma agregação (tipo min,max,count, etc).

Image description

Operação Comportamento
Map Transforma o dado, mudando seu tipo. Não muda o número de elementos.
Filter Não transforma o dado, reduz (ou mantém igual) o número de elementos
Reduce Combina os elementos em um resultado único. É uma operação terminal que produz um único valor com base em uma operação de redução, como soma, média ou máximo.

Por que usar Streams?

List<Person> people = ...;

int sum = 0;
int count = 0;
for (Person person: people) {
    if (person.getAge() > 20) {
        count++;
        sum += person.getAge();
    }
}
double average = 0d;

if(count > 0) {
    average = sum / count;
}
Enter fullscreen mode Exit fullscreen mode

No exemplo acima, temos um código que descreve com detalhes o que deve ser feito para chegarmos em um resultado (o resultado do exemplo apresentado), e se mudarmos o algoritmo, precisamos mudar o código, mesmo que o algoritmo seja o mesmo. Não necessariamente precisa ser assim, em SQL, por exemplo, escreveríamos:

Select AVG(age) from People
Where People.age>20
Enter fullscreen mode Exit fullscreen mode

Note que no exemplo acima, descrevemos ao código o que queremos que seja feito, as premissas, descrevemos o resultado, e não como o resultado deve ser computado.

Collection X Streams

Tudo certo, se o código faz parte de uma lista ou qualquer coleção, podemos tentar implementar assim:

List<Person> people = ...;
double average = 
people.map(person -> person.getAge())
    .filter(age  > age > 20)
    .average();
Enter fullscreen mode Exit fullscreen mode

ERRADO! A API de collections não provê esse tipo de operações, o código acima não compila 😄!

Mas por que a API de Collections e Streams são separadas?

Imagine que essas operações são feitas em uma lista de 1.000.000 de pessoas! Portanto, após o primeiro map, você teria uma Lista com 1.000.000 de inteiros de idade (porque o map não reduziria o tamanho em si), acho que já deu para perceber que duplicar a collection vai ser altamente custoso para o processador e para a memória

💡 Um objeto de stream oferece operações de map / filter / reduce sem duplicação e com máxima eficiência!

Mas como isso funciona? Quando usamos pessoas.stream(), retornamos uma Stream<Pessoa>. Por definição, um objeto de stream não carrega dados, é grátis cria-lo.

A collection is an in-memory data structure to hold values and before we start using collection, all the values should have been populated. Whereas a java Stream is a data structure that is computed on-demand. Java Stream doesn’t store data, it operates on the source data structure (collection and array) and produce pipelined data that we can use and perform specific operations.

https://www.digitalocean.com/community/tutorials/java-8-stream

Operações Intermediárias X Finais

Streams atuam sobre coleções para realizar map / filter / reduce, operações intermediárias são operações sobre streams que retornam streams ( Stream<T> map() → Método de stream, retorna stream).

Operações finais são operações que retornam a coleção de novo, depois do processamento / pipelining acabar toList() é um exemplo

E como ficaria o código

Aqui está um exemplo de como ficaria o código.

List<Pessoa> pessoas = new ArrayList<>();

    pessoas.stream()
                 .map(...)
                 .filter(...)
                 .average();
Enter fullscreen mode Exit fullscreen mode

💡 Importante: Você não pode processar a mesma stream duas vezes:

Stream personNames = personStream.map(Person::getName);
Stream emptyNames = personNames.filter( name -&gt; name.isEmpty())
Stream notEmptyNames = personNames.filter( name -&gt; !name.isEmpty())
Enter fullscreen mode Exit fullscreen mode

Esse código quebrará, tendo em vista que estamos processando a mesma stream personNames duas vezes!

Stream personNames = personStream.map(Person::getName);
Stream personAges = personStream.map(Person::getAge);
Enter fullscreen mode Exit fullscreen mode

Nesse caso, estamos aplicando o map para criar duas streams diferentes, não quebrará o código.

Stream emptyNames = personStream.map(Person::getName).filter(name -&gt; name.isEmpty());
Stream notEmptyNames = personStream.map(Person::getName).filter(name -&gt; !name.isEmpty());
Enter fullscreen mode Exit fullscreen mode

Esse código, processando duas streams diferentes também funcionará

Por favor, não crie variáveis para trabalhar com streams, fiz por didática

FlatMapping

Flatmapping funciona para lidar com relações 1:N

Exemplo: Cidades, onde temos várias pessoas por cidade.

Image description

Suponha que queremos pegar todas as pessoas, independente de suas cidades. Nesse caso, nos preocupamos com a entidade relacionada, não com as cidades em si*. O flatmap faz isso, pega uma entidade Cidade e retorna para nós uma Stream<Pessoas>.*

List<City> cities = ...;

Function<City, Stream<Person>> flatMapper = 
    city -> city.getPeople().stream();

long count = 
    cities.stream()
    .flatMap(flatMapper) /*poderíamos colocar city -> city.getPeople().stream() direto*/
    .count()
Enter fullscreen mode Exit fullscreen mode

💡 Uma operação de flatmap recebe um objeto e retorna uma stream de outros objetos

List words "Gomu","Gomu", "No", "Mi");

Stream streamStream =
words.stream()
.map(w -&gt; w.split("")) // Stream
.flatMap(Arrays::stream) ; // Stream
Enter fullscreen mode Exit fullscreen mode

O flatmap funciona nesse caso, pois ao aplicar flatMap(Arrays::stream), você está dizendo ao Java para pegar cada array de caracteres da Stream e transformá-lo em uma stream de strings (Stream) usando o método estático Arrays.stream().

Streams a partir de RegEx

String sentence = "the quick brown fox jumps over the lazy dog";
String[] words = sentence. split(
Stream<String> wordsStream = Arrays.stream(words);
Ion count = wordsStream.count()
Enter fullscreen mode Exit fullscreen mode

Ao fazermos dessa forma, criamos um array intermediário, perdemos o propósito de streams.

Podemos fazer:

Pattern pattern = Pattern.compile(" ");
long count = pattern.sp1itAsStream(sentence) . count();
Enter fullscreen mode Exit fullscreen mode

Streams de outro tipo

Em java, existem interfaces de stream feitas para manipular certos primitivos mais adequadamente, um exemplo disso é a IntStream

IntStream

IntStream tende a ser mais eficiente em termos de espaço e desempenho quando você está trabalhando com valores inteiros primitivos, uma vez que evita a criação de objetos Integer.

A IntStream oferece operações especializadas para trabalhar com valores inteiros, como sum(), average(), range(), rangeClosed(), etc. Uma Stream<Integer> fornece operações de stream genéricas aplicáveis a objetos, mas é menos eficiente quando se trata de cálculos com tipos primitivos.

💡 Além do IntStream, também existem LongStreame DoubleStream(Além da Stream)

Refactoring → Exemplo com múltiplas tarefas ao mesmo tempo

Streams não são uma boa para realizarem múltiplas tarefas simultaneamente, portanto se temos um for loop que agrega valores à 3 variáveis diferentes, precisamos dividir em 3 for’s (repetidos) e trocar cada um deles por uma stream. Claro, isso diminui muito a performance, mas em um for com poucos elementos a serem iterados, será basicamente imperceptível.

public String statement() {

    double totalAmount = rentals.stream()
        .mapToDoub1e(this::computeRenta1Amount)
        .sum();
    int frequentRenterPoints = rentals.stream()
    .mapToInt (this::getFrequentRenterPoints)
    .sum();

    String statement = composeHeader();

    statement += rentals.stream()
    .map(this::computeStatementLine)
    .collect(Collectors.joining());

}
Enter fullscreen mode Exit fullscreen mode

Reduções

Antes, ouvimos que redução era algo similar a uma agregação do SQL, vamos continuar com isso em mente, mas como isso funciona?

Vamos ter como exemplo a soma, a soma é uma uma implementação de BinaryOperator<Integer>, que pega dois elementos e os soma ( sum = (i1,i2) -> i1+i2;). Isso acontece com dois elementos por vez, associando os valores e depois somando com o outro.

Reduções de uma stream vazia

Mas e qual a redução de uma Stream vazia? A redução de uma Stream vazia é o seu elemento identidade (identity), um elemento identidade é um valor pré-definido que atua como ponto de partida em uma operação de redução. Em contextos de programação funcional, quando não há elementos para combinar na Stream, a operação de redução retorna o elemento identidade como resultado, garantindo que a operação seja definida e não cause exceções em casos de Stream vazia. Isso é especialmente útil para lidar com casos em que não há dados disponíveis para a operação específica, permitindo que o código se comporte de maneira previsível e segura.

Por exemplo, o identity de uma soma é 0, tendo em vista que o 0 não impactará no valor final, para múltiplicação, um.

Max, Min, Avg?

O "identity element" ou valor inicial para as operações de max, min e average em Streams do Java é um pouco diferente porque essas operações retornam um Optional, que pode ser vazio (caso a Stream esteja vazia) ou conter um valor, portanto, essas operações não tem valores identity.

Portanto, para acessar max, min e avg, você deve fazer algo do tipo:

Optional<Integer> max = numbers.stream().max(Integer::compareTo);
Optional<Integer> min = numbers.stream().min(Integer::compareTo);
OptionalDouble average = numbers.stream().mapToDouble(Integer::doubleValue).average();

    System.out.println("Max: " + max.orElse(null));
    System.out.println("Min: " + min.orElse(null));
    System.out.println("Average: " + average.orElse(Double.NaN));
Enter fullscreen mode Exit fullscreen mode

Como podemos escrever nossas reductions?

Talvez você esteja se perguntando o porquê de entendermos também a fundamentação de identity elements, se ainda não ficou claro, vai ficar agora:

A operação reduce em Streams do Java tem dois parâmetros principais:

  1. O valor inicial (identity element): Já foi explicado
  2. Uma função de acumulação (accumulator function): Esta função é usada para combinar os elementos da Stream em um único resultado. A função deve ser uma expressão lambda ou um método de referência que aceite dois argumentos e retorne um resultado. Em soma, seria (a, b) -> a + b

💡 Se você não passar um identity correto, o resultado estará incorreto.

Se sua stream não possuir um identity, você pode usar uma outra sobrecarga do método que não recebe o identity, mas retorna um Optional. Use-o somente se não possuir o identity…

Reduções em um container mutável

Reduções em um container mutável referem-se à aplicação de operações de redução, como soma, multiplicação, média, entre outras, a elementos armazenados em um contêiner que pode ser alterado durante o processo de redução. Um "container" neste contexto pode ser definido como uma estrutura de dados flexível que permite armazenar e modificar elementos de forma dinâmica (Lists, Maps, etc.). Portanto, podemos simplificar reduções em containers mutáveis como reduções em coleções.

Ao chamar .stream() em uma lista, você está criando uma Stream dos elementos contidos na lista. A Stream é uma sequência de elementos que pode ser processada de maneira funcional, mas não modifica a lista original. Quando você chama .max(), por exemplo, está solicitando o elemento máximo com base em algum critério da lista, mas a lista em si permanece a mesma.

Coletores!

List<Person> pessoasDaBaixada = new ArrayList() ;
pessoasDaBaixada.stream()
.filter(p -> p.getDDD().equals("013"))
.forEach(p -> pessoasDaBaixada.add(p));
Enter fullscreen mode Exit fullscreen mode

Mas esse exemplo acima não é muito diferente do que já vimos, por que estamos focando nesse tipo de redução?

Para filtrar e coletar elementos em uma nova lista usando a API de Streams do Java, você deve usar um coletor adequado, como Collectors.toList()

Os coletores permitem que você capture os resultados das operações de redução em coleções ou outros tipos de dados concretos. Em outras palavras, eles transformam os elementos processados em uma Stream em uma coleção real que pode ser usada e manipulada posteriormente.

List<Person> pessoasDaBaixada = new ArrayList<>();

// Supondo que o número de DDD seja uma String
List<Person> pessoasComDDD13 = pessoasDaBaixada.stream()
    .filter(p -> "13".equals(p.getDDD()))
    .collect(Collectors.toList());
    //.toList() retornará uma lista imutável!
Enter fullscreen mode Exit fullscreen mode

”O novo método Stream.toList() não produz nem uma lista não modificável nem é um atalho para collect(toUnmodifiableList()), porque toUnmodifiableList() **não aceita valores nulos. A implementação de Stream.toList() não é limitada pela interface Collector; portanto, Stream.toList() aloca menos memória. Isso a torna ideal para uso quando o tamanho da stream é conhecida antecipadamente.” Link para comentário no stackoverflow

Exemplos de Outros Coletores

  • Collectors.toSet(): Cria um conjunto a partir dos elementos da Stream, removendo duplicatas.
  • Collectors.toMap(keyMapper, valueMapper): Cria um map a partir dos elementos da Stream, usando funções de mapeamento para extrair chaves e valores.
  • Collectors.groupingBy(classifier): Agrupa elementos da Stream com base em um critério definido pela função de classificação.
  • Collectors.joining(delimiter): Concatena os elementos da Stream em uma única String usando um delimitador.
  • Collectors.summingInt() ou Collectors.summingLong(): Calcula a soma dos valores inteiros ou longos de elementos da Stream.

Exemplos de uso:

Map<Integer, List<Person>> pessoasPorIdade = pessoas.stream()
    .collect(Collectors.groupingBy(Person::getIdade));
Enter fullscreen mode Exit fullscreen mode
List<Person> pessoas = Arrays.asList(
    new Person("Jorge", 25),
    new Person("Ben", 30),
    new Person("Jor", 35)
);

Map<String, Integer> mapNomeIdade = pessoas.stream()
    .collect(Collectors.toMap(Person::getNome, Person::getIdade));
Enter fullscreen mode Exit fullscreen mode

💡 Não necessariamente precisamos de collectors para trabalhar com coleções, mas eles tornam a vida mais fácil, veja os dois exemplos anteriores:


// ex1
Map<Integer, List<Person>> pessoasPorIdade = new HashMap<>();
pessoas.forEach(person -> {
    Integer idade = person.getIdade();
    List<Person> pessoasComIdade = pessoasPorIdade.get(idade);
    if (pessoasComIdade == null) {
        pessoasComIdade = new ArrayList<>();
        pessoasPorIdade.put(idade, pessoasComIdade);
    }
    pessoasComIdade.add(person);
});

// ex2
Map<String, Integer> mapNomeIdade = new HashMap<>();
pessoas.stream().forEach(person -> mapNomeIdade.put(person.getNome(), person.getIdade()));
Enter fullscreen mode Exit fullscreen mode

Pincelando: Streams Paralelas

À medida que exploramos as reductions e operações terminais, é crucial considerar o potencial das streams paralelas em Java, mas o que são? 🤔

Streams paralelas oferecem a capacidade de executar operações em paralelo, aproveitando múltiplos núcleos da CPU, o que melhora muito o desempenho da stream.

Apesar disso, vale dizer que operações em paralelo nem sempre podem ser consideradas em uma stream, suponha que você esteja calculando uma média com uma reduction, para calcularmos a média, precisamos primeiro somar os elementos, e então dividirmos, se somarmos e depois dividirmos partes distintas, isso trará um resultado incorreto, isso significa que a média é uma operação não associativa!

Operações Associativas

São operações em que a ordem em que os elementos são combinados não afeta o resultado. Exemplos comuns de operações associativas incluem soma e multiplicação.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// Cálculo da soma em uma stream paralela
int sum = numbers.parallelStream().reduce(0, Integer::sum);
Esse parágrafo tem como objetivo apenas introduzir e notificar a existência de streams paralelas, procure saber mais sobre elas!
Enter fullscreen mode Exit fullscreen mode

Operações Não-Associativas

São o oposto!

Conclusão e Agradecimento

Obrigado pela leitura, espero que tenha sido produtiva e que você tenha todo o conhecimento necessário para conseguir dar seus próprios passos e construir streams funcionais e avançadas, caso necessário.

Referências

https://app.pluralsight.com/library/courses/692a1310-42db-4f3c-a33b-208a55b7bd84/table-of-contents

https://www.digitalocean.com/community/tutorials/java-8-stream

Maratona Java Virado no Jiraya

https://acervolima.com/coletores-java/

Top comments (4)

Collapse
 
prsaya profile image
Prasad Saya

Max, Min, Avg?

You can also use IntSummaryStatistics:

IntSummaryStatistics stats = IntStream.of(2, 5, 9).summaryStatistics();
Enter fullscreen mode Exit fullscreen mode

java.util.IntSummaryStatistics is an object for collecting statistics such as count, min, max, sum, and average. For example stats.getAverage(), stats.getMax(), etc., or just print all statistics using the stats.toString().

Collapse
 
kauegatto profile image
Kauê Gatto

Totally forgot about IntSummaryStatistics, thanks for the complement!

Collapse
 
prsaya profile image
Prasad Saya

I know there are a lot of features in the Streams API. It may not be possible to list everything :)

Collapse
 
kauegatto profile image
Kauê Gatto

Peguei o texto do meu notion e post do medium, caso haja algum problema que não percebi, me avisem!