Java Streams API: manipulando coleções de forma eficiente

Veja nesse artigo como manipular coleções de maneira mais simples e eficiente utilizando a API de streams do Java 8.

Motivação

Entre as diversas funcionalidades adicionadas à linguagem Java em sua versão 8 está a Streams API, recurso que oferece ao desenvolvedor a possibilidade de trabalhar com conjuntos de elementos de forma mais simples e com um número menor de linhas de código. Isso se tornou possível graças à incorporação do paradigma funcional, combinado com as expressões lambda, o que facilita a manutenção do código e aumenta a eficiência no processamento devido ao uso de paralelismo.

A proposta em torno da Streams API é reduzir a preocupação do desenvolvedor com a forma de implementar controle de fluxo ao lidar com coleções, deixando isso a cargo da API. A ideia é iterar sobre essas coleções de objetos e, a cada elemento, realizar alguma ação, seja ela de filtragem, mapeamento, transformação, etc. Caberá ao desenvolvedor apenas definir qual ação será realizada sobre o objeto.

Saiba mais sobre outros recursos do Java 8

Como criar streams

O primeiro passo para trabalhar com streams é saber como criá-las, e a forma mais comum para isso é através de uma coleção de dados. A Listagem 1 mostra como criar uma stream ao invocar o método stream() a partir da interface java.util.Collection.

Nesse trecho de código, primeiramente uma lista de strings é definida e três objetos são adicionados a ela. Em seguida, uma stream de strings é obtida ao chamar o método items.stream(), na linha 7. Outra forma de criar streams é invocando o método parallelStream(), que possibilitará paralelizar o seu processamento, oferecendo maior eficiência ao processamento.

01 List<String> items = new ArrayList<String>(); 02 03 items.add("um"); 04 items.add("dois"); 05 items.add("três"); 06 07 Stream<String> stream = items.stream();
Listagem 1. Criação de uma stream a partir de um List.

O método stream() também foi adicionado a outras interfaces, como a java.util.map, a partir da qual também podemos obter streams da mesma forma que vimos anteriormente.

Além disso, uma stream pode ser gerada a partir de I/O, arrays e valores. Para isso, basta chamar os métodos estáticos Files.lines(), Stream.of(), Arrays.stream(), respectivamente, como mostra o código a seguir:

Stream <String> lines= Files.lines(Paths.get(“myFile.txt”), Charset.defaultCharset()); Stream<Integer> numbersFromValues = Stream.of(1, 2, 3, 4, 5); IntStream numbersFromArray = Arrays.stream(new int[] {1, 2, 3, 4, 5});

Iterando sobre streams

Para iterar sobre uma coleção de dados usando streams, ou seja, percorrer os elementos em um loop, a API oferece o método forEach, que deve ser invocado a partir da stream e recebe como parâmetro a ação que será realizada em cada iteração.

No código abaixo, temos um exemplo de uso dessa funcionalidade, no qual criamos uma stream a partir de uma lista de objetos do tipo Pessoa e iteramos sobre ela, exibindo o nome de cada pessoa presente na lista:

List<Pessoa> pessoas = populaPessoas(); //Cria uma lista de Pessoa pessoas.stream().forEach(pessoa -> System.out.println(pessoa.getNome()));

Perceba que o argumento do método forEach foi passado utilizando o recurso de expressões lambda, no qual o operador pessoa faz referência a cada item da lista durante as iterações.

Seguindo essa ideia, outras operações mais complexas podem ser realizadas com cada item da lista, bastando indicar o método responsável por esse processamento como argumento de forEach.

Métodos das streams

Normalmente, para realizar ações em uma lista, como filtros e operações matemáticas, é necessário efetuar um loop sobre seus itens. Com a Streams API esse tipo de tarefa também foi simplificado, bastando agora fazer chamadas a métodos específicos que, em conjunto com as expressões lambda recebidas como parâmetro, se responsabilizam por percorrer a coleção e retornar apenas o resultado esperado.

Um desses métodos é o filter(), que é utilizado para filtrar elementos de uma stream de acordo com uma condição (predicado passado por parâmetro). O trecho de código abaixo mostra um exemplo de uso dessa operação. Primeiramente, é criada uma lista com alguns objetos do tipo Pessoa. Em seguida, é criada a stream e logo após o método filter() recebe como parâmetro uma condição, representada por uma expressão lambda, que tem por objetivo buscar todas as pessoas cuja nacionalidade é a brasileira.

List<Pessoa> pessoas = popularPessoas();//Cria uma lista de Pessoa Stream<Pessoa> stream = pessoas.stream().filter(pessoa -> pessoa.getNacionalidade().equals("Brasil"));

Outro método bastante útil nesse contexto é o average(), que permite calcular a média dos valores dos elementos. A Listagem 2 mostra um exemplo de uso dessa operação, no qual é calculada a média de idade de todas as pessoas que nasceram no Brasil.

01 List<Pessoa> pessoas = new Pessoa().populaPessoas(); 02 double media = pessoas.stream(). 03 filter(pessoa -> pessoa.getNacionalidade().equals("Brasil")). 04 mapToInt(pessoa -> pessoa.getIdade()). 05 average(). 06 getAsDouble();
Listagem 2. Exemplo de uso do método average();

De forma semelhante, também é possível realizar outras operações, como obter os valores máximo, mínimo e a soma dos elementos, sempre utilizando métodos simples e sem a necessidade de aplicar explicitamente laços de repetição.

Saiba mais sobre o Java

Artigos relacionados