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.
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();
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();
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.