Tradução de Benjamin API Winterberg Stream API Guide

Olá Habr! Apresento a você a tradução do artigo " Tutorial do Java 8 Stream ".

Este tutorial, baseado em exemplos de código, fornece uma visão geral abrangente dos fluxos no Java 8. Quando introduzi a API do Stream pela primeira vez, fiquei intrigado com o nome porque é muito consoante com o InputStream e OutputStream do pacote java.io; No entanto, os encadeamentos no Java 8 são algo completamente diferente. Threads são mônadas que desempenham um papel importante no desenvolvimento de programação funcional em Java.
Na programação funcional, uma mônada é uma estrutura que representa um cálculo na forma de uma cadeia de etapas sucessivas. O tipo e a estrutura da mônada determinam a cadeia de operações, no nosso caso, uma sequência de métodos com funções internas de um determinado tipo.
Este tutorial ensinará como trabalhar com fluxos e mostrará como lidar com os vários métodos disponíveis na API de fluxo. Analisaremos a ordem das operações e veremos como a sequência de métodos na cadeia afeta o desempenho. Conheça flatMap métodos avançados da API de fluxo, como reduce , collect e flatMap . No final do manual, prestaremos atenção ao trabalho paralelo com fluxos.

Se você não se sentir à vontade para trabalhar com expressões lambda, interfaces funcionais e métodos de referência, será útil que você se familiarize com meu guia de inovações em Java 8 ( tradução em Habré) e depois retorne ao estudo de fluxos.

Como os threads funcionam


Um fluxo representa uma sequência de elementos e fornece vários métodos para executar cálculos nesses elementos:

 List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1"); myList .stream() .filter(s -> s.startsWith("c")) .map(String::toUpperCase) .sorted() .forEach(System.out::println); // C1 // C2 

Os métodos de fluxo são intermediários (intermediários) e terminais (terminais). Métodos intermediários retornam um fluxo, o que permite que muitos desses métodos sejam chamados seqüencialmente. Os métodos de terminal não retornam um valor (nulo) ou retornam um resultado de um tipo diferente de um fluxo. No exemplo acima, os sorted filter , map e sorted são intermediários e forEach são terminais. Para obter uma lista completa dos métodos de fluxo disponíveis, consulte a documentação . Essa cadeia de operações de fluxo também é conhecida como um pipeline de operação.

A maioria dos métodos da API de fluxo aceita como parâmetros expressões lambda, uma interface funcional que descreve o comportamento específico do método. A maioria deles deve ser simultaneamente não interferente e apátrida. O que isso significa?

Um método não interfere se não modificar os dados subjacentes subjacentes ao fluxo. Por exemplo, no exemplo acima, nenhuma expressão lambda modifica a matriz da lista myList.

Um método é sem estado se a ordem em que a operação é executada for especificada. Por exemplo, nem uma única expressão lambda do exemplo depende de variáveis ​​mutáveis ​​ou estados de espaço externo que podem mudar no tempo de execução.

Diferentes tipos de threads


Os fluxos podem ser criados a partir de vários dados de origem, principalmente de coleções. Listas e conjuntos suportam os novos métodos stream() e parllelStream() para criar fluxos sequenciais e paralelos. Threads paralelos são capazes de trabalhar no modo multithread (em vários threads) e serão discutidos no final do manual. Enquanto isso, considere threads sequenciais:

 Arrays.asList("a1", "a2", "a3") .stream() .findFirst() .ifPresent(System.out::println); // a1 

Aqui, chamar o método stream() em uma lista retorna um objeto de fluxo normal.
No entanto, para trabalhar com um fluxo, não é necessário criar uma coleção:

 Stream.of("a1", "a2", "a3") .findFirst() .ifPresent(System.out::println); // a1 

Basta usar Stream.of() para criar um fluxo a partir de várias referências a objetos.

Além dos fluxos de objetos regulares, o Java 8 possui tipos especiais de fluxos para trabalhar com tipos primitivos: int, long, double. Como você pode imaginar, esse é IntStream , LongStream , DoubleStream .

