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);
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);
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);
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);
Todos esses fluxos para trabalhar com tipos primitivos funcionam como fluxos regulares de objetos, exceto pelo seguinte:
À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);
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);
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);
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"); });
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));
É 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));
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));
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);
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);
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);
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));
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);
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);
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);
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);
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(" | "),
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<>();
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));
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);
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);
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);
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; });
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; });
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 . 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 . , .