Java 8 Stream – Fluxo Java 8

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

  1. Java 8 Stream
  2. Coleções e Java Stream
  3. Interfaces Funcionais no Java 8 Stream
    1. Função e BiFunção
    2. Predicado e BiPredicado
    3. Consumidor e BiConsumidor
    4. Fornecedor
  4. java.util.Optional
  5. java.util.Spliterator
  6. Operações Intermediárias e Terminais do Fluxo Java
  7. Operações de Interrupção do Fluxo Java
  8. Exemplos de Fluxo Java
    1. Criando Fluxos Java
    2. Convertendo Fluxo Java para Coleção ou Array
    3. Operações Intermediárias do Fluxo Java
    4. Operações Terminais do Fluxo Java
  9. 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:

  1. 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.
  2. O programa é sequencial por natureza, não há uma maneira fácil de fazer isso em paralelo.
  3. 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:

  1. 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 onde Function 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)
  2. 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 de Predicate ou BiPredicate 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)
  3. 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)
  4. 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.

  1. 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);
    
  2. 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>
    
  3. Podemos usar Collection stream() para criar um fluxo sequencial e parallelStream() 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();
    
  4. Podemos usar os métodos Stream.generate() e Stream.iterate() para criar um Fluxo.

    Stream<String> stream1 = Stream.generate(() -> {return "abc";});
    Stream<String> stream2 = Stream.iterate("abc", (i) -> i);
    
  5. Usando os métodos Arrays.stream() e String.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.

  1. 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}
    
  2. 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.

  1. 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 "
    
  2. 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]
    
  3. 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]
    
  4. 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.

  1. 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
    
  2. 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
    
  3. 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,
    
  4. 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
    
  5. 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.

  1. 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á.

  2. 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.

  3. 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