Traduction du guide de l'API Benjamin Winterberg Stream

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); // C1 // C2 

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); // a1 

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); // a1 

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); // 1 // 2 // 3 

Tous ces flux pour travailler avec des types primitifs fonctionnent exactement comme les flux réguliers d'objets, à l'exception des suivants:

  • Les flux primitifs utilisent des expressions lambda spéciales. Par exemple, IntFunction au lieu de Function, ou IntPredicate au lieu de Predicate.
  • Les flux primitifs prennent en charge des méthodes terminales supplémentaires: sum() et average()

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


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); // 3 

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); // a1 // a2 // a3 

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); // a1 // a2 // a3 

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"); }); // map: d2 // anyMatch: D2 // map: a2 // anyMatch: A2 

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)); // map: d2 // filter: D2 // map: a2 // filter: A2 // forEach: A2 // map: b1 // filter: B1 // map: b3 // filter: B3 // map: c // filter: C 

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)); // filter: d2 // filter: a2 // map: a2 // forEach: A2 // filter: b1 // filter: b3 // filter: c 

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)); // filter: d2 // filter: a2 // filter: b1 // filter: b3 // filter: c // map: a2 // forEach: A2 

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); // ok stream.noneMatch(s -> true); // exception 

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); // ok streamSupplier.get().noneMatch(s -> true); // ok 

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); // [Peter, Pamela] 

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)); // age 18: [Max] // age 23: [Peter, Pamela] // age 12: [David] 

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); // 19.0 

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); // IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23} 

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); // In Germany Max and Peter and Pamela are of legal age. 

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); // {18=Max, 23=Peter;Pamela, 12=David} 

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(" | "), // 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 

É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<>(); // 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)))); 

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)); // Bar1 <- Foo1 // Bar2 <- Foo1 // Bar3 <- Foo1 // Bar1 <- Foo2 // Bar2 <- Foo2 // Bar3 <- Foo2 // Bar1 <- Foo3 // Bar2 <- Foo3 // Bar3 <- Foo3 

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); // Pamela 

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); // name=MaxPeterPamelaDavid; age=76 

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); // 76 

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; }); // accumulator: sum=0; person=Max // accumulator: sum=18; person=Peter // accumulator: sum=41; person=Pamela // accumulator: sum=64; person=David 

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; }); // 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 

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 

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/fr437038/


All Articles