O tutorial da API do Java 8 Stream

O tutorial da API do Java 8 Stream

1. Visão geral

Neste tutorial detalhado, vamos percorrer o uso prático do Java 8 Streams, desde a criação até a execução paralela.

Para entender este material, os leitores precisam ter um conhecimento básico de Java 8 (expressões lambda, referências de métodoOptional,) e da API de fluxo. Se você não estiver familiarizado com esses tópicos, dê uma olhada em nossos artigos anteriores -New Features in Java 8eIntroduction to Java 8 Streams.

Leitura adicional:

Expressões Lambda e interfaces funcionais: dicas e práticas recomendadas

Dicas e práticas recomendadas para usar o Java 8 lambdas e interfaces funcionais.

Read more

Guia para coletores do Java 8

O artigo discute o Java 8 Collectors, mostrando exemplos de coletores internos, além de mostrar como criar coletores personalizados.

Read more

2. Criação de fluxo

Existem várias maneiras de criar uma instância de fluxo de diferentes fontes. Uma vez criada, a instânciawill not modify its source,, portanto, permitindo a criação de várias instâncias a partir de uma única fonte.

2.1. Stream Vazio

O métodoempty() deve ser usado no caso de criação de um fluxo vazio:

Stream streamEmpty = Stream.empty();

Frequentemente, o métodoempty() é usado na criação para evitar o retorno denull para fluxos sem elemento:

public Stream streamOf(List list) {
    return list == null || list.isEmpty() ? Stream.empty() : list.stream();
}

2.2. Fluxo deCollection

O fluxo também pode ser criado de qualquer tipo deCollection (Collection, List, Set):

Collection collection = Arrays.asList("a", "b", "c");
Stream streamOfCollection = collection.stream();

2.3. Fluxo de Matriz

A matriz também pode ser uma fonte de um fluxo:

Stream streamOfArray = Stream.of("a", "b", "c");

Eles também podem ser criados a partir de uma matriz existente ou de uma parte de uma matriz:

String[] arr = new String[]{"a", "b", "c"};
Stream streamOfArrayFull = Arrays.stream(arr);
Stream streamOfArrayPart = Arrays.stream(arr, 1, 3);

2.4. Stream.builder()

When builder is usedthe desired type should be additionally specified in the right part of the statement,, caso contrário, o métodobuild() criará uma instância doStream<Object>:

Stream streamBuilder =
  Stream.builder().add("a").add("b").add("c").build();

2.5. Stream.generate()

O métodogenerate() aceita umSupplier<T> para geração de elemento. Como o fluxo resultante é infinito, o desenvolvedor deve especificar o tamanho desejado ou o métodogenerate() funcionará até atingir o limite de memória:

Stream streamGenerated =
  Stream.generate(() -> "element").limit(10);

O código acima cria uma sequência de dez strings com o valor -“element”.

2.6. Stream.iterate()

Outra maneira de criar um fluxo infinito é usando o métodoiterate():

Stream streamIterated = Stream.iterate(40, n -> n + 2).limit(20);

O primeiro elemento do fluxo resultante é um primeiro parâmetro do métodoiterate(). Para criar todos os elementos a seguir, a função especificada é aplicada ao elemento anterior. No exemplo acima, o segundo elemento será 42.

2.7. Fluxo de primitivos

Java 8 oferece a possibilidade de criar fluxos de três tipos primitivos:int, longedouble. ComoStream<T> é uma interface genérica e não há como usar primitivos como um parâmetro de tipo com genéricos, três novas interfaces especiais foram criadas:IntStream, LongStream, DoubleStream.

O uso das novas interfaces alivia o auto-box desnecessário, permitindo maior produtividade:

IntStream intStream = IntStream.range(1, 3);
LongStream longStream = LongStream.rangeClosed(1, 3);

O métodorange(int startInclusive, int endExclusive) cria um fluxo ordenado do primeiro parâmetro para o segundo parâmetro. Ele incrementa o valor dos elementos subseqüentes com a etapa igual a 1. O resultado não inclui o último parâmetro, é apenas um limite superior da sequência.

