Entendendo Java Stream API na prática
Tags: beginners, programming, tutorial, java
Published: 2024-01-09 13:10:16

Se você está aprendendo Java agora, ou vindo de outra linguagem, acredito que esse texto possa te ajudar. A _Stream API_ do Java é uma ferramenta muito poderosa e inversamente a esse fato, ela é muito simples. 

Com pouquíssimas linhas de código, podemos realizar operações que em outras linguagens precisaríamos de, talvez, o dobro.

## Stream API

A _Stream API_, apresentada no Java 8, consiste em uma sequencia/coleção de elementos agregados que suportam operações sequenciais e/ou paralelas:

- **stream()**
- **parallelStream()**

Com isso, a ideia é abstrair parte do processo de desenvolvimento que antes seria utilizado para "comportamentos", deixando o controle de fluxo e _loop's_ para a _Stream API_. Similar as _Threads_, onde aspectos mais complexos são encapsulados e o desenvolvedor fica responsável apenas pela implementação das regras de negócio.

Existem variações para suportar/facilitar a manipulação de tipos primitivos como:

- **IntStream()**
- **LongStream()**
- **DoubleStream()**

Dito isso, a _Stream API_ oferece dois tipos de operações: Operações intermediárias e Operações terminais.

## Operações intermediárias

Basicamente, operações intermediárias retornam novas stream's e permitem que o programador continue a concatenar/utilizar novas funções.

### Filter 

A operação **filter()** é usado para filtrar elementos de uma _stream_ de acordo com uma condição, um predicado, e retorna uma nova _stream_ contendo apenas os elementos que satisfazem à condição.

O código a seguir mostra um exemplo de uso dessa operação. Primeiramente é criada uma lista com alguns objetos do tipo **Usuario**:

```
public class Usuario {
    private String nome;
    private int idade;
    private String email;
    private String senha;;

    public Usuario(String nome, int idade, String email, String senha) {
        this.email = email;
        this.nome = nome;
        this.idade = idade;
        this.senha = senha;
    }

    @Override
    public String toString() {
        return "Usuario [nome=" + nome + 
                      ", idade=" + idade + 
                      ", email=" + email + 
                      ", senha=" + senha + "]";
    }
}
```


```
    private static List<Usuario> populaUsuarios(){
        return  List.of(new Usuario("Ana", 25, "<EMAIL>", "123456"), 
                        new Usuario("Bruno", 21, "<EMAIL>", "123456"),
                        new Usuario("Caio", 23, "<EMAIL>", "123456"),
                        new Usuario("Daniel", 26, "<EMAIL>", "123456"),
                        new Usuario("Simão", 27, "<EMAIL>", "123456"),
                        new Usuario("Pedro", 28, "<EMAIL>", "123456"),
                        new Usuario("Maria", 25, "<EMAIL>", "123456"),
                        new Usuario("João", 25, "<EMAIL>", "123456"),
                        new Usuario("Marcos", 22, "<EMAIL>", "123456"),
                        new Usuario("Paulo", 23, "<EMAIL>", "123456"));                  
    }
```

Agora, vamos montar uma nova lista apenas com usuários com 25 anos ou mais:

```
       public static void main(String[] args) throws Exception {
        var listaUsuarios = populaUsuarios();

        var listaUsuariosComVinteCincoAnos = 
                listaUsuarios
                             .stream()
                             .filter(usuario -> usuario.idade >= 25);
        
        listaUsuariosComVinteCincoAnos.forEach(System.out::println);
}
```

Chegando ao resultado:


![Lista de usuários com mais de 25 anos](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0sjizzf3c8f11ix0wvpp.png)

### Map

Em algumas situações nos deparamos com a necessidade de transformarmos/modificarmos os objetes da nossa lista. E ai entra a operação **map()**, que nos permite realizar essas mudanças sem a necessidade de variáveis intermediárias.

Usando uma função como argumento que, assim como o predicado na operação **filter()**, também é uma interface funcional. Tornando cada elemento da _Stream_ em um parâmetro e retornando o elemento processado como resposta. 

Dito isso, imagine que queremos transformar nossa lista de usuários em uma lista de pessoas, para ocultar algumas informações. Para isso vamos criar uma nova classe: **Pessoa**

```
public class Pessoa {
    private String nome;
    private int idade;

    public Pessoa(Usuario usuario) {
        this.nome = usuario.getNome();
        this.idade = usuario.getIdade();
    }

    @Override
    public String toString() {
        return "Pessoa [nome=" + nome + ", idade=" + idade + "]";
    }

}
```

