J'ai du mal à comprendre pleinement le rôle que combiner
remplit la reduce
méthode Streams .
Par exemple, le code suivant ne compile pas:
int length = asList("str1", "str2").stream()
.reduce(0, (accumulatedInt, str) -> accumulatedInt + str.length());
Erreur de compilation dit: (discordance 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 de combinaison est utilisée dans des flux parallèles - donc dans mon exemple, elle additionne deux entiers accumulés intermédiaires.
Mais je ne comprends pas pourquoi le premier exemple ne se compile pas sans le combineur ou comment le combineur résout la conversion de chaîne en int puisqu'il ne fait qu'ajouter deux entiers.
Quelqu'un peut-il faire la lumière sur cette question?
java
java-8
java-stream
Louise Miller
la source
la source
Réponses:
Les versions à deux et trois arguments
reduce
que vous avez essayé d'utiliser n'acceptent pas le même type pour leaccumulator
.Les deux arguments
reduce
sont définis comme :Dans votre cas, T est String, donc
BinaryOperator<T>
devrait accepter deux arguments String et renvoyer une String. Mais vous lui passez un int et une chaîne, ce qui entraîne l'erreur de compilation que vous avez -argument mismatch; int cannot be converted to java.lang.String
. En fait, je pense que passer 0 comme valeur d'identité est également incorrect ici, car 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 en un int.
Les trois arguments
reduce
sont définis comme :Dans votre cas, U est Integer et T est String, donc cette méthode réduira un flux de String à un Integer.
Pour l'
BiFunction<U,? super T,U>
accumulateur, vous pouvez passer des paramètres de deux types différents (U et? Super T), qui dans votre cas sont Integer et String. De plus, la valeur d'identité U accepte un Integer dans votre cas, donc lui passer 0 est très bien.Une autre façon de réaliser ce que vous voulez:
Ici, le type du flux correspond au type de retour de
reduce
, vous pouvez donc utiliser la version à deux paramètres dereduce
.Bien sûr, vous n'avez pas du tout à utiliser
reduce
:la source
mapToInt(String::length)
overmapToInt(s -> s.length())
, je ne sais pas si l'un serait meilleur que l'autre, mais je préfère le premier pour la lisibilité.combiner
est nécessaire, pourquoi ne pas avoir leaccumulator
suffit. Dans ce cas: Le combineur n'est nécessaire que pour les flux parallèles, pour combiner les résultats "accumulés" des threads.La réponse d'Eran a décrit les différences entre les versions
reduce
à deux et trois arguments de en ce que la première se réduitStream<T>
àT
alors que la seconde se réduitStream<T>
àU
. Cependant, cela n'expliquait pas réellement la nécessité de la fonction de combinateur supplémentaire lors de la réductionStream<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, ou en d'autres termes, une API particulière ne doit pas empêcher un flux de s'exécuter correctement de manière séquentielle ou 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:
La mise en œuvre séquentielle est simple. La valeur d'identité
I
est "accumulée" avec l'élément de flux zéro 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, et ainsi de suite. Une fois le dernier élément accumulé, le résultat final est renvoyé.La mise en œuvre 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 seul 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 d'accumulateur pour réduire ces N résultats intermédiaires à un seul résultat.
Considérons maintenant une opération de réduction hypothétique à deux arguments qui se réduit
Stream<T>
àU
. Dans d'autres langues, cela s'appelle une opération «plier» ou «plier à gauche», c'est ainsi que je l'appellerai ici. Notez que cela n'existe pas en Java.(Notez que la valeur d'identité
I
est de type U.)La version séquentielle de
foldLeft
est exactement comme la version séquentielle dereduce
sauf que les valeurs intermédiaires sont de type U au lieu de type T. Mais c'est par ailleurs la même chose. (UnefoldRight
opération hypothétique 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. On peut alors demander à chacun des N threads de réduire les valeurs T de son segment en N valeurs intermédiaires de type U. Et maintenant? 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 une seule, c'est suffisant pour réduire n'importe quel nombre de valeurs à un - tout comme la réduction originale ci-dessus. Ainsi, l'opération de réduction qui donne un résultat d'un type différent nécessite deux fonctions:
Ou, en utilisant la syntaxe Java:
En résumé, pour effectuer une réduction parallèle à un type de résultat différent, nous avons besoin de deux fonctions: une qui accumule les é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 la même que la fonction de combineur. C'est pourquoi la réduction au même type n'a que la fonction d'accumulateur et la réduction à un type différent nécessite des fonctions d'accumulateur et de combinateur séparées.
Enfin, Java ne fournit pas
foldLeft
etfoldRight
opérations parce qu'elles impliquent un ordre particulier des opérations qui est par nature séquentielle. Cela est en contradiction avec le principe de conception énoncé ci-dessus de fournir des API qui prennent en charge le fonctionnement séquentiel et parallèle de la même manière.la source
foldLeft
car le calcul dépend du résultat précédent et ne peut pas être parallélisé?forEachOrdered
. L'état intermédiaire doit cependant être conservé dans une variable capturée.foldLeft
soignée de .Puisque j'aime les griffonnages et les flèches pour clarifier les concepts ... commençons!
From String to String (flux séquentiel)
Supposons que vous ayez 4 chaînes: votre objectif est de concaténer ces chaînes en une seule. En gros, vous commencez par un type et terminez par le même type.
Vous pouvez y parvenir avec
et cela vous aide à visualiser ce qui se passe:
La fonction d'accumulateur convertit, étape par étape, les éléments de votre flux (rouge) en valeur finale réduite (verte). La fonction accumulateur transforme simplement un
String
objet en un autreString
.From String to int (flux parallèle)
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:
et c'est un schéma de ce qui se passe
Ici, la fonction accumulateur (a
BiFunction
) vous permet de transformer vosString
données enint
données. Étant le flux parallèle, il est divisé en deux parties (rouges), dont chacune est élaborée indépendamment l'une de l'autre et produit autant de résultats partiels (orange). La définition d'un combineur est nécessaire pour fournir une règle de fusion desint
résultats partiels dans le résultat final (vert)int
.From String to int (flux séquentiel)
Et si vous ne souhaitez pas paralléliser votre flux? Eh bien, un combineur doit quand même être fourni, mais il ne sera jamais invoqué, étant donné qu'aucun résultat partiel ne sera produit.
la source
Il n'y a pas de version réduite qui prend deux types différents sans combineur car elle ne peut pas être exécutée en parallèle (je ne sais pas pourquoi c'est une exigence). Le fait que l' accumulateur doive être associatif rend cette interface quasiment inutile puisque:
Produit les mêmes résultats que:
la source
map
astuce dépend du particulieraccumulator
etcombiner
peut ralentir sensiblement les choses.accumulator
en supprimant le premier paramètre.