O métodorangeClosed(int startInclusive, int endInclusive) faz o mesmo com apenas uma diferença - o segundo elemento é incluído. Esses dois métodos podem ser usados ​​para gerar qualquer um dos três tipos de fluxos de primitivos.

Desde Java 8, a classeRandom fornece uma ampla variedade de métodos para geração de fluxos de primitivos. Por exemplo, o código a seguir cria umDoubleStream, que possui três elementos:

Random random = new Random();
DoubleStream doubleStream = random.doubles(3);

2.8. Fluxo deString

String também pode ser usado como uma fonte para a criação de um fluxo.

Com a ajuda do métodochars() da classeString. Como não há interfaceCharStream no JDK, oIntStream é usado para representar um fluxo de caracteres.

IntStream streamOfChars = "abc".chars();

O exemplo a seguir divide aString em subcadeias de acordo comRegEx especificado:

Stream streamOfString =
  Pattern.compile(", ").splitAsStream("a, b, c");

2.9. Fluxo de arquivo

A classe Java NIOFiles permite gerar umStream<String> de um arquivo de texto por meio do métodolines(). Cada linha do texto se torna um elemento do fluxo:

Path path = Paths.get("C:\\file.txt");
Stream streamOfStrings = Files.lines(path);
Stream streamWithCharset =
  Files.lines(path, Charset.forName("UTF-8"));

OCharset pode ser especificado como um argumento do métodolines().

3. Referenciandoa Stream

É possível instanciar um fluxo e ter uma referência acessível a ele, desde que apenas operações intermediárias tenham sido chamadas. A execução de uma operação de terminal torna um fluxo inacessível.

Para demonstrar isso, esqueceremos por um tempo que a melhor prática é encadear a sequência de operações. Além de sua verbosidade desnecessária, tecnicamente o código a seguir é válido:

Stream stream =
  Stream.of("a", "b", "c").filter(element -> element.contains("b"));
Optional anyElement = stream.findAny();

Mas uma tentativa de reutilizar a mesma referência após chamar a operação de terminal irá acionar oIllegalStateException:

Optional firstElement = stream.findFirst();

Como oIllegalStateException é umRuntimeException, um compilador não sinalizará sobre um problema. Portanto, é muito importante lembrar queJava 8streams can’t be reused.

Esse tipo de comportamento é lógico porque os fluxos foram projetados para fornecer a capacidade de aplicar uma sequência finita de operações à origem dos elementos em um estilo funcional, mas não para armazenar elementos.

Portanto, para que o código anterior funcione corretamente, algumas alterações devem ser feitas:

List elements =
  Stream.of("a", "b", "c").filter(element -> element.contains("b"))
    .collect(Collectors.toList());
Optional anyElement = elements.stream().findAny();
Optional firstElement = elements.stream().findFirst();

4. Stream Pipeline

Para realizar uma sequência de operações sobre os elementos da fonte de dados e agregar seus resultados, três partes são necessárias - osource,intermediate operation(s)e umterminal operation.

Operações intermediárias retornam um novo fluxo modificado. Por exemplo, para criar um novo fluxo do existente sem poucos elementos, o métodoskip() deve ser usado:

Stream onceModifiedStream =
  Stream.of("abcd", "bbcd", "cbcd").skip(1);

Se mais de uma modificação for necessária, operações intermediárias poderão ser encadeadas. Suponha que também precisamos substituir cada elemento doStream<String> atual por uma subcadeia dos primeiros caracteres. Isso será feito encadeando os métodosskip()emap():

Stream twiceModifiedStream =
  stream.skip(1).map(element -> element.substring(0, 3));

Como você pode ver, o métodomap() usa uma expressão lambda como parâmetro. Se você quiser aprender mais sobre lambdas, dê uma olhada em nosso tutorialLambda Expressions and Functional Interfaces: Tips and Best Practices.

Um fluxo por si só é inútil, o que realmente interessa ao usuário é o resultado da operação do terminal, que pode ser um valor de algum tipo ou uma ação aplicada a todos os elementos do fluxo. Only one terminal operation can be used per stream.

