Je ne parviens pas à comprendre pleinement le rôle que la combiner
remplit dans la méthode Streams reduce
.
Par exemple, le code suivant ne compile pas:
int length = asList("str1", "str2").stream()
.reduce(0, (accumulatedInt, str) -> accumulatedInt + str.length());
L'erreur de compilation indique: (incompatibilité d'argument; int ne peut pas être converti en Java.lang.String)
mais ce code compile:
int length = asList("str1", "str2").stream()
.reduce(0, (accumulatedInt, str ) -> accumulatedInt + str.length(),
(accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2);
Je comprends que la méthode du combinateur est utilisée dans les flux parallèles. Par conséquent, dans mon exemple, elle consiste à additionner deux ints accumulés intermédiaires.
Mais je ne comprends pas pourquoi le premier exemple ne compile pas sans le combinateur ni comment le combineur résout la conversion de string en int puisqu'il ajoute simplement deux ints.
Quelqu'un peut-il faire la lumière sur cette question?
Les versions à deux et trois arguments de reduce
que vous avez essayé d'utiliser n'acceptent pas le même type pour le accumulator
.
Les deux arguments reduce
sont définis comme :
T reduce(T identity,
BinaryOperator<T> accumulator)
Dans votre cas, T est String, donc BinaryOperator<T>
devrait accepter deux arguments de chaîne et renvoyer une chaîne. Mais vous lui transmettez un int et une chaîne, ce qui entraîne l'erreur de compilation que vous avez obtenue - argument mismatch; int cannot be converted to Java.lang.String
. En fait, je pense que passer 0 comme valeur d’identité est également faux ici, puisqu’une chaîne est attendue (T).
Notez également que cette version de reduction traite un flux de Ts et renvoie un T. Vous ne pouvez donc pas l'utiliser pour réduire un flux de String à un entier.
Le trois argument reduce
est défini comme :
<U> U reduce(U identity,
BiFunction<U,? super T,U> accumulator,
BinaryOperator<U> combiner)
Dans votre cas, U est Integer et T est String. Cette méthode réduira donc un flux de String en Integer.
Pour le BiFunction<U,? super T,U>
_ accumulateur, vous pouvez transmettre des paramètres de deux types différents (U et? super T), qui sont dans votre cas Integer et String. De plus, la valeur d’identité U accepte un Integer dans votre cas, vous pouvez donc lui attribuer la valeur 0.
Une autre façon de réaliser ce que vous voulez:
int length = asList("str1", "str2").stream().mapToInt (s -> s.length())
.reduce(0, (accumulatedInt, len) -> accumulatedInt + len);
Ici, le type du flux correspond au type de retour de reduce
, vous pouvez donc utiliser la version à deux paramètres de reduce
.
Bien sûr, vous n'êtes pas obligé d'utiliser reduce
:
int length = asList("str1", "str2").stream().mapToInt (s -> s.length())
.sum();
réponse d'Eran décrit les différences entre les versions à deux arguments et à trois arguments de reduce
en ce sens que la première permet de réduire Stream<T>
à T
alors que ce dernier réduit Stream<T>
à U
. Cependant, cela n'expliquait pas réellement la nécessité de la fonction de combineur supplémentaire pour réduire Stream<T>
à U
.
L'un des principes de conception de l'API Streams est que l'API ne doit pas différer entre les flux séquentiels et parallèles, autrement dit, une API particulière ne doit pas empêcher un flux de s'exécuter correctement, de manière séquentielle ou en parallèle. Si vos lambdas ont les bonnes propriétés (associatives, non interférentes, etc.), un flux exécuté séquentiellement ou en parallèle devrait donner les mêmes résultats.
Considérons d’abord la version à deux arguments de la réduction:
T reduce(I, (T, T) -> T)
L'implémentation séquentielle est simple. La valeur d'identité I
est "accumulée" avec l'élément stream zeroth pour donner un résultat. Ce résultat est accumulé avec le premier élément de flux pour donner un autre résultat, qui à son tour est accumulé avec le deuxième élément de flux, etc. Une fois le dernier élément accumulé, le résultat final est renvoyé.
L'implémentation parallèle commence par diviser le flux en segments. Chaque segment est traité par son propre thread de la manière séquentielle que j'ai décrite ci-dessus. Maintenant, si nous avons N threads, nous avons N résultats intermédiaires. Celles-ci doivent être réduites à un résultat. Puisque chaque résultat intermédiaire est de type T et que nous en avons plusieurs, nous pouvons utiliser la même fonction accumulateur pour réduire ces N résultats intermédiaires à un seul résultat.
Considérons maintenant une opération hypothétique de réduction de deux arguments qui réduit Stream<T>
à U
. Dans d'autres langues, cela s'appelle une opération "fold" ou "fold-left", c'est ce que j'appellerai ici. Notez que cela n'existe pas en Java.
U foldLeft(I, (U, T) -> U)
(Notez que la valeur d'identité I
est de type U.)
La version séquentielle de foldLeft
est identique à la version séquentielle de reduce
sauf que les valeurs intermédiaires sont de type U au lieu de T. Cependant, il en va autrement. (Une opération hypothétique foldRight
serait similaire, sauf que les opérations seraient effectuées de droite à gauche au lieu de gauche à droite.)
Considérons maintenant la version parallèle de foldLeft
. Commençons par diviser le flux en segments. Nous pouvons alors avoir chacun des N threads réduire les valeurs T de son segment en N valeurs intermédiaires de type U. Maintenant quoi? Comment passer de N valeurs de type U à un seul résultat de type U?
Ce qui manque, c'est une autre fonction qui combine les multiples résultats intermédiaires de type U en un seul résultat de type U. Si nous avons une fonction qui combine deux valeurs U En un, c'est suffisant pour réduire un nombre de valeurs à un, tout comme la réduction initiale ci-dessus. Ainsi, l'opération de réduction qui donne un résultat d'un type différent nécessite deux fonctions:
U reduce(I, (U, T) -> U, (U, U) -> U)
Ou, en utilisant la syntaxe Java:
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
En résumé, pour effectuer une réduction parallèle sur un type de résultat différent, nous avons besoin de deux fonctions: une qui accumule éléments T en valeurs U intermédiaires, et une seconde qui combine les valeurs U intermédiaires en un seul résultat U. Si nous ne changeons pas de type, il s’avère que la fonction d’accumulateur est identique à la fonction de combinaison. C'est pourquoi la réduction au même type ne comporte que la fonction accumulateur et que la réduction à un type différent nécessite des fonctions distinctes d'accumulateur et de combineur.
Enfin, Java ne fournit pas les opérations foldLeft
et foldRight
, car elles impliquent un ordre particulier d'opérations intrinsèquement séquentiel. Cela heurte le principe de conception énoncé. ci-dessus de fournir des API qui prennent en charge le fonctionnement séquentiel et parallèle de manière égale.
Comme j'aime les gribouillis et les flèches pour clarifier les concepts ... commençons!
Supposons avoir 4 chaînes: votre objectif est de concaténer ces chaînes en une seule. Vous commencez par un type et vous finissez par le même type.
Vous pouvez y arriver avec
String res = Arrays.asList("one", "two","three","four")
.stream()
.reduce("",
(accumulatedStr, str) -> accumulatedStr + str); //accumulator
et cela vous aide à visualiser ce qui se passe:
La fonction accumulateur convertit, pas à pas, les éléments de votre flux (rouge) en valeur finale réduite (verte). La fonction accumulateur transforme simplement un objet String
en un autre String
.
Supposons que vous ayez les mêmes 4 chaînes: votre nouvel objectif est de faire la somme de leurs longueurs et vous souhaitez paralléliser votre flux.
Ce dont vous avez besoin est quelque chose comme ceci:
int length = Arrays.asList("one", "two","three","four")
.parallelStream()
.reduce(0,
(accumulatedInt, str) -> accumulatedInt + str.length(), //accumulator
(accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2); //combiner
et ceci est un schéma de ce qui se passe
Ici, la fonction accumulateur (un BiFunction
) vous permet de transformer vos données String
en données int
. Le flux étant parallèle, il est scindé en deux parties (rouges), chacune élaborée indépendamment l'une de l'autre et produisant autant de résultats partiels (orange). Définir un combinateur est nécessaire pour fournir une règle de fusion des résultats partiels int
dans le résultat final (vert) int
.
Que faire si vous ne voulez pas paralléliser votre flux? De toute façon, un combinateur doit être fourni de toute façon, mais il ne sera jamais invoqué, étant donné qu'aucun résultat partiel ne sera produit.
Il n'y a pas de version réduire qui prend deux types différents sans un combineur car elle ne peut pas être exécutée en parallèle (vous ne savez pas pourquoi il s'agit d'une exigence). Le fait que accumulateur soit obligatoirement associatif rend cette interface plutôt inutile puisque:
list.stream().reduce(identity,
accumulator,
combiner);
Produit les mêmes résultats que:
list.stream().map(i -> accumulator(identity, i))
.reduce(identity,
combiner);