Os fluxos IntStream podem substituir os IntStream.range() regulares para (;;) usando IntStream.range() :

 IntStream.range(1, 4) .forEach(System.out::println); // 1 // 2 // 3 

Todos esses fluxos para trabalhar com tipos primitivos funcionam como fluxos regulares de objetos, exceto pelo seguinte:

  • Fluxos primitivos usam expressões lambda especiais. Por exemplo, IntFunction em vez de Function ou IntPredicate em vez de Predicate.
  • Fluxos primitivos suportam métodos terminais adicionais: sum() e average()

     Arrays.stream(new int[] {1, 2, 3}) .map(n -> 2 * n + 1) .average() .ifPresent(System.out::println); // 5.0 


Às vezes, é útil transformar um fluxo de objetos em um fluxo de primitivas ou vice-versa. Para esse propósito, os fluxos de objetos suportam métodos especiais: mapToInt() , mapToLong() , mapToDouble() :

 Stream.of("a1", "a2", "a3") .map(s -> s.substring(1)) .mapToInt(Integer::parseInt) .max() .ifPresent(System.out::println); // 3 

Os fluxos de primitivos podem ser convertidos em fluxos de objetos chamando mapToObj() :

 IntStream.range(1, 4) .mapToObj(i -> "a" + i) .forEach(System.out::println); // a1 // a2 // a3 

No exemplo a seguir, um fluxo de números de ponto flutuante é mapeado para um fluxo de números inteiros e, em seguida, mapeado para um fluxo de objetos:

 Stream.of(1.0, 2.0, 3.0) .mapToInt(Double::intValue) .mapToObj(i -> "a" + i) .forEach(System.out::println); // a1 // a2 // a3 

Ordem de execução


Agora que aprendemos como criar vários fluxos e como trabalhar com eles, vamos nos aprofundar e considerar como as operações de streaming ficam sob o capô.

Uma característica importante dos métodos intermediários é a preguiça . Não há método terminal neste exemplo:

 Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return true; }); 

Quando esse trecho de código é executado, nada será gerado no console. E tudo porque os métodos intermediários são executados apenas se houver um método terminal. Vamos expandir o exemplo adicionando o método de terminal forEach :

 Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return true; }) .forEach(s -> System.out.println("forEach: " + s)); 

A execução desse fragmento de código leva à saída para o console do seguinte resultado:

 filter: d2 forEach: d2 filter: a2 forEach: a2 filter: b1 forEach: b1 filter: b3 forEach: b3 filter: c forEach: c 

A ordem em que os resultados são organizados pode surpreender. Pode-se esperar ingenuamente que os métodos sejam executados "horizontalmente": um após o outro para todos os elementos do fluxo. No entanto, o elemento se move ao longo da cadeia "verticalmente". Primeiro, a primeira linha de “d2” passa pelo método de filter , depois pelo forEach e somente então, depois de passar o primeiro elemento por toda a cadeia de métodos, o próximo elemento começa a ser processado.

Dado esse comportamento, você pode reduzir o número real de operações:

 Stream.of("d2", "a2", "b1", "b3", "c") .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .anyMatch(s -> { System.out.println("anyMatch: " + s); return s.startsWith("A"); }); // map: d2 // anyMatch: D2 // map: a2 // anyMatch: A2 

O método anyMatch retornará true assim que o predicado for aplicado ao elemento recebido. Nesse caso, este é o segundo elemento da sequência - "A2". Consequentemente, devido à execução “vertical” da cadeia de encadeamentos, o map será chamado apenas duas vezes. Assim, em vez de exibir todos os elementos do fluxo, o map será chamado o menor número de vezes possível.

Por que a sequência é importante


O exemplo a seguir consiste em dois métodos intermediários map e filter e um método terminal forEach . Considere como esses métodos são executados:

 Stream.of("d2", "a2", "b1", "b3", "c") .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .filter(s -> { System.out.println("filter: " + s); return s.startsWith("A"); }) .forEach(s -> System.out.println("forEach: " + s)); // map: d2 // filter: D2 // map: a2 // filter: A2 // forEach: A2 // map: b1 // filter: B1 // map: b3 // filter: B3 // map: c // filter: C 