A maneira certa e mais conveniente de usar streams é porstream pipeline, which is a chain of stream source, intermediate operations, and a terminal operation.. Por exemplo:

List list = Arrays.asList("abc1", "abc2", "abc3");
long size = list.stream().skip(1)
  .map(element -> element.substring(0, 3)).sorted().count();

5. Invocação Preguiçosa

Intermediate operations are lazy. Isso significa quethey will be invoked only if it is necessary for the terminal operation execution.

Para demonstrar isso, imagine que temos o métodowasCalled(), que incrementa um contador interno toda vez que é chamado:

private long counter;

private void wasCalled() {
    counter++;
}

Vamos chamar o método eraCalled() da operaçãofilter():

List list = Arrays.asList(“abc1”, “abc2”, “abc3”);
counter = 0;
Stream stream = list.stream().filter(element -> {
    wasCalled();
    return element.contains("2");
});

Como temos uma fonte de três elementos, podemos assumir que o métodofilter() será chamado três vezes e o valor da variávelcounter será 3. Mas a execução deste código não mudacounter, ele ainda é zero, então, o métodofilter() não foi chamado nem uma vez. A razão pela qual - está faltando na operação do terminal.

Vamos reescrever este código um pouco adicionando uma operaçãomap() e uma operação de terminal -findFirst().. Também adicionaremos a capacidade de rastrear uma ordem de chamadas de método com a ajuda de registro:

Optional stream = list.stream().filter(element -> {
    log.info("filter() was called");
    return element.contains("2");
}).map(element -> {
    log.info("map() was called");
    return element.toUpperCase();
}).findFirst();

O log resultante mostra que o métodofilter() foi chamado duas vezes e o métodomap() apenas uma vez. Isso ocorre porque o pipeline é executado verticalmente. Em nosso exemplo, o primeiro elemento do fluxo não satisfez o predicado do filtro, então o métodofilter() foi invocado para o segundo elemento, que passou no filtro. Sem chamarfilter() para o terceiro elemento, descemos pelo pipeline para o métodomap().

A operaçãofindFirst() é satisfeita por apenas um elemento. Portanto, neste exemplo específico, a invocação preguiçosa permitiu evitar duas chamadas de método - uma parafilter()e outra paramap().

6. Ordem de Execução

Do ponto de vista do desempenho,the right order is one of the most important aspects of chaining operations in the stream pipeline:

long size = list.stream().map(element -> {
    wasCalled();
    return element.substring(0, 3);
}).skip(2).count();

A execução deste código aumentará o valor do contador em três. Isso significa que o métodomap() do stream foi chamado três vezes. Mas o valor desize é um. Portanto, o fluxo resultante tem apenas um elemento e executamos as caras operaçõesmap() sem motivo duas vezes em três.

Se mudarmos a ordem dos métodosskip()emap(),, ocounter aumentará apenas em um. Portanto, o métodomap() será chamado apenas uma vez:

long size = list.stream().skip(2).map(element -> {
    wasCalled();
    return element.substring(0, 3);
}).count();

Isso nos leva à regra:intermediate operations which reduce the size of the stream should be placed before operations which are applying to each element. Portanto, mantenha métodos como skip(), filter(), distinct() no topo do pipeline de fluxo.

7. Redução de fluxo

A API possui muitas operações de terminal que agregam um fluxo a um tipo ou a uma primitiva, por exemplo,count(), max(), min(), sum(),, mas essas operações funcionam de acordo com a implementação predefinida. E o queif a developer needs to customize a Stream’s reduction mechanism? Existem dois métodos que permitem fazer isso - os métodosreduce()ecollect().

7.1. O Métodoreduce()

Existem três variações desse método, que diferem por suas assinaturas e tipos de retorno. Eles podem ter os seguintes parâmetros:

identity – o valor inicial para um acumulador ou um valor padrão se um fluxo estiver vazio e não houver nada para acumular;

