Dans les flux Java, l'aperçu est-il vraiment uniquement destiné au débogage?

137

Je lis des informations sur les flux Java et je découvre de nouvelles choses au fur et à mesure. L'une des nouveautés que j'ai trouvées était la peek()fonction. Presque tout ce que j'ai lu sur peek dit qu'il devrait être utilisé pour déboguer vos Streams.

Et si j'avais un Stream où chaque compte a un nom d'utilisateur, un champ de mot de passe et une méthode login () et logIn ().

j'ai aussi

Consumer<Account> login = account -> account.login();

et

Predicate<Account> loggedIn = account -> account.loggedIn();

Pourquoi serait-ce si mauvais?

List<Account> accounts; //assume it's been setup
List<Account> loggedInAccount = 
accounts.stream()
    .peek(login)
    .filter(loggedIn)
    .collect(Collectors.toList());

Pour autant que je sache, cela fait exactement ce qu'il est prévu de faire. Il;

  • Prend une liste de comptes
  • Tente de se connecter à chaque compte
  • Filtre tous les comptes non connectés
  • Collecte les comptes connectés dans une nouvelle liste

Quel est l'inconvénient de faire quelque chose comme ça? Une raison pour laquelle je ne devrais pas continuer? Enfin, sinon cette solution alors quoi?

La version originale de ceci utilisait la méthode .filter () comme suit;

.filter(account -> {
        account.login();
        return account.loggedIn();
    })
Adam.J
la source
38
Chaque fois que j'ai besoin d'un lambda multiligne, je déplace les lignes vers une méthode privée et passe la référence de la méthode au lieu de la lambda.
VGR
1
Quelle est l'intention - essayez-vous de vous connecter à tous les comptes et de les filtrer en fonction de leur connexion (ce qui peut être trivial)? Ou voulez-vous les connecter, puis les filtrer en fonction de leur connexion ou non? Je pose la question dans cet ordre, car c'est forEachpeut-être l'opération que vous souhaitez par opposition à peek. Ce n'est pas parce que c'est dans l'API qu'il n'est pas ouvert aux abus (comme Optional.of).
Makoto
8
Notez également que votre code pourrait être simplement .peek(Account::login)et .filter(Account::loggedIn); il n'y a aucune raison d'écrire un consommateur et un prédicat qui appelle simplement une autre méthode comme celle-là.
Joshua Taylor
2
Notez également que l'API de flux décourage explicitement les effets secondaires dans les paramètres comportementaux .
Didier L
6
Les consommateurs utiles ont toujours des effets secondaires, ceux-ci ne sont évidemment pas découragés. Ceci est en fait mentionné dans la même section: « Un petit nombre d'opérations de flux, telles que forEach()et peek(), ne peuvent fonctionner que via des effets secondaires; ceux-ci doivent être utilisés avec précaution. ». Ma remarque était plutôt de rappeler que l' peekopération (qui est conçue à des fins de débogage) ne doit pas être remplacée en faisant la même chose dans une autre opération comme map()ou filter().
Didier L

Réponses:

77

Les principaux points à retenir:

N'utilisez pas l'API de manière involontaire, même si elle atteint votre objectif immédiat. Cette approche peut se rompre à l'avenir, et elle n'est pas non plus claire pour les futurs responsables.


Il n'y a aucun mal à diviser cela en plusieurs opérations, car ce sont des opérations distinctes. L' utilisation de l'API de manière peu claire et involontaire est nuisible, ce qui peut avoir des ramifications si ce comportement particulier est modifié dans les futures versions de Java.

L'utilisation forEachde cette opération indiquerait clairement au mainteneur qu'il existe un effet secondaire prévu sur chaque élément de accounts, et que vous effectuez une opération qui peut le muter.

C'est également plus conventionnel dans le sens où il peeks'agit d'une opération intermédiaire qui n'opère pas sur l'ensemble de la collection tant que l'opération de terminal n'est pas exécutée, mais qui forEachest en fait une opération de terminal. De cette façon, vous pouvez faire des arguments forts autour du comportement et du flux de votre code plutôt que de poser des questions sur peekle comportement de celui- forEachci dans ce contexte.

accounts.forEach(a -> a.login());
List<Account> loggedInAccounts = accounts.stream()
                                         .filter(Account::loggedIn)
                                         .collect(Collectors.toList());