É fácil adivinhar que os métodos de map e filter são chamados 5 vezes em tempo de execução - uma vez para cada elemento da coleção de origem, enquanto o forEach é chamado apenas uma vez - para o elemento que passou no filtro.

Você pode reduzir significativamente o número de operações alterando a ordem das chamadas de método colocando o filter em primeiro lugar:

 Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return s.startsWith("a"); }) .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .forEach(s -> System.out.println("forEach: " + s)); // filter: d2 // filter: a2 // map: a2 // forEach: A2 // filter: b1 // filter: b3 // filter: c 

Agora o mapa é chamado apenas uma vez. Com um grande número de elementos de entrada, observaremos um aumento notável na produtividade. Lembre-se disso ao compor cadeias de métodos complexos.

Expandimos o exemplo acima adicionando uma operação de classificação adicional - o método classificado:

 Stream.of("d2", "a2", "b1", "b3", "c") .sorted((s1, s2) -> { System.out.printf("sort: %s; %s\n", s1, s2); return s1.compareTo(s2); }) .filter(s -> { System.out.println("filter: " + s); return s.startsWith("a"); }) .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .forEach(s -> System.out.println("forEach: " + s)); 

A classificação é um tipo especial de operação intermediária. Essa é a chamada operação com estado, porque para classificar uma coleção, seu estado deve ser levado em consideração durante toda a operação.

Como resultado da execução desse código, obtemos a seguinte saída no console:

 sort: a2; d2 sort: b1; a2 sort: b1; d2 sort: b1; a2 sort: b3; b1 sort: b3; d2 sort: c; b3 sort: c; d2 filter: a2 map: a2 forEach: A2 filter: b1 filter: b3 filter: c filter: d2 

Primeiro, a coleção inteira é classificada. Em outras palavras, o método sorted é executado horizontalmente. Nesse caso, sorted é chamado 8 vezes para várias combinações dos elementos na coleção de entrada.

Mais uma vez, otimizamos a execução desse código alterando a ordem das chamadas de método na cadeia:

 Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return s.startsWith("a"); }) .sorted((s1, s2) -> { System.out.printf("sort: %s; %s\n", s1, s2); return s1.compareTo(s2); }) .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .forEach(s -> System.out.println("forEach: " + s)); // filter: d2 // filter: a2 // filter: b1 // filter: b3 // filter: c // map: a2 // forEach: A2 

Neste exemplo, sorted não é chamado. filter reduz a coleção de entrada para um elemento. No caso de grandes dados de entrada, o desempenho será beneficiado significativamente.

Reutilizar fluxos


No Java 8, os encadeamentos não podem ser reutilizados. Depois de chamar qualquer método de terminal, o encadeamento termina:

 Stream<String> stream = Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> s.startsWith("a")); stream.anyMatch(s -> true); // ok stream.noneMatch(s -> true); // exception 

Chamar noneMatch após anyMatch em um encadeamento resulta na seguinte exceção:

 java.lang.IllegalStateException: stream has already been operated upon or closed at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229) at java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459) at com.winterbe.java8.Streams5.test7(Streams5.java:38) at com.winterbe.java8.Streams5.main(Streams5.java:28) 

Para superar essa limitação, um novo encadeamento deve ser criado para cada método de terminal.

Por exemplo, você pode criar um fornecedor para um novo construtor de encadeamentos no qual todos os métodos intermediários serão instalados:

 Supplier<Stream<String>> streamSupplier = () -> Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> s.startsWith("a")); streamSupplier.get().anyMatch(s -> true); // ok streamSupplier.get().noneMatch(s -> true); // ok 

Cada chamada para o método get cria um novo encadeamento no qual você pode chamar com segurança o método de terminal desejado.

Métodos avançados


Threads suportam um grande número de métodos diferentes. Já nos familiarizamos com os métodos mais importantes. Para se familiarizar com o resto, consulte a documentação . E agora mergulhe ainda mais em métodos mais complexos: collect , flatMap e reduce .

