Accueil / Articles PiApplications. / La plate-forme Java / Le langage

Les streams finis et infinis.

S'il y a bien quelque chose que la programmation impérative évite, ce sont les collections d'objets infinies. Toute au plus utilise-t-on des boucles infinies que l'on force au moyen d'instructions de type while(true) ou for(;;). Ces instructions entraine une attente active lorsque les instructions à l'intérieur de la boucle infinie n'exécutent rien car elles attendent un changement de contexte. Notez qu'une utilisation astucieuse d'un thread permet le même effet mais en provoquant si nécessaire une attente passive.

Certaines méthodes du JDK8 sont parfaitement capable de produire des sur les flux d'éléments (streams). Par exemple, Random.ints() produit un flux infini d'entiers de type primitif int.

Ici l'opération ints est une opération source. L'arrêt du flux sera produit par une opération terminale comme limit(10) qui limitera le flux aux 10 premiers nombres aléatoirement produits ou findFirst(i -> i > 256) qui s'interrompra au premier entier aléatoirement produit de valeur supérieure à 256.

En revanche, l'opération terminale forEach appliquée à un flux de source infinie n'interrompra jamais ce stream. Voici un exemple où un capteur de température produit en continu un fichier de mesures de température en °F qu'il stocke dans un fichier texte. L'idée est ici de produire un code qui transmet à un "auditeur d'évènements" (listener) la température en °C lorsque cette dernière diffère de la précédente :

thermalReader.lines()
 .mapToDouble(sTeta -> Double.parseDouble(sTeta.substring(0, sTeta.length() - 1)))
 .map(dTeta -> ((dTeta  – 32) * 5 / 9)                    // Conversion °F vers °C
 .filter(dTeta -> currentTemperature.equals(dTeta))
 .peek(dTeta -> listener.ifPresent(l -> l.temperatureChanged(dTeta)))
 .forEach(dTeta -> currentTemperature.set(dTeta));

Un des principaux inconvénients de l'opération terminale forEach est que l'on a tendance à l'utiliser en lieu et place d'une boucle dans une approche impérative et non pas fonctionnelle. Voyons ceci sur un exemple. Nous disposons d'une liste de transactions et nous souhaitons en obtenir le montant global. Voic ce que pourrait être une "mauvaise" approche :

List<Transactions> lst = ...
LongAdder ladTotal = new LongAdder();
lst.stream().forEach(trs -> transactionTotal.add(trs.getValue()));
long lTotal = ladTotal.sum();

Cette approche ne répond pas à la programmation fonctionnelle car elle impose la maintient d'un état (variable ladTotal) en dehors du tuyau des opérations. Ceci peut poser des difficultés ou une perte de performance si nous exécutons ce code en parallèle.

Une meilleure approche serait la suivante :

List<Transactions> lst = ...
long lTotal = lst.stream().mapToLong(trs -> trs.getValue()).sum();

Nous parvenons au même résultat mais il n'y a plus d'état intermédiaire maintenu en dehors de la séquence d'opérations.

En revanche, lorsque l'opération forEach est utilisée pour "consommer" l'élément d'un flux, elle est généralement conforme à la programmation fonctionnelle. Par exemple : lst.stream().forEach(trs -> trs.printClientName());.

Ainsi, lorsque l'on compte utiliser une opération forEach, il faut prendre un temps de réflexion et se demander si elle ne peut pas être remplacée par une combinaison d'opérations de transposition (map, flatMap) et de réduction (reduce, sum, average, min, max, etc.).

(c) PiApplications 2015