accumulator – uma função que especifica uma lógica de agregação de elementos. Como o acumulador cria um novo valor para cada etapa de redução, a quantidade de novos valores é igual ao tamanho do fluxo e apenas o último valor é útil. Isso não é muito bom para o desempenho.

combiner – uma função que agrega resultados do acumulador. O combinador é chamado apenas em modo paralelo para reduzir os resultados de acumuladores de diferentes segmentos.

Então, vamos ver esses três métodos em ação:

OptionalInt reduced =
  IntStream.range(1, 4).reduce((a, b) -> a + b);

reduced = 6 (1 + 2 + 3)

int reducedTwoParams =
  IntStream.range(1, 4).reduce(10, (a, b) -> a + b);

reducedTwoParams = 16 (10 + 1 + 2 + 3)

int reducedParams = Stream.of(1, 2, 3)
  .reduce(10, (a, b) -> a + b, (a, b) -> {
     log.info("combiner was called");
     return a + b;
  });

O resultado será o mesmo do exemplo anterior (16) e não haverá login, o que significa que o combinador não foi chamado. Para fazer um combinador funcionar, um fluxo deve ser paralelo:

int reducedParallel = Arrays.asList(1, 2, 3).parallelStream()
    .reduce(10, (a, b) -> a + b, (a, b) -> {
       log.info("combiner was called");
       return a + b;
    });

O resultado aqui é diferente (36) e o combinador foi chamado duas vezes. Aqui, a redução funciona pelo seguinte algoritmo: o acumulador rodou três vezes adicionando todos os elementos do fluxo aidentity para cada elemento do fluxo. Essas ações estão sendo realizadas em paralelo. Como resultado, eles têm (10 + 1 = 11; 10 + 2 = 12; 10 + 3 = 13;). Agora o combinador pode mesclar esses três resultados. Para isso, são necessárias duas iterações (12 + 13 = 25; 25 + 11 = 36).

7.2. O Métodocollect()

A redução de um fluxo também pode ser executada por outra operação de terminal - o métodocollect(). Ele aceita um argumento do tipoCollector, que especifica o mecanismo de redução. Já existem coletores predefinidos criados para as operações mais comuns. Eles podem ser acessados ​​com a ajuda do tipoCollectors.

Nesta seção, usaremos o seguinteList como fonte para todos os fluxos:

List productList = Arrays.asList(new Product(23, "potatoes"),
  new Product(14, "orange"), new Product(13, "lemon"),
  new Product(23, "bread"), new Product(13, "sugar"));

Convertendo um stream emCollection (Collection, List ouSet):

List collectorCollection =
  productList.stream().map(Product::getName).collect(Collectors.toList());

Reduzindo paraString:

String listToString = productList.stream().map(Product::getName)
  .collect(Collectors.joining(", ", "[", "]"));

O métodojoiner() pode ter de um a três parâmetros (delimitador, prefixo, sufixo). A coisa mais útil sobre o uso dejoiner() - o desenvolvedor não precisa verificar se o fluxo chega ao fim para aplicar o sufixo e não para aplicar um delimitador. Collector cuidará disso.

Processando o valor médio de todos os elementos numéricos do fluxo:

double averagePrice = productList.stream()
  .collect(Collectors.averagingInt(Product::getPrice));

Processando a soma de todos os elementos numéricos do fluxo:

int summingPrice = productList.stream()
  .collect(Collectors.summingInt(Product::getPrice));

Os métodosaveragingXX(), summingXX()esummarizingXX() podem funcionar tanto com primitivas (int, long, double) como com suas classes de wrapper (Integer, Long, Double). Um recurso mais poderoso desses métodos é fornecer o mapeamento. Portanto, o desenvolvedor não precisa usar uma operaçãomap() adicional antes do métodocollect().

Coleta de informações estatísticas sobre os elementos do fluxo:

IntSummaryStatistics statistics = productList.stream()
  .collect(Collectors.summarizingInt(Product::getPrice));

Usando a instância resultante do tipoIntSummaryStatistics, o desenvolvedor pode criar um relatório estatístico aplicando o métodotoString(). O resultado será umString comum a este“IntSummaryStatistics\{count=5, sum=86, min=13, average=17,200000, max=23}”.

