Bem-vindo ao tutorial do Java 8 Stream API. Nos últimos posts sobre o Java 8, analisamos as Alterações na Interface do Java 8 e as Interfaces Funcionais e Expressões Lambda. Hoje, vamos examinar uma das principais APIs introduzidas no Java 8 – Java Stream.
Java 8 Stream
- Java 8 Stream
- Coleções e Java Stream
- Interfaces Funcionais no Java 8 Stream
- java.util.Optional
- java.util.Spliterator
- Operações Intermediárias e Terminais do Java Stream
- Operações de Interrompimento do Java Stream
- Exemplos de Java Stream
- Limitações da API de Stream do Java 8
Java Stream
Antes de examinarmos os Exemplos da API de Stream do Java, vamos entender por que ela foi necessária. Suponha que queiramos iterar sobre uma lista de inteiros e descobrir a soma de todos os inteiros maiores que 10. Antes do Java 8, a abordagem para fazer isso seria:
private static int sumIterator(List<Integer> list) {
Iterator<Integer> it = list.iterator();
int sum = 0;
while (it.hasNext()) {
int num = it.next();
if (num > 10) {
sum += num;
}
}
return sum;
}
Há três problemas principais com a abordagem acima:
- Apenas queremos saber a soma dos números inteiros, mas também teríamos que fornecer como a iteração ocorrerá, isso também é chamado de iteração externa porque o programa cliente está manipulando o algoritmo para iterar sobre a lista.
- O programa é sequencial por natureza, não há maneira fácil de fazer isso em paralelo.
- Há muito código mesmo para uma tarefa simples.
Para superar todas as deficiências acima, a API de Stream do Java 8 foi introduzida. Podemos usar a API de Stream do Java para implementar a iteração interna, que é melhor porque o framework Java está no controle da iteração. A iteração interna fornece várias funcionalidades, como execução sequencial e paralela, filtragem com base nos critérios fornecidos, mapeamento etc. A maioria dos argumentos dos métodos da API de Stream do Java 8 são interfaces funcionais, então as expressões lambda funcionam muito bem com elas. Vamos ver como podemos escrever a lógica acima em uma única linha de declaração usando Streams do Java.
private static int sumStream(List<Integer> list) {
return list.stream().filter(i -> i > 10).mapToInt(i -> i).sum();
}
Observe que o programa acima utiliza a estratégia de iteração, filtragem e mapeamento do framework Java e aumentaria a eficiência. Primeiro, vamos analisar os conceitos básicos da API de Stream do Java 8 e depois passaremos por alguns exemplos para entender os métodos mais comumente usados.
Coleções e Java Stream
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. Such as we can create a stream from the list and filter it based on a condition. Java Stream operations use functional interfaces, that makes it a very good fit for functional programming using lambda expression. As you can see in the above example that using lambda expressions make our code readable and short. Java 8 Stream internal iteration principle helps in achieving lazy-seeking in some of the stream operations. For example filtering, mapping, or duplicate removal can be implemented lazily, allowing higher performance and scope for optimization. Java Streams are consumable, so there is no way to create a reference to stream for future usage. Since the data is on-demand, it’s not possible to reuse the same stream multiple times. Java 8 Stream support sequential as well as parallel processing, parallel processing can be very helpful in achieving high performance for large collections. All the Java Stream API interfaces and classes are in the java.util.stream
package. Since we can use primitive data types such as int, long in the collections using auto-boxing and these operations could take a lot of time, there are specific classes for primitive types – IntStream
, LongStream
and DoubleStream
.
Interfaces Funcionais no Stream do Java 8
Alguns das interfaces funcionais comumente utilizadas nos métodos da API Stream do Java 8 são:
- Function e BiFunction: Function representa uma função que recebe um tipo de argumento e retorna outro tipo de argumento.
Function<T, R>
é a forma genérica onde T é o tipo de entrada para a função e R é o tipo do resultado da função. Para lidar com tipos primitivos, existem interfaces Function específicas –ToIntFunction
,ToLongFunction
,ToDoubleFunction
,ToIntBiFunction
,ToLongBiFunction
,ToDoubleBiFunction
,LongToIntFunction
,LongToDoubleFunction
,IntToLongFunction
,IntToDoubleFunction
etc. Alguns dos métodos de Stream ondeFunction
ou suas especializações primitivas são utilizadas são:- <R> Stream<R> map(Function<? super T, ? extends R> mapper)
- IntStream mapToInt(ToIntFunction<? super T> mapper) – similarmente para streams específicas de long e double retornando tipos primitivos.
- IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper) – similarmente para long e double
- <A> A[] toArray(IntFunction<A[]> generator)
- <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
- Predicate e BiPredicate: Representam um predicado contra o qual os elementos do fluxo são testados. Isso é usado para filtrar elementos do fluxo Java. Assim como
Function
, existem interfaces específicas para int, long e double. Alguns dos métodos do Stream nos quais as especializações dePredicate
ouBiPredicate
são usadas incluem:- Stream<T> filter(Predicate<? super T> predicado)
- boolean anyMatch(Predicate<? super T> predicado)
- boolean allMatch(Predicate<? super T> predicado)
- boolean noneMatch(Predicate<? super T> predicado)
- Consumidor e BiConsumidor: Representa uma operação que aceita um único argumento de entrada e não retorna resultado. Pode ser usado para realizar alguma ação em todos os elementos do fluxo Java. Alguns dos métodos do Stream do Java 8 onde são usadas interfaces
Consumidor
,BiConsumidor
ou suas especializações primitivas são:- Stream<T> peek(Consumidor<? super T> ação)
- void forEach(Consumidor<? super T> ação)
- void forEachOrdered(Consumidor<? super T> ação)
- Fornecedor: Fornecedor representa uma operação através da qual podemos gerar novos valores no fluxo. Alguns dos métodos em Stream que aceitam argumento do tipo
Fornecedor
são:- public static<T> Stream<T> generate(Fornecedor<T> s)
- <R> R collect(Fornecedor<R> fornecedor, BiConsumidor<R, ? super T> acumulador, BiConsumidor<R, R> combinador)
java.util.Optional
O Java Optional é um objeto contêiner que pode ou não conter um valor não nulo. Se um valor estiver presente, isPresent()
retornará true e get()
retornará o valor. As operações terminais de Stream retornam um objeto Optional. Alguns desses métodos são:
- Optional<T> reduce(BinaryOperator<T> accumulator)
- Optional<T> min(Comparator<? super T> comparator)
- Optional<T> max(Comparator<? super T> comparator)
- Optional<T> findFirst()
- Optional<T> findAny()
java.util.Spliterator
Para dar suporte à execução paralela na API de Stream do Java 8, a interface Spliterator
é usada. O método trySplit
do Spliterator retorna um novo Spliterator que gerencia um subconjunto dos elementos do Spliterator original.
Operações Intermediárias e Terminais do Java Stream
As operações da API de Java Stream que retornam um novo Stream são chamadas de operações intermediárias. Na maioria das vezes, essas operações são preguiçosas por natureza, começando a produzir novos elementos de stream e enviando-os para a próxima operação. Operações intermediárias nunca são operações finais que produzem resultados. As operações intermediárias comumente utilizadas são filter
e map
. As operações da API de Java 8 Stream que retornam um resultado ou produzem um efeito colateral. Uma vez que o método terminal é chamado em um stream, ele consome o stream e, depois disso, não podemos mais usá-lo. As operações terminais são ávidas por natureza, ou seja, processam todos os elementos no stream antes de retornar o resultado. Métodos terminais comumente utilizados são forEach
, toArray
, min
, max
, findFirst
, anyMatch
, allMatch
, etc. Você pode identificar os métodos terminais pelo tipo de retorno, pois nunca retornarão um Stream.
Operações de Curtocircuito de Java Stream
Uma operação intermediária é chamada de curtocircuito se puder produzir um stream finito para um stream infinito. Por exemplo, limit()
e skip()
são duas operações intermediárias de curtocircuito. Uma operação terminal é chamada de curtocircuito se puder ser encerrada em tempo finito para um stream infinito. Por exemplo, anyMatch
, allMatch
, noneMatch
, findFirst
e findAny
são operações terminais de curtocircuito.
Exemplos de Java Stream
I have covered almost all the important parts of the Java 8 Stream API. It’s exciting to use this new API features and let’s see it in action with some java stream examples.
Criando Java Streams
Há várias maneiras pelas quais podemos criar um fluxo Java a partir de matrizes e coleções. Vamos analisar isso com exemplos simples.
-
Podemos usar
Stream.of()
para criar um fluxo a partir de tipos semelhantes de dados. Por exemplo, podemos criar um Fluxo Java de inteiros a partir de um grupo de objetos int ou Integer.Stream<Integer> stream = Stream.of(1,2,3,4);
-
Pode-se usar
Stream.of()
com uma matriz de Objetos para retornar o fluxo. Note que ele não suporta autoboxing, então não podemos passar uma matriz de tipo primitivo.Stream<Integer> stream = Stream.of(new Integer[]{1,2,3,4}); // funciona bem Stream<Integer> stream1 = Stream.of(new int[]{1,2,3,4}); // Erro de compilação, Incompatibilidade de tipos: não é possível converter de Stream<int[]> para Stream<Integer>
-
Podemos usar Collection
stream()
para criar um fluxo sequencial eparallelStream()
para criar um fluxo paralelo.List<Integer> myList = new ArrayList<>(); for(int i=0; i<100; i++) myList.add(i); //fluxo sequencial Stream<Integer> sequentialStream = myList.stream(); //fluxo paralelo Stream<Integer> parallelStream = myList.parallelStream();
-
Podemos usar os métodos
Stream.generate()
eStream.iterate()
para criar um Stream.Stream<String> stream1 = Stream.generate(() -> {return "abc";}); Stream<String> stream2 = Stream.iterate("abc", (i) -> i);
-
Usando os métodos
Arrays.stream()
eString.chars()
.LongStream is = Arrays.stream(new long[]{1,2,3,4}); IntStream is2 = "abc".chars();
Convertendo Java Stream para Coleção ou Array
Há várias maneiras pelas quais podemos obter uma Coleção ou Array de um Java Stream.
-
Pode-se utilizar o método
collect()
do Java Stream para obter List, Map ou Set a partir do stream.Stream<Integer> intStream = Stream.of(1,2,3,4); List<Integer> intList = intStream.collect(Collectors.toList()); System.out.println(intList); //imprime [1, 2, 3, 4] intStream = Stream.of(1,2,3,4); //o stream está fechado, então precisamos criá-lo novamente Map<Integer,Integer> intMap = intStream.collect(Collectors.toMap(i -> i, i -> i+10)); System.out.println(intMap); //imprime {1=11, 2=12, 3=13, 4=14}
-
Pode-se utilizar o método
toArray()
do stream para criar um array a partir do stream.Stream<Integer> intStream = Stream.of(1,2,3,4); Integer[] intArray = intStream.toArray(Integer[]::new); System.out.println(Arrays.toString(intArray)); //imprime [1, 2, 3, 4]
Operações Intermediárias do Java Stream
Vamos dar uma olhada em exemplos comuns de operações intermediárias do Java Stream.
-
Exemplo de filtro de Stream(): Podemos usar o método filter() para testar elementos de um stream para uma condição e gerar uma lista filtrada.
List<Integer> myList = new ArrayList<>(); for(int i=0; i<100; i++) myList.add(i); Stream<Integer> sequentialStream = myList.stream(); Stream<Integer> highNums = sequentialStream.filter(p -> p > 90); // filtrar números maiores que 90 System.out.print("Números Altos maiores que 90="); highNums.forEach(p -> System.out.print(p+" ")); // imprime "Números Altos maiores que 90=91 92 93 94 95 96 97 98 99 "
-
Exemplo de map() de Stream: Podemos usar map() para aplicar funções a um stream. Vamos ver como podemos usá-lo para aplicar a função de maiúsculas a uma lista de Strings.
Stream<String> nomes = Stream.of("aBc", "d", "ef"); System.out.println(nomes.map(s -> { return s.toUpperCase(); }).collect(Collectors.toList())); // imprime [ABC, D, EF]
-
Exemplo de sorted() em Stream: Podemos usar sorted() para ordenar os elementos da stream passando um argumento Comparator.
Stream<String> names2 = Stream.of("aBc", "d", "ef", "123456"); List<String> reverseSorted = names2.sorted(Comparator.reverseOrder()).collect(Collectors.toList()); System.out.println(reverseSorted); // [ef, d, aBc, 123456] Stream<String> names3 = Stream.of("aBc", "d", "ef", "123456"); List<String> naturalSorted = names3.sorted().collect(Collectors.toList()); System.out.println(naturalSorted); //[123456, aBc, d, ef]
-
Exemplo de flatMap() em Stream: Podemos usar flatMap() para criar uma stream a partir da stream de listas. Vamos ver um exemplo simples para esclarecer essa dúvida.
Stream<List<String>> namesOriginalList = Stream.of( Arrays.asList("Pankaj"), Arrays.asList("David", "Lisa"), Arrays.asList("Amit")); // Aplaina a stream de List<String> para String stream Stream<String> flatStream = namesOriginalList .flatMap(strList -> strList.stream()); flatStream.forEach(System.out::println);
Operações Terminais de Fluxo Java
Vamos dar uma olhada em alguns exemplos de operações terminais de fluxo em Java.
-
Exemplo de reduce() de Fluxo: Podemos usar reduce() para realizar uma redução nos elementos do fluxo, utilizando uma função de acumulação associativa, e retornar um Optional. Vamos ver como podemos usá-lo para multiplicar os inteiros em um fluxo.
Stream<Integer> numeros = Stream.of(1,2,3,4,5); Optional<Integer> intOptional = numeros.reduce((i,j) -> {return i*j;}); if(intOptional.isPresent()) System.out.println("Multiplicação = "+intOptional.get()); //120
-
Exemplo de count() de Fluxo: Podemos usar esta operação terminal para contar o número de itens no fluxo.
Stream<Integer> numeros1 = Stream.of(1,2,3,4,5); System.out.println("Número de elementos no fluxo="+numeros1.count()); //5
-
Exemplo de forEach() de Stream: Isso pode ser usado para iterar sobre o stream. Podemos usar isso no lugar do iterador. Vamos ver como usá-lo para imprimir todos os elementos do stream.
Stream<Integer> numeros2 = Stream.of(1,2,3,4,5); numeros2.forEach(i -> System.out.print(i+",")); //1,2,3,4,5,
-
Exemplos de match() de Stream: Vamos ver alguns exemplos dos métodos de correspondência na API de Stream.
Stream<Integer> numeros3 = Stream.of(1,2,3,4,5); System.out.println("O stream contém 4? "+numeros3.anyMatch(i -> i==4)); //O stream contém 4? true Stream<Integer> numeros4 = Stream.of(1,2,3,4,5); System.out.println("O stream contém todos os elementos menores que 10? "+numeros4.allMatch(i -> i<10)); //O stream contém todos os elementos menores que 10? true Stream<Integer> numeros5 = Stream.of(1,2,3,4,5); System.out.println("O stream não contém 10? "+numeros5.noneMatch(i -> i==10)); //O stream não contém 10? true
-
Exemplo de findFirst() do Stream: Esta é uma operação terminal de interrupção rápida, vejamos como podemos usá-la para encontrar a primeira string de um fluxo que começa com D.
Stream<String> nomes4 = Stream.of("Pankaj","Amit","David", "Lisa"); Optional<String> primeiroNomeComD = nomes4.filter(i -> i.startsWith("D")).findFirst(); if(primeiroNomeComD.isPresent()){ System.out.println("Primeiro nome começando com D="+primeiroNomeComD.get()); //David }
Limitações da API Stream do Java 8
A API Stream do Java 8 traz muitas coisas novas para trabalhar com listas e arrays, mas também possui algumas limitações.
-
Expressões lambda sem estado: Se estiver utilizando fluxo paralelo e as expressões lambda forem estado-dependentes, isso pode resultar em respostas aleatórias. Vamos ver isso com um programa simples.
StatefulParallelStream.java
package com.journaldev.java8.stream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Stream; public class StatefulParallelStream { public static void main(String[] args) { List<Integer> ss = Arrays.asList(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15); List<Integer> result = new ArrayList<Integer>(); Stream<Integer> stream = ss.parallelStream(); stream.map(s -> { synchronized (result) { if (result.size() < 10) { result.add(s); } } return s; }).forEach( e -> {}); System.out.println(result); } }
Se executarmos o programa acima, obteremos resultados diferentes porque isso depende da maneira como o fluxo está sendo iterado e não temos nenhuma ordem definida para o processamento paralelo. Se utilizarmos um fluxo sequencial, então esse problema não surgirá.
-
Uma vez que um Stream é consumido, não pode ser utilizado posteriormente. Como você pode ver nos exemplos acima, toda vez estou criando um stream.
-
Há muitos métodos na API Stream e a parte mais confusa são os métodos sobrecarregados. Isso torna a curva de aprendizado demorada.
Isso é tudo para o tutorial de exemplo do Java 8 Stream. Estou ansioso para usar esse recurso e tornar o código legível com melhor desempenho por meio do processamento paralelo. Referência: Documentação da API Java Stream
Source:
https://www.digitalocean.com/community/tutorials/java-8-stream