Makoto
la source
3
Si vous effectuez la connexion lors d'une étape de prétraitement, vous n'avez pas du tout besoin d'un flux. Vous pouvez jouer forEachdirectement à la collection source:accounts.forEach(a -> a.login());
Holger
1
@Holger: Excellent point. J'ai incorporé cela dans la réponse.
Makoto
2
@ Adam.J: Oui, ma réponse s'est concentrée davantage sur la question générale contenue dans votre titre, c'est-à-dire est-ce que cette méthode est vraiment uniquement pour le débogage, en expliquant les aspects de cette méthode. Cette réponse est davantage liée à votre cas d'utilisation réel et à la façon de le faire à la place. Donc, vous pourriez dire, ensemble, ils fournissent une image complète. Premièrement, la raison pour laquelle ce n'est pas l'utilisation prévue, deuxièmement la conclusion, de ne pas s'en tenir à une utilisation non conforme et que faire à la place. Ce dernier aura une utilisation plus pratique pour vous.
Holger
2
Bien sûr, c'était beaucoup plus facile si la login()méthode retournait une booleanvaleur indiquant le statut de réussite…
Holger
3
C'est ce que je visais. Si login()retourne a boolean, vous pouvez l'utiliser comme prédicat qui est la solution la plus propre. Cela a toujours un effet secondaire, mais c'est correct tant que cela n'interfère pas, c'est-à-dire que le loginprocessus de l'un Accountn'a aucune influence sur le processus de connexion d'un autre Account.
Holger
111

La chose importante que vous devez comprendre est que les flux sont pilotés par le fonctionnement du terminal . L'opération de terminal détermine si tous les éléments doivent être traités ou pas du tout. Il en collectva de même pour une opération qui traite chaque élément, alors qu'elle findAnypeut arrêter le traitement des éléments une fois qu'elle a rencontré un élément correspondant.

Et count()ne peut traiter aucun élément du tout lorsqu'il peut déterminer la taille du flux sans traiter les éléments. Puisqu'il s'agit d'une optimisation non faite en Java 8, mais qui le sera en Java 9, il peut y avoir des surprises lorsque vous passez à Java 9 et que vous avez du code reposant sur le count()traitement de tous les éléments. Ceci est également lié à d'autres détails dépendants de l'implémentation, par exemple même dans Java 9, l'implémentation de référence ne sera pas capable de prédire la taille d'une source de flux infinie combinée avec limitalors qu'il n'y a pas de limitation fondamentale empêchant une telle prédiction.

Puisque peekpermet «d'effectuer l'action fournie sur chaque élément au fur et à mesure que les éléments sont consommés à partir du flux résultant », il n'impose pas le traitement des éléments mais exécutera l'action en fonction des besoins de l'opération du terminal. Cela implique que vous devez l'utiliser avec beaucoup de précaution si vous avez besoin d'un traitement particulier, par exemple si vous voulez appliquer une action sur tous les éléments. Cela fonctionne si le fonctionnement du terminal est garanti pour traiter tous les éléments, mais même dans ce cas, vous devez être sûr que le développeur suivant ne modifie pas le fonctionnement du terminal (ou vous oubliez cet aspect subtil).

En outre, alors que les flux garantissent le maintien de l'ordre de rencontre pour une certaine combinaison d'opérations même pour des flux parallèles, ces garanties ne s'appliquent pas à peek. Lors de la collecte dans une liste, la liste résultante aura le bon ordre pour les flux parallèles ordonnés, mais l' peekaction peut être invoquée dans un ordre arbitraire et simultanément.

Ainsi, la chose la plus utile que vous puissiez faire peekest de savoir si un élément de flux a été traité, ce qui est exactement ce que dit la documentation de l'API:

Cette méthode existe principalement pour prendre en charge le débogage, où vous souhaitez voir les éléments au fur et à mesure qu'ils passent un certain point dans un pipeline

