Bem-vindo ao tutorial da API de Stream do Java 8. 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 explorar 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 Fluxo Java
- Operações de Interrupção do Fluxo Java
- Exemplos de Fluxo Java
- Limitações da API de Fluxo Java 8
Fluxo Java
Antes de examinarmos os Exemplos da API de Fluxo Java, vamos entender por que ela foi necessária. Suponha que desejamos 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:
- Nós só queremos saber a soma dos 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á lidando com o algoritmo para iterar sobre a lista.
- O programa é sequencial por natureza, não há uma maneira fácil de fazer isso em paralelo.
- Há muito código para fazer até 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 expressões lambda funcionam muito bem com eles. Vamos ver como podemos escrever a lógica acima em uma declaração de uma única linha usando Java Streams.
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 examinar 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 Java 8 Stream
Alguns das interfaces funcionais comumente usadas nos métodos da API de 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 usadas são:- <R> Stream<R> map(Function<? super T, ? extends R> mapper)
- IntStream mapToInt(ToIntFunction<? super T> mapper) – similarmente para long e double retornando stream específico de 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 onde são usadas especializações dePredicate
ouBiPredicate
são:- Stream<T> filter(Predicate<? super T> predicate)
- boolean anyMatch(Predicate<? super T> predicate)
- boolean allMatch(Predicate<? super T> predicate)
- boolean noneMatch(Predicate<? super T> predicate)
- 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 de Stream do Java 8 onde
Consumidor
,BiConsumidor
ou suas interfaces de especialização primitivas são usadas são:- Stream<T> espiar(Consumidor<? super T> ação)
- void forEach(Consumidor<? super T> ação)
- void forEachOrdenado(Consumidor<? super T> ação)
- Fornecedor: O Fornecedor representa uma operação através da qual podemos gerar novos valores no fluxo. Alguns dos métodos em Stream que recebem argumento
Fornecedor
são:- public static<T> Stream<T> gerar(Fornecedor<T> s)
- <R> R coletar(Fornecedor<R> fornecedor, BiConsumidor<R, ? super T> acumulador, BiConsumidor<R, R> combinador)
java.util.Opcional
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á verdadeiro e get()
retornará o valor. As operações terminais do 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 suportar a execução paralela na API Stream do Java 8, a interface Spliterator
é usada. O método trySplit
de Spliterator retorna um novo Spliterator que gerencia um subconjunto dos elementos do Spliterator original.
Operações Intermediárias e Terminais do Java Stream
Java Stream API operações 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 usadas são filter
e map
. As operações da API Stream do Java 8 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, após isso, não podemos mais usar o stream. Operações terminais são ágeis por natureza, ou seja, processam todos os elementos no stream antes de retornar o resultado. Métodos terminais comumente usados são forEach
, toArray
, min
, max
, findFirst
, anyMatch
, allMatch
, etc. Você pode identificar métodos terminais pelo tipo de retorno, eles nunca retornarão um Stream.
Operações de Curto-Circuito do Java Stream
Uma operação intermediária é chamada de curto-circuito se puder produzir um stream finito para um stream infinito. Por exemplo, limit()
e skip()
são duas operações intermediárias de curto-circuito. Uma operação terminal é chamada de curto-circuito 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 curto-circuito.
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 stream java a partir de arrays e coleções. Vamos analisar essas com exemplos simples.
-
Podemos usar
Stream.of()
para criar um stream a partir de um tipo semelhante de dados. Por exemplo, podemos criar um Java Stream de inteiros a partir de um grupo de int ou objetos Integer.Stream<Integer> stream = Stream.of(1,2,3,4);
-
Podemos usar
Stream.of()
com uma matriz de Objetos para retornar o stream. Note que ele não suporta autoboxing, então não podemos passar um array 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, Tipo incompatível: 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 Fluxo.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 um Fluxo Java para Coleção ou Array
Há várias maneiras pelas quais podemos obter uma Coleção ou Array de um fluxo Java.
-
Podemos usar o método
collect()
do Java Stream para obter uma Lista, Mapa ou Conjunto a partir do fluxo.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 fluxo 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}
-
Podemos usar o método
toArray()
do stream para criar um array a partir do fluxo.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 analisar um exemplo de operações intermediárias comumente usadas no Java Stream.
-
Exemplo de uso do filter() em Stream: Podemos usar o método filter() para testar elementos de um stream por uma condição e gerar uma lista filtrada.
List<Integer> minhaLista = new ArrayList<>(); for(int i=0; i<100; i++) minhaLista.add(i); Stream<Integer> streamSequencial = minhaLista.stream(); Stream<Integer> numerosAltos = streamSequencial.filter(p -> p > 90); //filtra números maiores que 90 System.out.print("Números altos maiores que 90="); numerosAltos.forEach(p -> System.out.print(p+" ")); //imprime "Números altos maiores que 90=91 92 93 94 95 96 97 98 99 "
-
Exemplo de uso do map() em Stream: Podemos usar map() para aplicar funções em um stream. Vejamos como podemos usá-lo para aplicar a função de converter para maiúsculas em 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() Stream: Podemos usar sorted() para ordenar os elementos da stream passando o 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() 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")); //flat the stream from List<String> to String stream Stream<String> flatStream = namesOriginalList .flatMap(strList -> strList.stream()); flatStream.forEach(System.out::println);
Operações Terminais do Java Stream
Vamos dar uma olhada em alguns exemplos de operações terminais do java stream.
-
Exemplo de reduce() do Stream: Podemos usar o reduce() para realizar uma redução nos elementos do stream, usando uma função de acumulação associativa, e retornar um Optional. Vamos ver como podemos usá-lo para multiplicar os inteiros em um stream.
Stream<Integer> numbers = Stream.of(1,2,3,4,5); Optional<Integer> intOptional = numbers.reduce((i,j) -> {return i*j;}); if(intOptional.isPresent()) System.out.println("Multiplicação = "+intOptional.get()); //120
-
Exemplo de count() do Stream: Podemos usar esta operação terminal para contar o número de itens no stream.
Stream<Integer> numbers1 = Stream.of(1,2,3,4,5); System.out.println("Número de elementos no stream="+numbers1.count()); //5
-
Exemplo de forEach() em Stream: Isso pode ser usado para iterar sobre o fluxo. Podemos usar isso em vez do iterador. Vamos ver como usá-lo para imprimir todos os elementos do fluxo.
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() em Stream: Vamos ver alguns exemplos de métodos de correspondência na API de Stream.
Stream<Integer> numeros3 = Stream.of(1,2,3,4,5); System.out.println("O fluxo contém 4? "+numeros3.anyMatch(i -> i==4)); //O fluxo contém 4? true Stream<Integer> numeros4 = Stream.of(1,2,3,4,5); System.out.println("O fluxo contém todos os elementos menores que 10? "+numeros4.allMatch(i -> i<10)); //O fluxo contém todos os elementos menores que 10? true Stream<Integer> numeros5 = Stream.of(1,2,3,4,5); System.out.println("O fluxo não contém 10? "+numeros5.noneMatch(i -> i==10)); //O fluxo não contém 10? true
-
Exemplo de findFirst() em Stream: Esta é uma operação terminal de circuito curto, vamos ver como podemos usá-la para encontrar a primeira string de uma stream começando com D.
Stream<String> names4 = Stream.of("Pankaj","Amit","David", "Lisa"); Optional<String> firstNameWithD = names4.filter(i -> i.startsWith("D")).findFirst(); if(firstNameWithD.isPresent()){ System.out.println("Primeiro nome começando com D="+firstNameWithD.get()); //David }
Limitações da API de Stream do Java 8
A API de Stream do Java 8 traz muitas novidades para trabalhar com listas e arrays, mas também possui algumas limitações.
-
Expressões lambda sem estado: Se você estiver usando um fluxo paralelo e as expressões lambda tiverem estado, 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
ss = Arrays.asList(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15); List result = new ArrayList (); Stream 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, você obterá resultados diferentes porque depende da maneira como o fluxo está sendo iterado e não temos nenhuma ordem definida para o processamento paralelo. Se usarmos um fluxo sequencial, então esse problema não surgirá.
-
Uma vez que um Stream é consumido, ele não pode ser usado posteriormente. Como você pode ver nos exemplos acima, toda vez que 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 Stream do Java
Source:
https://www.digitalocean.com/community/tutorials/java-8-stream