A maioria dos exemplos de código nesta seção se refere ao seguinte snippet de código para demonstrar a operação:

 class Person { String name; int age; Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return name; } } List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12)); 

Coletar


Collect método de terminal muito útil, usado para converter elementos de fluxo em um resultado de um tipo diferente, por exemplo, Lista, Conjunto ou Mapa.

Collect aceita um Collector que contém quatro métodos diferentes: um fornecedor. acumulador, combinador, finalizador. À primeira vista, isso parece muito complicado, mas o Java 8 suporta vários coletores internos por meio da classe Collectors , onde os métodos mais usados ​​são implementados.

Caso popular:

 List<Person> filtered = persons .stream() .filter(p -> p.name.startsWith("P")) .collect(Collectors.toList()); System.out.println(filtered); // [Peter, Pamela] 

Como você pode ver, a criação de uma lista de itens de fluxo é muito simples. Não precisa de uma lista, mas muito? Use Collectors.toSet() .

No exemplo a seguir, as pessoas são agrupadas por idade:

 Map<Integer, List<Person>> personsByAge = persons .stream() .collect(Collectors.groupingBy(p -> p.age)); personsByAge .forEach((age, p) -> System.out.format("age %s: %s\n", age, p)); // age 18: [Max] // age 23: [Peter, Pamela] // age 12: [David] 

Os colecionadores são incrivelmente diversos. Você também pode agregar os elementos da coleção, por exemplo, determinar a idade média:

 Double averageAge = persons .stream() .collect(Collectors.averagingInt(p -> p.age)); System.out.println(averageAge); // 19.0 

Para obter estatísticas mais abrangentes, usamos um coletor de resumo que retorna um objeto especial com informações: valores mínimos, máximos e médios, a soma dos valores e o número de elementos:

 IntSummaryStatistics ageSummary = persons .stream() .collect(Collectors.summarizingInt(p -> p.age)); System.out.println(ageSummary); // IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23} 

O exemplo a seguir combina todos os nomes em uma linha:

 String phrase = persons .stream() .filter(p -> p.age >= 18) .map(p -> p.name) .collect(Collectors.joining(" and ", "In Germany ", " are of legal age.")); System.out.println(phrase); // In Germany Max and Peter and Pamela are of legal age. 

O coletor de conexão aceita um separador, bem como um prefixo e sufixo opcional.

Para converter os elementos de um fluxo em uma exibição, você deve determinar como as chaves e os valores devem ser exibidos. Lembre-se de que as chaves no mapeamento devem ser exclusivas. Caso contrário, obteremos uma IllegalStateException . Opcionalmente, você pode adicionar uma função de mesclagem para ignorar a exceção:

 Map<Integer, String> map = persons .stream() .collect(Collectors.toMap( p -> p.age, p -> p.name, (name1, name2) -> name1 + ";" + name2)); System.out.println(map); // {18=Max, 23=Peter;Pamela, 12=David} 