Holger
la source
y aura-t-il un problème, futur ou présent, dans le cas d'utilisation d'OP? Son code fait-il toujours ce qu'il veut?
ZhongYu
9
@ bayou.io: pour autant que je sache, il n'y a pas de problème sous cette forme exacte . Mais comme j'ai essayé de l'expliquer, l'utiliser de cette manière implique que vous devez vous souvenir de cet aspect, même si vous revenez au code un ou deux ans plus tard pour incorporer «feature request 9876» dans le code…
Holger
1
"l'action peek peut être invoquée dans un ordre arbitraire et simultanément". Cette affirmation ne va-t-elle pas à l'encontre de leur règle concernant le fonctionnement de Peek, par exemple "au fur et à mesure que les éléments sont consommés"?
Jose Martinez
5
@Jose Martinez: Il dit «car les éléments sont consommés à partir du flux résultant », ce qui n'est pas l'action terminale mais le traitement, bien que même l'action terminale puisse consommer des éléments dans le désordre tant que le résultat final est cohérent. Mais je pense aussi, la phrase de la note de l'API, « voir les éléments au fur et à mesure qu'ils passent au-delà d'un certain point dans un pipeline » fait un meilleur travail pour le décrire.
Holger
23

Peut-être qu'une règle de base devrait être que si vous utilisez un coup d'oeil en dehors du scénario de «débogage», vous ne devriez le faire que si vous êtes sûr des conditions de filtrage de fin et intermédiaires. Par exemple:

return list.stream().map(foo->foo.getBar())
                    .peek(bar->bar.publish("HELLO"))
                    .collect(Collectors.toList());

semble être un cas valable où vous voulez, en une seule opération, transformer tous les Foos en Bars et leur dire à tous bonjour.

Semble plus efficace et élégant que quelque chose comme:

List<Bar> bars = list.stream().map(foo->foo.getBar()).collect(Collectors.toList());
bars.forEach(bar->bar.publish("HELLO"));
return bars;

et vous ne finissez pas par itérer une collection deux fois.

chimère8
la source
4

Je dirais que cela peekoffre la possibilité de décentraliser le code qui peut muter des objets de flux, ou modifier l'état global (en fonction d'eux), au lieu de tout mettre dans une fonction simple ou composée passée à une méthode de terminal.

Maintenant, la question pourrait être: devrions-nous muter les objets de flux ou changer l'état global à partir des fonctions dans la programmation Java de style fonctionnel ?

Si la réponse à l'une des 2 questions ci-dessus est oui (ou: dans certains cas oui), ce peek()n'est certainement pas uniquement à des fins de débogage , pour la même raison qui forEach()n'est pas uniquement à des fins de débogage .

Pour moi, lorsque je choisis entre forEach()et peek(), je choisis ce qui suit: Est-ce que je veux que des morceaux de code qui mutent des objets de flux soient attachés à un composable, ou est-ce que je veux qu'ils s'attachent directement au flux?

Je pense qu'il peek()sera mieux associé aux méthodes java9. par exemple, il takeWhile()peut être nécessaire de décider quand arrêter l'itération en se basant sur un objet déjà muté, de sorte que le coupler avec forEach()n'aurait pas le même effet.

PS je ne l'ai référencé map()nulle part car au cas où on voudrait muter des objets (ou état global), plutôt que de générer de nouveaux objets, ça fonctionne exactement comme peek().

Marinos An
la source
3

Bien que je sois d'accord avec la plupart des réponses ci-dessus, j'ai un cas dans lequel l'utilisation de peek semble en fait la façon la plus propre de procéder.

Similaire à votre cas d'utilisation, supposons que vous souhaitiez filtrer uniquement sur les comptes actifs, puis effectuer une connexion sur ces comptes.

accounts.stream()
    .filter(Account::isActive)
    .peek(login)
    .collect(Collectors.toList());

Peek est utile pour éviter l'appel redondant sans avoir à itérer la collection deux fois:

accounts.stream()
    .filter(Account::isActive)
    .map(account -> {
        account.login();
        return account;
    })
    .collect(Collectors.toList());
UltimaWeapon
la source
3
Tout ce que vous avez à faire est d'obtenir cette méthode de connexion correcte. Je ne vois vraiment pas comment le coup d'oeil est la façon la plus propre d'aller. Comment quelqu'un qui lit votre code devrait-il savoir que vous utilisez réellement l'API de manière abusive? Un code bon et propre ne force pas un lecteur à faire des hypothèses sur le code.
kaba713
1

La solution fonctionnelle consiste à rendre l'objet compte immuable. Donc account.login () doit retourner un nouvel objet de compte. Cela signifie que l'opération de carte peut être utilisée pour la connexion au lieu de peek.

Solubris
la source