Supposons que j'ai un flux de choses et que je souhaite les "enrichir" à mi-chemin, je peux utiliser peek()
pour ce faire, par exemple:
streamOfThings.peek(this::thingMutator).forEach(this::someConsumer);
Supposons que la mutation des choses à ce stade du code soit un comportement correct - par exemple, la méthode thingMutator
peut définir le champ "lastProcessed" à l'heure actuelle.
Cependant, peek()
dans la plupart des contextes signifie "regardez, mais ne touchez pas".
Est-ce que l'utilisation de peek()
pour muter les éléments de flux est un contre-modèle ou mal avisé?
L'approche alternative, plus conventionnelle, serait de convertir le consommateur:
private void thingMutator(Thing thing) {
thing.setLastProcessed(System.currentTimeMillis());
}
à une fonction qui renvoie le paramètre:
private Thing thingMutator(Thing thing) {
thing.setLastProcessed(currentTimeMillis());
return thing;
}
et utilisez map()
à la place:
stream.map(this::thingMutator)...
Mais cela introduit du code superficiel (le return
) et je ne suis pas convaincu que ce soit plus clair, car vous savez que peek()
renvoie le même objet, mais avec map()
ce n'est même pas clair en un coup d'œil qu'il s'agit du même classe d'objet.
De plus, avec peek()
vous pouvez avoir un lambda qui mute, mais avec map()
vous devez construire une épave de train. Comparer:
stream.peek(t -> t.setLastProcessed(currentTimeMillis())).forEach(...)
stream.map(t -> {t.setLastProcessed(currentTimeMillis()); return t;}).forEach(...)
Je pense que la version peek()
est plus claire, et le lambda est clairement en mutation, donc il n'y a pas d'effet secondaire "mystérieux". De même, si une référence de méthode est utilisée et que le nom de la méthode implique clairement une mutation, cela aussi est clair et évident.
Sur une note personnelle, je n'hésite pas à utiliser peek()
pour muter - je trouve cela très pratique.
Vous avez raison, "jeter un œil" au sens anglais du mot signifie "regardez, mais ne touchez pas".
Cependant le JavaDoc déclare:
jeter un œil
Stream peek (Action des consommateurs)
Renvoie un flux composé des éléments de ce flux, effectuant en outre l'action fournie sur chaque élément lorsque les éléments sont consommés à partir du flux résultant.
Mots clés: "exécuter ... action" et "consommé". Le JavaDoc est très clair que nous devrions nous attendre à ce que peek
ait la capacité de modifier le flux.
Cependant le JavaDoc indique également:
Note de l'API:
Cette méthode existe principalement pour prendre en charge le débogage, où vous voulez voir les éléments lorsqu'ils passent au-delà d'un certain point dans un pipeline
Cela indique qu'il est davantage destiné à l'observation, par ex. éléments de journalisation dans le flux.
Ce que je retiens de tout cela, c'est que nous pouvons effectuer des actions en utilisant les éléments du flux, mais que nous devons éviter muter les éléments du flux. Par exemple, allez-y et appelez des méthodes sur les objets, mais essayez d'éviter les opérations de mutation sur eux.
À tout le moins, j'ajouterais un bref commentaire à votre code dans ce sens:
// Note: this peek() call will modify the Things in the stream.
streamOfThings.peek(this::thingMutator).forEach(this::someConsumer);
Les opinions divergent sur l'utilité de tels commentaires, mais j'utiliserais un tel commentaire dans ce cas.
Il pourrait être facilement mal interprété, donc j'éviterais de l'utiliser comme ça. La meilleure option est probablement d'utiliser une fonction lambda pour fusionner les deux actions dont vous avez besoin dans l'appel forEach. Vous pouvez également envisager de renvoyer un nouvel objet plutôt que de muter celui existant - il peut être légèrement moins efficace, mais il est susceptible d'entraîner un code plus lisible et réduit le potentiel d'utilisation accidentelle de la liste modifiée pour une autre opération qui devrait ont reçu l'original.