Também é fácil extrair desse objeto valores separados paracount, sum, min, average aplicando os métodosgetCount(), getSum(), getMin(), getAverage(), getMax(). Todos esses valores podem ser extraídos de um único pipeline.

Agrupamento de elementos do stream de acordo com a função especificada:

Map> collectorMapOfLists = productList.stream()
  .collect(Collectors.groupingBy(Product::getPrice));

No exemplo acima, o fluxo foi reduzido paraMap, que agrupa todos os produtos por seus preços.

Dividindo os elementos do stream em grupos de acordo com algum predicado:

Map> mapPartioned = productList.stream()
  .collect(Collectors.partitioningBy(element -> element.getPrice() > 15));

Empurrando o coletor para realizar uma transformação adicional:

Set unmodifiableSet = productList.stream()
  .collect(Collectors.collectingAndThen(Collectors.toSet(),
  Collections::unmodifiableSet));

Nesse caso específico, o coletor converteu um fluxo emSete criou oSet não modificável dele.

Coletor personalizado:

Se por algum motivo, um coletor personalizado deve ser criado, a maneira mais fácil e menos prolixo de fazer isso - é usar o métodoof() do tipoCollector.

Collector> toLinkedList =
  Collector.of(LinkedList::new, LinkedList::add,
    (first, second) -> {
       first.addAll(second);
       return first;
    });

LinkedList linkedListOfPersons =
  productList.stream().collect(toLinkedList);

Neste exemplo, uma instância deCollector foi reduzida paraLinkedList<Persone>.

Fluxos Paralelos

Antes do Java 8, a paralelização era complexa. O surgimento deExecutorServiceeForkJoin simplificou um pouco a vida do desenvolvedor, mas eles ainda devem ter em mente como criar um executor específico, como executá-lo e assim por diante. O Java 8 introduziu uma maneira de realizar o paralelismo em um estilo funcional.

A API permite criar fluxos paralelos, que executam operações em modo paralelo. Quando a fonte de um fluxo é umCollection ou umarray, isso pode ser alcançado com a ajuda do métodoparallelStream():

Stream streamOfCollection = productList.parallelStream();
boolean isParallel = streamOfCollection.isParallel();
boolean bigPrice = streamOfCollection
  .map(product -> product.getPrice() * 12)
  .anyMatch(price -> price > 200);

Se a fonte do fluxo for algo diferente deCollection ouarray, o métodoparallel() deve ser usado:

IntStream intStreamParallel = IntStream.range(1, 150).parallel();
boolean isParallel = intStreamParallel.isParallel();

Nos bastidores, a API Stream usa automaticamente a estruturaForkJoin para executar operações em paralelo. Por padrão, o pool de threads comuns será usado e não há como (pelo menos por enquanto) atribuir algum pool de threads personalizado a ele. This can be overcome by using a custom set of parallel collectors.

Ao usar fluxos no modo paralelo, evite operações de bloqueio e use o modo paralelo quando as tarefas precisarem de um período de tempo semelhante para serem executadas (se uma tarefa durar muito mais que a outra, poderá diminuir o fluxo de trabalho do aplicativo).

O fluxo em modo paralelo pode ser convertido de volta para o modo sequencial usando o métodosequential():

IntStream intStreamSequential = intStreamParallel.sequential();
boolean isParallel = intStreamSequential.isParallel();

Conclusões

A API Stream é um conjunto de ferramentas poderoso, mas simples de entender, para processar a sequência de elementos. Isso nos permite reduzir uma quantidade enorme de código padrão, criar programas mais legíveis e melhorar a produtividade do aplicativo quando usado corretamente.

Na maioria dos exemplos de código mostrados neste artigo, os fluxos não foram consumidos (não aplicamos o métodoclose() ou uma operação de terminal). Em um aplicativo real,don’t leave an instantiated streams unconsumed as that will lead to memory leaks.

Os exemplos de código completos que acompanham o artigo estão disponíveisover on GitHub.