Aplicando a operação **map()**, teremos os seguinte código:

```
public class App {
    public static void main(String[] args) throws Exception {
        var listaUsuarios = populaUsuarios();

        var listaPessoas = 
               listaUsuarios
                            .stream()
                            .map(usuario -> {return new Pessoa(usuario);});

        listaPessoas.forEach(System.out::println);
    }
}
```

Chegando ao resultado:

![Lista de usuários convertidos em pessoas](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m6qp9xcvivv6jz8rhmgj.png)

Lembra que as _Streams_ possuem abstrações para otimizar a manipulação de tipos primitivos (**IntStream**, **LongStream**, **DoubleStream**) e assim evitar _overhead_? Então, vou demonstrar isso com a operação **map()**. 

Utilizando nossa lista de pessoas, vamos criar uma lista contendo apenas a idade das pessoas que estão na lista "original", para isso é preciso implementar os **getters** da classe **Pessoa**:

```
public class Pessoa {
    private String nome;
    private int idade;

    public Pessoa(Usuario usuario) {
        this.nome = usuario.getNome();
        this.idade = usuario.getIdade();
    }

    @Override
    public String toString() {
        return "Pessoa [nome=" + nome + ", idade=" + idade + "]";
    }

    public String getNome() {
        return nome;
    }

    public int getIdade() {
        return idade;
    }

}
```

Agora vamos montar nossa lista de idades, que por baixo dos panos é uma _Stream< int >_:

```
public class App {
    public static void main(String[] args) throws Exception {
        var listaUsuarios = populaUsuarios();

        var listaPessoas = 
               listaUsuarios
                            .stream()
                            .map(usuario -> {return new Pessoa(usuario);});

        var listaPessoaIdade =
                       listaPessoas
                      .stream()
                      .mapToInt(Pessoa::getIdade)
                      .boxed()
                      .toList();    

        listaPessoaIdade.forEach(System.out::println); 
    }
}
```
Chegando ao resultado:


![Lista com a idade das pessoas](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ny0n5ts25pmtqns5eb7x.png)

É importante reduzir o _overhead_ pois nem existem situações onde será necessário operar sobre _Streams_ de tamanho indeterminado. Para contextos controlados, talvez não faça tanto sentindo se preocupar com o custo computacional das operações.

### Sorted

Ordenar elementos em uma coleção é uma tarefa recorrente para todo desenvolvedor. E no Java isso é bastante simples, as _Stream API_ oferece a operação **sorted()**. Retornando uma nova _stream_ contendo os elementos da _stream_ original ordenados de acordo com o critério fornecido.

Utilizando o método **comparing()** da interface **Comparator**, é fornecida uma _Function_ como parâmetro e assim obtemos um valor chave que será utilizado na ordenação. 

```
public class App {
    public static void main(String[] args) throws Exception {
        var listaUsuarios = populaUsuarios();

        var listaPessoa = listaUsuarios
                .stream()
                .map(usuario -> {
                    return new Pessoa(usuario);
                });
        
        var listaPessoaOrdenadoPorIdade = listaPessoa
                .stream()
                .sorted(Comparator.comparing(Pessoa::getIdade));       
                
        listaPessoaOrdenadoPorIdade.forEach(System.out::println);
                
        var listaPessoaOrdenadoPorNome = listaPessoa
                .stream()
                .sorted(Comparator.comparing(Pessoa::getNome));        

        listaPessoaOrdenadoPorNome.forEach(System.out::println);
    }
}

```

Nesse caso, a classificação das informações foi feita primeiro por idade:

![Lista de pessoas ordenadas por idade](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7mvm8ap2zwzvi6akj31h.png)

E depois de acordo com o nome da pessoa, utilizando a ordem natural (alfabética) definida na interface **Comparator** para classificar **Strings**:

![Lista de pessoas ordenadas por nome](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g14xh7vc9zpe5vncsylu.png)

Como pôde ver, através de _method reference_ conseguirmos usar a referência dos métodos **getIdade()** e **getNome()** da classe Pessoa como parâmetro para a operação **Comparator.comparing()**.

###Distinct

 A operação **distinct()** retorna uma _stream_ contendo apenas elementos que são exclusivos, isto é, que não se repetem, de acordo com a implementação do método **equals()** e **hashCode()**. 

Ou seja, é necessário sobrescrever esses métodos dentro na nossa classe **Pessoa**:

```
public class Pessoa {
    private String nome;
    private int idade;

    public Pessoa(Usuario usuario) {
        this.nome = usuario.getNome();
        this.idade = usuario.getIdade();
    }

    @Override
    public String toString() {
        return "Pessoa [nome=" + nome + ", idade=" + idade + "]";
    }

    public String getNome() {
        return nome;
    }

    public int getIdade() {
        return idade;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((nome == null) ? 0 : nome.hashCode());
        result = prime * result + idade;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;

        if (obj == null)
            return false;

        if (getClass() != obj.getClass())
            return false;

        Pessoa other = (Pessoa) obj;

        if (nome == null) {
            if (other.nome != null)
                return false;
        } else if (!nome.equals(other.nome))
            return false;
        if (idade != other.idade)
            return false;

        return true;
    }
}
```

Após sobrescrever os métodos **equals()** e **hashCode()**, vamos adicionar usuários repetidos a nossa lista e utilizar a operação **distinct()**:

```
    private static List<Usuario> populaUsuarios(){
        return  List.of(new Usuario("Ana", 25, "<EMAIL>", "123456"), 
                        new Usuario("Bruno", 21, "<EMAIL>", "123456"),
                        new Usuario("Caio", 23, "<EMAIL>", "123456"),
                        new Usuario("Daniel", 26, "<EMAIL>", "123456"),
                        new Usuario("Simão", 27, "<EMAIL>", "123456"),
                        new Usuario("Pedro", 28, "<EMAIL>", "123456"),
                        new Usuario("Maria", 25, "<EMAIL>", "123456"),
                        new Usuario("João", 25, "<EMAIL>", "123456"),
                        new Usuario("Marcos", 22, "<EMAIL>", "123456"),
                        new Usuario("Paulo", 23, "<EMAIL>", "123456"),

//Nomes Repetidos
                        new Usuario("Ana", 25, "<EMAIL>", "123456"), 
                        new Usuario("Bruno", 21, "<EMAIL>", "123456"));                  
    }
```

```
public class App {
    public static void main(String[] args) throws Exception {
        var listaUsuarios = populaUsuarios(); 
        
        var listaPessoa = listaUsuarios
                .stream()
                .map(usuario -> {
                    return new Pessoa(usuario);
                })
                .distinct();       
                 
        listaPessoa.forEach(System.out::println);
    }
}
```

Chegando ao resultado:

![Lista de usuários sem redundâncias](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3fx1zt0nsssajvnzc7a9.png)

### Limit

A operação **limit()** retorna uma nova _Stream_ com apenas a quantidade de elementos passada por parâmetro. Também é conhecida como curto-circuito pois não precisa processar todos os elementos da _Stream_ original. 

```
public class App {
    public static void main(String[] args) throws Exception {
        var listaUsuarios = populaUsuarios();
   
        var listaPessoa = listaUsuarios
                .stream()
                .map(usuario -> {
                    return new Pessoa(usuario);
                })
                .limit(3);       

        listaPessoa.forEach(System.out::println);
    }
}
```

Chegando ao resultado:

![Lista de pessoas limitando a 3 resultados](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fdcxgm0gl0eneo7iowov.png)

## Operações terminais

Enquanto operações terminais retornam objetos e/ou resultados que, diferente das operações intermediárias, não são do tipo _Stream_. São consideradas operações terminais as operações:

### ForEach

Através da operação **forEach()** é possível realizar um _loop_ sobre todos os elementos de uma _Stream_ e executar algum tipo de processamento. 

```
public class App {
    public static void main(String[] args) throws Exception {
        var listaUsuarios = populaUsuarios();

        listaUsuarios
                .stream()
                .map(usuario -> {
                    return new Pessoa(usuario);
                })
                .forEach(pessoa -> {
                         System.out.println(pessoa.getNome());
                                   });               
    }
}
```

No exemplo, utilizamos _lambda_ para invocar o método **getNome()** do objeto pessoa e passa-lo como parâmetro para a operação **forEach()**. Desta forma será exibidos apenas os nomes de todas as pessoas presentes na _Stream_.

### Average

Para tipos primitivos, a operação **average()** permite calcular a média dos valores de uma _Stream_. Como exemplo, para calcular a média de idade das pessoas, teremos um código similar ao seguinte:

```
public class App {
    public static void main(String[] args) throws Exception {
        var listaUsuarios = populaUsuarios();

        var listaPessoas = listaUsuarios
                .stream()
                .map(usuario -> {
                    return new Pessoa(usuario);
                });
        
        var value = listaUsuarios
                                 .stream()
                                 .mapToInt(pessoa -> pessoa.getIdade())
                                 .average()
                                 .getAsDouble();

        System.out.println("A média de idade das pessoas é: " + value);
    }
}
```