Então, nos familiarizamos com alguns dos mais poderosos coletores internos. Vamos tentar construir o seu próprio. Queremos converter todos os elementos do fluxo em uma única linha, que consiste em nomes em maiúsculas separados por uma barra vertical |. Para fazer isso, crie um novo coletor usando Collector.of() . Precisamos dos quatro componentes de nosso coletor: fornecedor, bateria, conector, finalizador.

 Collector<Person, StringJoiner, String> personNameCollector = Collector.of( () -> new StringJoiner(" | "), // supplier (j, p) -> j.add(p.name.toUpperCase()), // accumulator (j1, j2) -> j1.merge(j2), // combiner StringJoiner::toString); // finisher String names = persons .stream() .collect(personNameCollector); System.out.println(names); // MAX | PETER | PAMELA | DAVID 

Como as strings em Java são imutáveis, precisamos de uma classe auxiliar como StringJoiner que permita que o coletor construa uma string para nós. Na primeira etapa, o provedor constrói um StringJoiner com um delimitador atribuído. Bateria é usada para adicionar cada nome ao StringJoiner .

O conector sabe como conectar dois StringJoiner em um. E, no final, o finalizador constrói a sequência desejada de StringJoiner s.

Flatmap


Então, aprendemos como transformar objetos de fluxo em outros tipos de objetos usando o método map . Map é um tipo de método limitado, pois cada objeto pode ser mapeado para apenas um outro objeto. Mas e se você quiser mapear um objeto para muitos outros, ou não exibi-lo? É aqui que o método flatMap ajuda. FlatMap transforma cada objeto de fluxo em um fluxo de outros objetos. O conteúdo desses encadeamentos é empacotado no fluxo retornado do método flatMap .

Para analisar o flatMap em ação, vamos criar uma hierarquia de tipos adequada para um exemplo:

 class Foo { String name; List<Bar> bars = new ArrayList<>(); Foo(String name) { this.name = name; } } class Bar { String name; Bar(String name) { this.name = name; } } 

Vamos criar alguns objetos:

 List<Foo> foos = new ArrayList<>(); // create foos IntStream .range(1, 4) .forEach(i -> foos.add(new Foo("Foo" + i))); // create bars foos.forEach(f -> IntStream .range(1, 4) .forEach(i -> f.bars.add(new Bar("Bar" + i + " <- " + f.name)))); 

Agora temos uma lista de três foo , cada um dos quais contém três barras .

FlatMap aceita uma função que deve retornar um fluxo de objetos. Assim, para acessar os objetos de barra de cada foo , basta encontrar a função apropriada:

 foos.stream() .flatMap(f -> f.bars.stream()) .forEach(b -> System.out.println(b.name)); // Bar1 <- Foo1 // Bar2 <- Foo1 // Bar3 <- Foo1 // Bar1 <- Foo2 // Bar2 <- Foo2 // Bar3 <- Foo2 // Bar1 <- Foo3 // Bar2 <- Foo3 // Bar3 <- Foo3 

Assim, transformamos com sucesso um fluxo de três objetos foo em um fluxo de 9 objetos de barra .

Por fim, todo o código acima pode ser reduzido a um simples pipeline de operações:

 IntStream.range(1, 4) .mapToObj(i -> new Foo("Foo" + i)) .peek(f -> IntStream.range(1, 4) .mapToObj(i -> new Bar("Bar" + i + " <- " f.name)) .forEach(f.bars::add)) .flatMap(f -> f.bars.stream()) .forEach(b -> System.out.println(b.name)); 

FlatMap também FlatMap disponível na classe Optional introduzida no Java 8. O FlatMap da classe Optional retorna um objeto opcional de outra classe. Isso pode ser usado para evitar null verificações null .

Imagine uma estrutura hierárquica como esta:

 class Outer { Nested nested; } class Nested { Inner inner; } class Inner { String foo; } 

Para obter a string aninhada foo de um objeto externo, você precisa adicionar várias verificações null para evitar uma NullPointException :

 Outer outer = new Outer(); if (outer != null && outer.nested != null && outer.nested.inner != null) { System.out.println(outer.nested.inner.foo); } 

O mesmo pode ser alcançado usando o flatMap da classe Opcional:

 Optional.of(new Outer()) .flatMap(o -> Optional.ofNullable(o.nested)) .flatMap(n -> Optional.ofNullable(n.inner)) .flatMap(i -> Optional.ofNullable(i.foo)) .ifPresent(System.out::println); 

Cada chamada para flatMap retorna um wrapper Optional para o objeto desejado, se presente, ou para null se o objeto estiver ausente.

Reduzir


A operação de simplificação combina todos os elementos de um fluxo em um único resultado. O Java 8 suporta três tipos diferentes de métodos de redução.

O primeiro reduz o fluxo de elementos para um único elemento de fluxo. Usamos este método para determinar o elemento com a maior idade:

 persons .stream() .reduce((p1, p2) -> p1.age > p2.age ? p1 : p2) .ifPresent(System.out::println); // Pamela 

O método de reduce assume uma função acumulativa com um operador binário (BinaryOperator). Aqui reduce é uma bi-função (BiFunction), na qual os dois argumentos pertencem ao mesmo tipo. No nosso caso, para o tipo Pessoa . Uma bi-função é quase a mesma que uma , mas são necessários 2 argumentos. No nosso exemplo, a função compara a idade de duas pessoas e retorna um elemento com uma idade maior.

A próxima forma do método de reduce leva um valor inicial e uma bateria com um operador binário. Este método pode ser usado para criar um novo item. Nós temos - Pessoa com nome e idade, consistindo na adição de todos os nomes e na soma dos anos vividos:

 Person result = persons .stream() .reduce(new Person("", 0), (p1, p2) -> { p1.age += p2.age; p1.name += p2.name; return p1; }); System.out.format("name=%s; age=%s", result.name, result.age); // name=MaxPeterPamelaDavid; age=76 

O terceiro método de reduce utiliza três parâmetros: o valor inicial, o acumulador com uma bi-função e uma função combinada, como um operador binário. Como o valor inicial do tipo não se limita ao tipo Pessoa, você pode usar a redução para determinar o total de anos de vida de cada pessoa:

 Integer ageSum = persons .stream() .reduce(0, (sum, p) -> sum += p.age, (sum1, sum2) -> sum1 + sum2); System.out.println(ageSum); // 76 

Como você pode ver, obtivemos o resultado 76, mas o que realmente acontece sob o capô?

Expandimos o fragmento de código acima com a saída do texto para a depuração:

 Integer ageSum = persons .stream() .reduce(0, (sum, p) -> { System.out.format("accumulator: sum=%s; person=%s\n", sum, p); return sum += p.age; }, (sum1, sum2) -> { System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2); return sum1 + sum2; }); // accumulator: sum=0; person=Max // accumulator: sum=18; person=Peter // accumulator: sum=41; person=Pamela // accumulator: sum=64; person=David 

