En Java, les stream ne sont vraiment que pour le débogage?

Je suis en train de lire sur les stream Java et de découvrir de nouvelles choses au fur et à mesure. Une des nouvelles choses que j’ai trouvées était la fonction peek() . Presque tout ce que j’ai lu sur jets dit qu’il devrait être utilisé pour déboguer vos Streams.

Que faire 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 logsIn ().

j’ai aussi

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

et

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

Pourquoi serait-ce si mauvais?

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

Maintenant, autant que je sache, cela fait exactement ce que l’on veut faire. Il;

  • Prend une liste de comptes
  • Essaie de se connecter à chaque compte
  • Filtre tous les comptes qui ne sont pas connectés
  • Recueille 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 procéder? Enfin, sinon cette solution alors quoi?

La version originale de cette méthode utilisait la méthode .filter () comme suit:

 .filter(account -> { account.login(); return account.loggedIn(); }) 

La clé à retenir de ceci:

N’utilisez pas l’API de manière involontaire, même si elle atteint votre objective immédiat. Cette approche pourrait s’interrompre à l’avenir, et les futurs responsables ne le savent pas non plus.


Il n’y a pas de mal à répartir cette opération entre plusieurs opérations, car ce sont des opérations distinctes. L’utilisation de l’API de manière imprécise et imprévisible est préjudiciable, ce qui peut avoir des conséquences si ce comportement particulier est modifié dans les futures versions de Java.

L’utilisation de forEach sur cette opération forEach clairement au responsable qu’il y a un effet secondaire prévu sur chaque élément de accounts et que vous effectuez des opérations qui peuvent le muter.

Il est également plus conventionnel dans le sens où peek est une opération intermédiaire qui ne fonctionne pas sur l’ensemble de la collection tant que l’opération du terminal n’a pas été forEach , mais forEach est en effet une opération de terminal. De cette façon, vous pouvez formuler des arguments solides concernant le comportement et le stream de votre code, au lieu de poser des questions sur le comportement de peek comme pour forEach contexte.

 accounts.forEach(a -> a.login()); List loggedInAccounts = accounts.stream() .filter(Account::loggedIn) .collect(Collectors.toList()); 

La chose importante à comprendre est que les stream 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. Donc collect est une opération qui traite chaque élément, alors que findAny peut arrêter de traiter les éléments une fois qu’il a rencontré un élément correspondant.

Et count() ne peut traiter aucun élément lorsqu’il peut déterminer la taille du stream sans traiter les éléments. Comme il ne s’agit pas d’une optimisation Java 8, mais de Java 9, il peut y avoir des sursockets lorsque vous passez à Java 9 et que le code s’appuie sur count() traiter tous les éléments. Ceci est également lié à d’autres détails dépendant 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 stream infinie combinée à une limit , sans limitation fondamentale.

Étant donné que peek permet «d’exécuter l’action fournie sur chaque élément à mesure que les éléments sont consommés à partir du stream résultant », il ne requirejs pas le traitement des éléments, mais effectue l’action en fonction des besoins de l’opération du terminal. Cela implique que vous devez l’utiliser avec le plus grand soin si vous avez besoin d’un traitement particulier, par exemple si vous souhaitez appliquer une action sur tous les éléments. Cela fonctionne si l’opération du terminal est garantie 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).

De plus, alors que les stream garantissent le maintien de l’ordre de rencontre pour certaines combinaisons d’opérations, même pour des stream parallèles, ces garanties ne s’appliquent pas au peek . Lors de la collecte dans une liste, la liste résultante aura le bon ordre pour les stream parallèles ordonnés, mais l’action peek peut être invoquée dans un ordre arbitraire et simultanément.

La chose la plus utile que vous puissiez faire avec peek est donc de savoir si un élément de stream a été traité, ce qui correspond exactement à la documentation de l’API:

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

Peut-être une règle de base devrait être que si vous utilisez peek en dehors du scénario “debug”, vous ne devriez le faire que si vous êtes sûr de ce que sont les conditions de filtrage terminales 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 pour transformer tous les Foos en Bars et leur dire bonjour.

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

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

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

Bien que je sois d’accord avec la plupart des réponses ci-dessus, j’ai un cas où l’utilisation de peek semble être la manière la plus propre de procéder.

Semblable à votre cas d’utilisation, supposons que vous souhaitiez filtrer uniquement sur les comptes actifs, puis effectuez une connexion sur ces comptes.

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

Peek est utile pour éviter les appels redondants sans avoir à parcourir la collection deux fois:

 accounts.stream() .filter(Account::isActive) .map(account -> { account.login(); return account; }) .collect(Collectors.toList()); 

Je dirais que peek fournit la possibilité de décentraliser le code qui peut muter des objects de stream, ou modifier l’état global (basé sur 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 des objects de stream ou changer d’état global à partir de fonctions dans la programmation Java de style fonctionnel ?

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

Pour moi, lorsque vous choisissez entre forEach() et peek() , choisissez ce qui suit: Est-ce que je veux que des morceaux de code qui mutent des objects de stream soient attachés à un composable, ou est-ce que je veux les attacher directement au stream?

Je pense que peek() sera mieux couplé avec les méthodes java9. par exemple, takeWhile() peut avoir besoin de décider quand arrêter l’itération sur la base d’un object déjà muté. Par conséquent, le forEach() avec forEach() n’aurait pas le même effet.

PS Je n’ai pas référencé map() n’importe où car si nous voulons muter des objects (ou un état global), plutôt que de générer de nouveaux objects, cela fonctionne exactement comme peek() .