Tendo como resultado:

![Média de idade dos usuários presentes na lista](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uf3k4ljlir4fncx1he6p.png)

Repare que no exemplo acima foi utilizada a operação **getAsDouble()**, isso ocorre pois a operação **average()** não retorna um valor numérico mas sim um objeto da classe **java.util.Optional**. Objetos do tipo **Optinonal** nos permitem lidar com algumas situações de forma simples.

### Collect

A operação **collect()** permite a conversão de uma _Stream_ em uma _Collection_, convertendo para os tipos: 

- **List**
- **Set** 
- **Map** 

Ao chamarmos a operação **collect()**, determinando via argumento qual _Collection_ será retornada através da classe **Collectors**. Como por exemplo:

- **Collectors.toList()**
- **Collectors.toMap()**
- **Collectiors.toSet()**

```
    public static void main(String[] args) throws Exception {
        var listaUsuarios = populaUsuarios();

        var listaPessoasList = 
               listaUsuarios
                            .stream()
                            .map(usuario -> {return new Pessoa(usuario);})
                            .collect(Collectors.toList());

        System.out.println("List: ");
        System.out.println(listaPessoasList.toString());
        System.out.println();

        var listaPessoasMap = 
               listaUsuarios
                            .stream()
                            .map(usuario -> {return new Pessoa(usuario);})
                            .collect(Collectors.toMap(Pessoa::getNome, Pessoa::getIdade));

        System.out.println("Map: ");                            
        System.out.println(listaPessoasMap.get("Ana"));
        System.out.println();

        var listaPessoasSet = 
               listaUsuarios
                            .stream()
                            .map(usuario -> {return new Pessoa(usuario);})
                            .collect(Collectors.toSet());

        System.out.println("Set: ");
        System.out.println(listaPessoasSet);
        System.out.println();

    }
```

Chegando ao resultado:

![Resultado da conversão para: List, Map e Set](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7rkjq4edqfkyh4lfwvjw.png)

### Count

A operação **count()** retorna a quantidade de elementos presentes em uma _Stream_. Assim como **average()**, também é classificado como uma operação de redução (_reduction_). 

Por exemplo, se quisermos retornar apenas as pessoas cujo o nome se inicia com a letra 'M':

```
    public static void main(String[] args) throws Exception {
        var listaUsuarios = populaUsuarios();

        var listaPessoas = 
               listaUsuarios
                            .stream()
                            .map(usuario -> {return new Pessoa(usuario);})
                            .filter(pessoa -> pessoa.getNome().toUpperCase().startsWith("M"))
                            .count();

        System.out.println("Quantidade de pessoas com a letra inicial do nome M: " + listaPessoas);
    }
```

Teremos como resultado:

![Quantidade de pessoas cujo nome começa com a letra 'M'](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8efm8g8jtb691i91xduf.png)

### AllMatch

A operação **allMatch()** verifica se todos os elementos de uma _Stream_ atendem a um critério passado como parâmetro, através de um _Predicate_, e retorna um valor booleano.  

Imagine que queremos saber se todas as pessoas da nossa lista são maiores de idade, com a operação **allMatch()** teríamos um código similar a este:

```
    public static void main(String[] args) throws Exception {
        var listaUsuarios = populaUsuarios();

        var todasAsPessoasSaoMaioresDeIdade = 
               listaUsuarios
                            .stream()
                            .map(usuario -> {return new Pessoa(usuario);})
                            .allMatch(pessoa -> {return pessoa.getIdade() >= 18;});

        System.out.println(todasAsPessoasSaoMaioresDeIdade);
    }
```
Resultado: true.

## Collections e Streams

Apesar da semelhança entre _Collections_ e _Streams_, os objetivos são bem diferentes. As _Collections_ tem como principal objetiva a facilidade e eficiência na gestão de seu elementos, fornecendo meios simples intuitivos de armazenamento e acesso de objetos armazenados. 

Enquanto as Streams, apesar de fornecer meios de acessar e manipular seus elementos, não permite que você os altere diretamente. Tendo como objetivo maior descrever declarativamente sua fonte e as operações computacionais que serão executadas sobre essa fonte.

## Conclusão

Se você é um dev mais "cascudo" (veio), esse artigo pode parecer simplório e até defasado, afinal tudo isso foi introduzido ao Java na versão 8. Mas para novos programadores e/ou pessoas que estão chegando agora na linguagem, espero que esse artigo possa ajudar a entender um pouco do poder do Java e da funcionalidades/facilidades que a linguagem nos oferece.

Um grande abraço e até a próxima!