Como você pode ver, a função acumuladora realiza todo o trabalho. Ele é chamado pela primeira vez com um valor inicial de 0 e a primeira pessoa máx. Nas próximas três etapas, a soma aumenta constantemente pela idade da pessoa desde a última etapa até atingir a idade total de 76 anos.

Então, o que vem a seguir? O combinador nunca é chamado? Considere a execução paralela deste segmento:

 Integer ageSum = persons .parallelStream() .reduce(0, (sum, p) -> { System.out.format("accumulator: sum=%s; person=%s\n", sum, p); return sum += p.age; }, (sum1, sum2) -> { System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2); return sum1 + sum2; }); // accumulator: sum=0; person=Pamela // accumulator: sum=0; person=David // accumulator: sum=0; person=Max // accumulator: sum=0; person=Peter // combiner: sum1=18; sum2=23 // combiner: sum1=23; sum2=12 // combiner: sum1=41; sum2=35 

Com a execução paralela, obtemos uma saída do console completamente diferente. Agora o combinador está realmente sendo chamado. , , -.

.


. ForkJoinPool ForkJoinPool.commonPool() . 5 — .

 ForkJoinPool commonPool = ForkJoinPool.commonPool(); System.out.println(commonPool.getParallelism()); // 3 

3 . JVM:

 -Djava.util.concurrent.ForkJoinPool.common.parallelism=5 

parallelStream() . parallel() .

, (thread) System.out :

 Arrays.asList("a1", "a2", "b1", "c2", "c1") .parallelStream() .filter(s -> { System.out.format("filter: %s [%s]\n", s, Thread.currentThread().getName()); return true; }) .map(s -> { System.out.format("map: %s [%s]\n", s, Thread.currentThread().getName()); return s.toUpperCase(); }) .forEach(s -> System.out.format("forEach: %s [%s]\n", s, Thread.currentThread().getName())); 

