Agora que já vimos sobre o uso de coleções em Java, estudaremos sobre a Streams API, um recurso introduzido em Java 8 que fornece uma abordagem declarativa para manipular coleções, tornando o processo mais simples e eficiente. Em uma linguagem de programação imperativa, descrevemos os comandos que o programa irá executar, determinando o que ele deve fazer e como fazer. Por outro lado, em programação declarativa, escrevemos o que o programa deve fazer e a linguagem se encarrega de como fazer. Dessa forma, o programa pode ficar mais conciso e tem um potencial de ser mais eficiente.

sumEven :: [Int] -> Int
sumEven xs = sum (filter even xs)
int sumEven(List<Integer> numbers) {
    int sum = 0;
    for (int n : numbers) {
        if (n % 2 == 0) {
            sum += n;
        }
    }
    return sum;
}

Para usar a Streams API, você fará extensivo uso de expressões lambda, que são uma forma concisa de criar implementações de interfaces funcionais. Elas permitem tratar funções como objetos e passá-las como argumentos de métodos ou retorná-las como valores de métodos.

O uso da Streams API proporciona uma forma diferente de lidar com conjuntos de elementos, oferecendo ao desenvolvedor uma maneira simples e concisa de escrever código que resulta em facilidade de manutenção e paralelização sem efeitos indesejados em tempo de execução.

Streams

Streams (chamadas de fluxos, de agora em diante) **não são estruturas de dados, mas sim uma sequência de elementos que suporta operações de agregação realizadas de forma sequencial ou paralela. Representam um pipeline de operações de processamento de dados que podem ser executadas em uma fonte de elementos, como coleções, arrays ou canais de entrada e saída. Pipelines são uma cadeia de operações que formam um fluxo de processamento.

Fluxos usam avaliação tardia (lazy evaluation), fazendo com que elementos do fluxo sejam processados somente quando necessário. Operações intermediárias são tipicamente avaliadas de forma tardia; assim, elas produzem um novo fluxo sem de fato processar os elementos, até que uma operação final seja chamada.

Diferente de coleções, fluxos não armazenam elementos. Em vez disso, processam elementos conforme passam pelo pipeline de operações.

Para criar um Stream, você pode invocar o método stream() a partir de uma lista ou de um array, como no exemplo seguinte.

// Lista
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = numbers.stream();

// Array
int[] array = {1, 2, 3, 4, 5};
Stream<Integer> stream = Arrays.stream(array);

Operações intermediárias

Algumas das operações intermediárias mais utilizadas são: filter()map()sorted()limit() e distinct(). Essas operações permitem manipular e transformar dados dentro do fluxo.

filter

Filtra elementos de um fluxo de acordo com uma condição (predicado), que é uma função com valor de retorno igual a um boolean) e retorna um novo fluxo contendo apenas os elementos que satisfazem à condição. O exemplo de código a seguir filtra apenas os números pares da lista de inteiros nums.

List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Stream<Integer> res = nums.stream()
		.filter(e -> e % 2 == 0);

map

Permite realizar transformações em uma lista de dados utilizando como argumento uma função que toma cada elemento do fluxo como parâmetro e retorna o elemento processado como resposta. O resultado será um novo fluxo contendo os elementos mapeados a partir do fluxo original. Antes de mostrar um exemplo de uso do map, considere a classe Pessoa declarada a seguir.

public class Pessoa {
    public String nome;
    public int idade;
    public Pessoa(String nome, int idade) {
        this.nome = nome;
        this.idade = idade;
    }
}

O exemplo de código a seguir extrai apenas a idade dos objetos do tipo Pessoa que estão na lista pessoas. Como resultado, teremos uma Stream<Integer>, ou seja, um fluxo de inteiros.