Bonjour, Habr! Je vous présente la traduction de l'article "
Java 8 Stream Tutorial ".
Ce tutoriel, basé sur des exemples de code, fournit une vue d'ensemble complète des flux en Java 8. Lorsque j'ai introduit l'API Stream pour la première fois, mon nom m'a intrigué car il est très en accord avec InputStream et OutputStream du package java.io; Cependant, les threads en Java 8 sont quelque chose de complètement différent.
Les threads sont des
monades qui jouent un rôle important dans le développement de la programmation fonctionnelle en Java.
En programmation fonctionnelle, une monade est une structure qui représente un calcul sous la forme d'une chaîne d'étapes successives. Le type et la structure de la monade déterminent la chaîne d'opérations, dans notre cas, une séquence de méthodes avec des fonctions intégrées d'un type donné.
Ce didacticiel vous apprendra à travailler avec des flux et vous montrera comment gérer les différentes méthodes disponibles dans l'API Stream. Nous analyserons l'ordre des opérations et verrons comment la séquence de méthodes dans la chaîne affecte les performances. Apprenez à
flatMap
méthodes puissantes de l'API Stream telles que
reduce
,
collect
et
flatMap
. À la fin du manuel, nous ferons attention au travail parallèle avec les flux.
Si vous ne vous sentez pas libre de travailler avec des expressions lambda, des interfaces fonctionnelles et des méthodes de référence, il vous sera utile de vous familiariser avec
mon guide des innovations en Java 8 (
traduction en Habré), puis de revenir à l'étude des flux.
Fonctionnement des threads
Un flux représente une séquence d'éléments et fournit diverses méthodes pour effectuer des calculs sur ces éléments:
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);
Les modes de flux sont
intermédiaires (intermédiaires) et
terminaux (terminaux). Les méthodes intermédiaires renvoient un flux, ce qui permet à plusieurs de ces méthodes d'être appelées séquentiellement. Les méthodes de terminal ne renvoient pas de valeur (void) ou renvoient un résultat d'un type autre qu'un flux. Dans l'exemple ci-dessus, le
filter
, la
map
et les
sorted
sont intermédiaires et
forEach
sont terminaux. Pour une liste complète des méthodes de flux disponibles, consultez la
documentation . Une telle chaîne d'opérations de flux est également connue sous le nom de pipeline d'opérations.
La plupart des méthodes de l'API Stream acceptent comme paramètres les expressions lambda, une interface fonctionnelle qui décrit le comportement spécifique de la méthode. La plupart d'entre eux doivent à la fois ne pas interférer et être apatrides. Qu'est-ce que cela signifie?
Une méthode n'interfère pas si elle ne modifie pas les données sous-jacentes sous-jacentes au flux. Par exemple, dans l'exemple ci-dessus, aucune expression lambda ne modifie le tableau de listes myList.
Une méthode est sans état si l'ordre dans lequel l'opération est effectuée est spécifié. Par exemple, aucune expression lambda de l'exemple ne dépend de variables mutables ou d'états d'espace externes qui pourraient changer au moment de l'exécution.
Différents types de fils
Les flux peuvent être créés à partir de diverses données sources, principalement à partir de collections. Les listes et les ensembles prennent en charge les nouvelles méthodes
stream()
et
parllelStream()
pour créer des flux séquentiels et parallèles. Les threads parallèles peuvent fonctionner en mode multi-thread (sur plusieurs threads) et seront discutés à la fin du manuel. En attendant, considérez les threads séquentiels:
Arrays.asList("a1", "a2", "a3") .stream() .findFirst() .ifPresent(System.out::println);
Ici, l'appel de la méthode
stream()
sur une liste renvoie un objet stream normal.
Cependant, pour travailler avec un flux, il n'est pas du tout nécessaire de créer une collection:
Stream.of("a1", "a2", "a3") .findFirst() .ifPresent(System.out::println);
Utilisez simplement
Stream.of()
pour créer un flux à partir de plusieurs références d'objet.
En plus des flux d'objets réguliers, Java 8 dispose de types spéciaux de flux pour travailler avec des types primitifs: int, long, double. Comme vous pouvez le deviner, il s'agit d'
IntStream
,
LongStream
,
DoubleStream
.
Les flux IntStream peuvent remplacer les
IntStream.range()
régulières pour (;;) en utilisant
IntStream.range()
:
IntStream.range(1, 4) .forEach(System.out::println);
Tous ces flux pour travailler avec des types primitifs fonctionnent exactement comme les flux réguliers d'objets, à l'exception des suivants:
Il est parfois utile de transformer un flux d'objets en un flux de primitives ou vice versa. À cet effet, les flux d'objets prennent en charge des méthodes spéciales:
mapToInt()
,
mapToLong()
,
mapToDouble()
:
Stream.of("a1", "a2", "a3") .map(s -> s.substring(1)) .mapToInt(Integer::parseInt) .max() .ifPresent(System.out::println);
Les flux de primitives peuvent être convertis en flux d'objets en appelant
mapToObj()
:
IntStream.range(1, 4) .mapToObj(i -> "a" + i) .forEach(System.out::println);
Dans l'exemple suivant, un flux de nombres à virgule flottante est mappé à un flux d'entiers, puis mappé à un flux d'objets:
Stream.of(1.0, 2.0, 3.0) .mapToInt(Double::intValue) .mapToObj(i -> "a" + i) .forEach(System.out::println);
Ordre d'exécution
Maintenant que nous avons appris à créer différents flux et à travailler avec eux, nous allons approfondir et examiner l'aspect des opérations de streaming sous le capot.
Une caractéristique importante des méthodes intermédiaires est leur
paresse . Il n'y a pas de méthode de terminal dans cet exemple:
Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return true; });
Lorsque ce morceau de code est exécuté, rien ne sera sorti sur la console. Et tout cela parce que les méthodes intermédiaires ne sont exécutées que s'il existe une méthode terminale. Développons l'exemple en ajoutant la méthode du 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));
L'exécution de ce fragment de code conduit à la sortie sur la console du résultat suivant:
filter: d2 forEach: d2 filter: a2 forEach: a2 filter: b1 forEach: b1 filter: b3 forEach: b3 filter: c forEach: c
L'ordre dans lequel les résultats sont organisés peut surprendre. On peut naïvement s'attendre à ce que les méthodes soient exécutées «horizontalement»: l'une après l'autre pour tous les éléments du flux. Cependant, au lieu de cela, l'élément se déplace le long de la chaîne «verticalement». Premièrement, la première ligne de «d2» passe par la méthode de
filter
, puis par
forEach
et seulement ensuite, après avoir passé le premier élément à travers la chaîne de méthodes entière, l'élément suivant commence à être traité.
Compte tenu de ce comportement, vous pouvez réduire le nombre réel d'opérations:
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"); });
La méthode
anyMatch
renvoie
true dès que le prédicat est appliqué à l'élément entrant. Dans ce cas, il s'agit du deuxième élément de la séquence - «A2». Par conséquent, en raison de l'exécution «verticale» de la chaîne de threads, la
map
ne sera appelée que deux fois. Ainsi, au lieu d'afficher tous les éléments du flux, la
map
sera appelée le moins de fois possible.
Pourquoi la séquence est importante
L'exemple suivant consiste en deux méthodes intermédiaires de
map
et de
filter
et d'une méthode terminale
forEach
. Considérez comment ces méthodes sont exécutées:
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));
Il est facile de deviner que les méthodes
map
et
filter
sont appelées 5 fois au moment de l'exécution - une fois pour chaque élément de la collection source, tandis que
forEach
n'est appelé qu'une seule fois - pour l'élément qui a passé le filtre.
Vous pouvez réduire considérablement le nombre d'opérations en modifiant l'ordre des appels de méthode en plaçant le
filter
en premier lieu:
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));
Maintenant, la carte n'est appelée qu'une seule fois. Avec un grand nombre d'éléments d'entrée, nous observerons une augmentation notable de la productivité. Gardez cela à l'esprit lorsque vous composez des chaînes de méthodes complexes.
Nous développons l'exemple ci-dessus en ajoutant une opération de tri supplémentaire - la méthode triée:
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));
Le tri est un type spécial d'opération intermédiaire. Il s'agit de l'opération dite avec état, car pour trier une collection, son état doit être pris en compte tout au long de l'opération.
À la suite de l'exécution de ce code, nous obtenons la sortie suivante sur la 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
Tout d'abord, la collection entière est triée. En d'autres termes, la méthode
sorted
s'exécute horizontalement. Dans ce cas,
sorted
est appelé 8 fois pour plusieurs combinaisons des éléments de la collection entrante.
Encore une fois, nous optimisons l'exécution de ce code en modifiant l'ordre des appels de méthode dans la chaîne:
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));
Dans cet exemple,
sorted
n'est pas appelé du tout.
filter
réduit la collection d'entrée à un élément. Dans le cas de données d'entrée volumineuses, les performances en bénéficieront considérablement.
Réutiliser les flux
Dans Java 8, les threads ne peuvent pas être réutilisés. Après avoir appelé une méthode de terminal, le thread se termine:
Stream<String> stream = Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> s.startsWith("a")); stream.anyMatch(s -> true);
L'appel de
noneMatch
après
anyMatch
dans un thread entraîne l'exception suivante:
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)
Pour surmonter cette limitation, un nouveau thread doit être créé pour chaque méthode de terminal.
Par exemple, vous pouvez créer un
fournisseur pour un nouveau constructeur de threads dans lequel toutes les méthodes intermédiaires seront installées:
Supplier<Stream<String>> streamSupplier = () -> Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> s.startsWith("a")); streamSupplier.get().anyMatch(s -> true);
Chaque appel à la méthode
get
crée un nouveau thread dans lequel vous pouvez appeler en toute sécurité la méthode de terminal souhaitée.
Méthodes avancées
Les threads prennent en charge un grand nombre de méthodes différentes. Nous nous sommes déjà familiarisés avec les méthodes les plus importantes. Pour vous familiariser avec le reste, reportez-vous à la
documentation . Et maintenant, plongez encore plus dans des méthodes plus complexes:
collect
,
flatMap
et
reduce
.
La plupart des exemples de code de cette section font référence à l'extrait de code suivant pour illustrer le fonctionnement:
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));
Recueillir
Collect
méthode de terminal très utile, qui est utilisée pour convertir des éléments de flux en un résultat d'un type différent, par exemple, List, Set ou Map.
Collect
accepte un
Collector
qui contient quatre méthodes différentes: un fournisseur. accumulateur, combineur, finisseur. À première vue, cela semble très compliqué, mais Java 8 prend en charge divers collecteurs intégrés via la classe
Collectors
, où les méthodes les plus utilisées sont implémentées.
Cas populaire:
List<Person> filtered = persons .stream() .filter(p -> p.name.startsWith("P")) .collect(Collectors.toList()); System.out.println(filtered);
Comme vous pouvez le voir, la création d'une liste à partir d'éléments de flux est très simple. Pas besoin d'une liste mais beaucoup? Utilisez
Collectors.toSet()
.
Dans l'exemple suivant, les personnes sont regroupées par âge:
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));
Les collectionneurs sont incroyablement diversifiés. Vous pouvez également agréger les éléments de la collection, par exemple, déterminer l'âge moyen:
Double averageAge = persons .stream() .collect(Collectors.averagingInt(p -> p.age)); System.out.println(averageAge);
Pour obtenir des statistiques plus complètes, nous utilisons un collecteur récapitulatif qui renvoie un objet spécial avec des informations: valeurs minimale, maximale et moyenne, la somme des valeurs et le nombre d'éléments:
IntSummaryStatistics ageSummary = persons .stream() .collect(Collectors.summarizingInt(p -> p.age)); System.out.println(ageSummary);
L'exemple suivant combine tous les noms sur une seule ligne:
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);
Le collecteur de connexion accepte un séparateur, ainsi qu'un préfixe et un suffixe facultatifs.
Pour convertir les éléments d'un flux en affichage, vous devez déterminer comment les clés et les valeurs doivent être affichées. N'oubliez pas que les clés du mappage doivent être uniques. Sinon, nous obtenons une
IllegalStateException
. Vous pouvez éventuellement ajouter une fonction de fusion pour contourner l'exception:
Map<Integer, String> map = persons .stream() .collect(Collectors.toMap( p -> p.age, p -> p.name, (name1, name2) -> name1 + ";" + name2)); System.out.println(map);
Nous avons donc fait la connaissance de certains des collecteurs intégrés les plus puissants. Essayons de construire le vôtre. Nous voulons convertir tous les éléments du flux en une seule ligne, qui se compose de noms en majuscules séparés par une barre verticale |. Pour ce faire, créez un nouveau collecteur à l'aide de
Collector.of()
. Nous avons besoin des quatre composants de notre collecteur: fournisseur, batterie, connecteur, finisseur.
Collector<Person, StringJoiner, String> personNameCollector = Collector.of( () -> new StringJoiner(" | "),
Étant donné que les chaînes en Java sont immuables, nous avons besoin d'une classe d'assistance comme
StringJoiner
qui permet au collecteur de créer une chaîne pour nous. Dans la première étape, le fournisseur construit un
StringJoiner
avec un délimiteur affecté. La batterie est utilisée pour ajouter chaque nom à
StringJoiner
.
Le connecteur sait comment connecter deux
StringJoiner
en un seul. Et à la fin, le finisseur construit la chaîne souhaitée à partir de
StringJoiner
s.
Flatmap
Nous avons donc appris à transformer des objets de flux en d'autres types d'objets à l'aide de la méthode
map
.
Map
est une sorte de méthode limitée, car chaque objet ne peut être mappé qu'à un seul autre objet. Mais que se passe-t-il si vous souhaitez mapper un objet à plusieurs autres, ou ne pas l'afficher du tout? C'est là que la méthode
flatMap
aide.
FlatMap
transforme chaque objet de flux en un flux d'autres objets. Le contenu de ces threads est ensuite conditionné dans le flux renvoyé de la méthode
flatMap
.
Afin de regarder
flatMap
en action, construisons une hiérarchie de types appropriée pour un exemple:
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; } }
Créons quelques objets:
List<Foo> foos = new ArrayList<>();
Nous avons maintenant une liste de trois
foo , chacune contenant trois
barres .
FlatMap
accepte une fonction qui devrait renvoyer un flux d'objets. Ainsi, pour accéder aux objets
bar de chaque
foo , il suffit de trouver la fonction appropriée:
foos.stream() .flatMap(f -> f.bars.stream()) .forEach(b -> System.out.println(b.name));
Nous avons donc réussi à transformer un flux de trois objets
foo en un flux de 9 objets
bar .
Enfin, tout le code ci-dessus peut être réduit à un simple pipeline d'opérations:
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
également disponible dans la classe
Optional
introduite dans Java 8.
FlatMap
de la classe
Optional
renvoie un objet facultatif d'une autre classe. Cela peut être utilisé pour éviter un tas de contrôles
null
.
Imaginez une structure hiérarchique comme celle-ci:
class Outer { Nested nested; } class Nested { Inner inner; } class Inner { String foo; }
Pour obtenir la chaîne imbriquée
foo d'un objet externe, vous devez ajouter plusieurs vérifications
null
pour éviter une
NullPointException
:
Outer outer = new Outer(); if (outer != null && outer.nested != null && outer.nested.inner != null) { System.out.println(outer.nested.inner.foo); }
La même chose peut être obtenue en utilisant le flatMap de la classe facultative:
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);
Chaque appel à
flatMap
renvoie un wrapper
Optional
pour l'objet souhaité, s'il est présent, ou pour
null
si l'objet est manquant.
Réduire
L'opération de simplification combine tous les éléments d'un flux en un seul résultat. Java 8 prend en charge trois types différents de méthodes de réduction.
Le premier réduit le flux d'éléments à un seul élément de flux. Nous utilisons cette méthode pour déterminer l'élément ayant le plus grand âge:
persons .stream() .reduce((p1, p2) -> p1.age > p2.age ? p1 : p2) .ifPresent(System.out::println);
La méthode de
reduce
prend une fonction d'accumulation avec un
opérateur binaire (BinaryOperator). Ici,
reduce
est une
bi-fonction (BiFunction), où les deux arguments appartiennent au même type. Dans notre cas, au type
Personne . Une bi-fonction est presque la même chose qu'une
, mais elle prend 2 arguments. Dans notre exemple, la fonction compare l'âge de deux personnes et renvoie un élément avec un âge supérieur.
La forme suivante de la méthode de
reduce
prend à la fois une valeur initiale et une batterie avec un opérateur binaire. Cette méthode peut être utilisée pour créer un nouvel élément. Nous avons -
Personne avec un nom et un âge, comprenant l'addition de tous les noms et la somme des années vécues:
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);
La troisième méthode de
reduce
prend trois paramètres: la valeur initiale, l'accumulateur avec une bi-fonction et une fonction de combinaison telle qu'un opérateur binaire. Étant donné que la valeur initiale du type n'est pas limitée au type Personne, vous pouvez utiliser la réduction pour déterminer le nombre total d'années vécues de chaque personne:
Integer ageSum = persons .stream() .reduce(0, (sum, p) -> sum += p.age, (sum1, sum2) -> sum1 + sum2); System.out.println(ageSum);
Comme vous pouvez le voir, nous avons obtenu le résultat 76, mais que se passe-t-il vraiment sous le capot?
Nous développons le fragment de code ci-dessus avec la sortie du texte pour le débogage:
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; });
Comme vous pouvez le voir, la fonction d'accumulation effectue tout le travail. Il est d'abord appelé avec une valeur initiale de 0 et la première personne Max. Au cours des trois prochaines étapes, la somme augmente constamment selon l'âge de la personne depuis la dernière étape jusqu'à ce qu'elle atteigne un âge total de 76 ans.
Et maintenant? Le combineur n'est-il jamais appelé? Considérez l'exécution parallèle de ce fil:
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; });
Avec une exécution parallèle, nous obtenons une sortie de console complètement différente. Maintenant, le combineur est vraiment appelé. , , -.
.
.
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 . , .