, (thread) (stream):

 filter: b1 [main] filter: a2 [ForkJoinPool.commonPool-worker-1] map: a2 [ForkJoinPool.commonPool-worker-1] filter: c2 [ForkJoinPool.commonPool-worker-3] map: c2 [ForkJoinPool.commonPool-worker-3] filter: c1 [ForkJoinPool.commonPool-worker-2] map: c1 [ForkJoinPool.commonPool-worker-2] forEach: C2 [ForkJoinPool.commonPool-worker-3] forEach: A2 [ForkJoinPool.commonPool-worker-1] map: b1 [main] forEach: B1 [main] filter: a1 [ForkJoinPool.commonPool-worker-3] map: a1 [ForkJoinPool.commonPool-worker-3] forEach: A1 [ForkJoinPool.commonPool-worker-3] forEach: C1 [ForkJoinPool.commonPool-worker-2] 

, (threads) ForkJoinPool . , (thread).

sort :

 Arrays.asList("a1", "a2", "b1", "c2", "c1") .parallelStream() .filter(s -> { System.out.format("filter: %s [%s]\n", s, Thread.currentThread().getName()); return true; }) .map(s -> { System.out.format("map: %s [%s]\n", s, Thread.currentThread().getName()); return s.toUpperCase(); }) .sorted((s1, s2) -> { System.out.format("sort: %s <> %s [%s]\n", s1, s2, Thread.currentThread().getName()); return s1.compareTo(s2); }) .forEach(s -> System.out.format("forEach: %s [%s]\n", s, Thread.currentThread().getName())); 

:

 filter: c2 [ForkJoinPool.commonPool-worker-3] filter: c1 [ForkJoinPool.commonPool-worker-2] map: c1 [ForkJoinPool.commonPool-worker-2] filter: a2 [ForkJoinPool.commonPool-worker-1] map: a2 [ForkJoinPool.commonPool-worker-1] filter: b1 [main] map: b1 [main] filter: a1 [ForkJoinPool.commonPool-worker-2] map: a1 [ForkJoinPool.commonPool-worker-2] map: c2 [ForkJoinPool.commonPool-worker-3] sort: A2 <> A1 [main] sort: B1 <> A2 [main] sort: C2 <> B1 [main] sort: C1 <> C2 [main] sort: C1 <> B1 [main] sort: C1 <> C2 [main] forEach: A1 [ForkJoinPool.commonPool-worker-1] forEach: C2 [ForkJoinPool.commonPool-worker-3] forEach: B1 [main] forEach: A2 [ForkJoinPool.commonPool-worker-2] forEach: C1 [ForkJoinPool.commonPool-worker-1] 

, sort main . sort Stream API Arrays , Java 8, — Arrays.parallelSort() . , , — :
“”, Arrays.sort.
reduce . , . , :

 List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12)); persons .parallelStream() .reduce(0, (sum, p) -> { System.out.format("accumulator: sum=%s; person=%s [%s]\n", sum, p, Thread.currentThread().getName()); return sum += p.age; }, (sum1, sum2) -> { System.out.format("combiner: sum1=%s; sum2=%s [%s]\n", sum1, sum2, Thread.currentThread().getName()); return sum1 + sum2; }); 

, : , , :

 accumulator: sum=0; person=Pamela; [main] accumulator: sum=0; person=Max; [ForkJoinPool.commonPool-worker-3] accumulator: sum=0; person=David; [ForkJoinPool.commonPool-worker-2] accumulator: sum=0; person=Peter; [ForkJoinPool.commonPool-worker-1] combiner: sum1=18; sum2=23; [ForkJoinPool.commonPool-worker-1] combiner: sum1=23; sum2=12; [ForkJoinPool.commonPool-worker-2] combiner: sum1=41; sum2=35; [ForkJoinPool.commonPool-worker-2] 

, . , ( ), .

, ForkJoinPool , JVM. , (threads), .


Java 8 . . , , (Martin Fowler) Collection Pipelines .

JavaScript, Stream.js — JavaScript Java 8 Streams API. , Java 8 Tutorial ( ) Java 8 Nashorn Tutorial .

, , . GitHub . , .

Source: https://habr.com/ru/post/pt437038/


All Articles