Introduction aux Streams de Java 8

 Chapitre 4. Présentation des Stream

Ce chapitre couvre

  • Qu’est-ce qu’une Stream ?
  • Collections vs Stream
  • Itération interne vs externe
  • Opérations intermédiaires par rapport aux opérations terminales

Collections est l’API la plus utilisée en Java. Que feriez-vous sans Collections ? Presque chaque application Java fabrique et traite des collections. Les collections sont fondamentales pour de nombreuses tâches de programmation : elles vous permettent de regrouper et de traiter les données. Pour illustrer les collections en action, imaginez que vous voulez créer une collection de plats afin de représenter un menu, puis la parcourir pour additionner les calories de chaque plat. Vous voudrez peut-être traiter la collection pour sélectionner uniquement des plats faibles en calories pour un menu spécial sain. Mais en dépit des collections nécessaires pour presque toutes les applications Java, la manipulation des collections est loin d’être parfaite :

  • Une grande partie de la logique métier implique des opérations de type base de données telles que le regroupement d’une liste de plats par catégorie (par exemple, tous les plats végétariens) ou la recherche du plat le plus cher. Combien de fois vous arrive-t-il de réimplémenter ces opérations en utilisant des itérateurs ? La plupart des bases de données vous permettent de spécifier ces opérations de manière déclarative. Par exemple, la requête SQL suivante vous permet de sélectionner les noms des plats peu caloriques : SELECT name FROM plats WHERE calorie <400. Comme vous pouvez le voir, vous n’avez pas besoin de mettre en place un filtre en utilisant les attributs d’un plat (par exemple, en utilisant un itérateur et un accumulateur). Au lieu de cela, vous exprimez seulement ce que vous attendez. Cette idée de base signifie que vous vous inquiétez moins sur la façon d’implémenter explicitement de telles requêtes – c’est géré pour vous ! Pourquoi ne pourriez-vous pas faire quelque chose de similaire avec les collections ?
  • Pour gagner en performance, vous devez la traiter en parallèle et tirer parti des architectures multicœurs. Mais écrire du code parallèle est compliqué, encore plus s’il cela est couplé avec l’utilisation d’itérateur. De plus, ce n’est pas facile à déboguer.

Alors, que pourraient faire les concepteurs de Java pour économiser votre précieux temps et vous faciliter la vie en tant que programmeurs ? Vous avez peut-être deviné : la réponse est Streams.

4.1. C’est quoi l’API Stream?

Les Streams sont une mise à jour de l’API Java 8 qui vous permet de manipuler des collections de données de manière déclarative (vous exprimez une requête plutôt que de coder une implémentation ad hoc). Pour l’instant, vous pouvez les considérer comme des itérateurs de fantaisie sur une collection de données. En outre, les Streams peuvent être traités en parallèle de manière transparente, sans que vous ayez à écrire du code multithreadé ! Nous expliquons en détail dans le chapitre 7 comment fonctionnent les Stream et la parallélisation. Voici un aperçu des avantages de l’utilisation des Stream : comparez le code suivant pour renvoyer les noms des plats à faible teneur en calories, classés par nombre de calories, d’abord en Java 7, puis en Java 8 en utilisant les flux. Ne vous inquiétez pas trop du code Java 8 ; nous l’expliquerons en détail dans les prochaines sections.

Avant (Java 7):

Dans ce code, vous utilisez une variable, lowCaloricDishes. Son seul but est d’agir comme un conteneur jetable intermédiaire. Dans Java 8, ce détail d’implémentation est poussé dans la bibliothèque à laquelle il appartient.

Après (Java 8):

Pour exploiter une architecture multicœur et exécuter ce code en parallèle, il suffit de changer stream () en parallelStream () :

Vous vous demandez peut-être ce qui se passe exactement lorsque vous appelez la méthode parallelStream. Combien de threads sont utilisés ? Quels sont les avantages de performance ? Le chapitre 7 traite de ces questions en détail. Pour l’instant, vous pouvez voir que la nouvelle approche offre plusieurs avantages immédiats du point de vue de l’ingénierie logicielle :

  • Le code est écrit de façon déclarative : vous spécifiez ce que vous voulez réaliser (c’est-à-dire filtrer les plats qui sont peu caloriques) plutôt que de spécifier comment implémenter une opération (en utilisant des blocs de contrôle tels que des boucles et les conditions si). Comme vous l’avez vu dans le chapitre précédent, cette approche, combinée au paramétrage du comportement, vous permet de faire face à l’évolution des besoins : vous pouvez facilement créer une version supplémentaire de votre code permettant de filtrer les plats hypercaloriques en utilisant une expression lambda sans avoir à recourir au copy-paste.
  • Vous enchaînez plusieurs opérations de bloc de construction pour exprimer un pipeline complexe de traitement de données (vous enchaînez le filtre en liant les opérations de tri, de mapping et de collecte, comme illustré à la figure 4.1) tout en conservant la lisibilité de votre code d’une part, et la fonction attendue d’autre part. Le résultat du filtre est transmis à la méthode sort, qui est à son tour ensuite transmise à la méthode map puis à la méthode collect.

Parce que des opérations telles que le filtrage (ou trié, mapper et collecter) sont disponibles en tant que blocs de construction de haut niveau qui ne dépendent pas d’un modèle de thread spécifique, leur implémentation interne pourrait être mono thread ou potentiellement être maximisée en utilisant l’architecture multi cœur de manière transparente ! En pratique, cela signifie que vous n’avez plus à vous soucier des threads et des verrous pour savoir comment paralléliser certaines tâches de traitement de données : l’API Streams le fait pour vous.

La nouvelle API Streams est très expressive. Par exemple, après avoir lu ce chapitre et les chapitres 5 et 6, vous serez capable d’écrire du code comme ceci :

Cet exemple particulier est expliqué en détail dans le chapitre 6, « Collecte de données avec des Stream ». Il regroupe fondamentalement les plats par leurs types à l’intérieur d’une Map. Par exemple, le résultat peut être le suivant :

Maintenant, essayez de réfléchir à la façon de l’implémenter avec l’approche de programmation impérative typique utilisant des boucles. Ne perdez plus votre temps et embrassez le pouvoir des Stream dans ce chapitre et les suivants.



Autres bibliothèques: Guava, Apache

Il y a eu de nombreuses tentatives pour fournir aux programmeurs Java de meilleures bibliothèques pour manipuler les collections. Par exemple, Guava est une bibliothèque populaire créée par Google. Elle fournit des classes de conteneurs supplémentaires telles que les multimaps et les multisets. La bibliothèque Apache Commons Collections offre des fonctionnalités similaires.

Maintenant, Java 8 est livré avec sa propre bibliothèque officielle pour manipuler les collections dans un style plus déclaratif.



Pour résumer, l’API Streams dans Java 8 vous permet d’écrire du code

• Déclaratif – Plus concis et lisible
• Composable- Flexibilité accrue
• Parallélisable- Meilleure performance

Pour le reste de ce chapitre et le suivant, nous utiliserons le domaine suivant pour nos exemples : un menu qui n’est rien de plus qu’une liste de plats.

Dish est une classe non modifiable:

Nous allons maintenant explorer comment vous pouvez utiliser l’API Streams plus en détail. Nous comparerons les Stream aux collections et fournirons des informations de fond. Dans le chapitre suivant, nous allons étudier en détail les opérations de Stream disponibles permettant de construire des requêtes de traitement de données plus sophistiquées. Nous examinerons de nombreux modèles tels que le filtrage, le découpage, la recherche, l’appariement, le mapping et la réduction. Il y aura beaucoup de quiz et d’exercices pour essayer de consolider votre compréhension.

Ensuite, nous discuterons de comment vous pouvez créer et manipuler des Streams numériques, par exemple, pour générer un flux de nombres pairs ou de triplets pythagoriciens. Enfin, nous allons discuter de la façon dont vous pouvez créer des Streams provenant de différentes sources, par exemple à partir d’un fichier. Nous allons également discuter de la façon de générer des flux avec un nombre infini d’éléments, chose que vous ne pouviez certainement pas faire avec des collections.

4.2. Premiers pas avec les flux

Nous commençons notre discussion sur les Streams avec des collections, parce que c’est la façon la plus simple de commencer à travailler avec les flux. Les collections de Java 8 prennent en charge une nouvelle méthode Stream() qui renvoie une Stream (la définition de l’interface est disponible dans java.util.stream.Stream). Vous verrez plus tard que vous pouvez également obtenir des flux de différentes manières (par exemple, générer des éléments de flux à partir d’une plage numérique ou à partir de ressources d’E / S).

Donc, d’abord, qu’est-ce qu’une Stream ? Une courte définition est « une séquence d’éléments provenant d’une source qui prend en charge les opérations de traitement de données ». Décomposons cette définition étape par étape :

  • Séquence d’éléments – Comme une collection, un flux fournit une interface à un ensemble séquencé de valeurs d’un type d’élément spécifique. Les collections étant des structures de données, elles concernent principalement le stockage et l’accès à des éléments présentant des complexités temporelles spécifiques (par exemple, une liste ArrayList ou LinkedList). Mais les Streams sont sur l’expression des calculs tels que le filtre, le tri et le mapping que vous avez vu plus tôt. En d’autres mots : Les collections concernent les données; tandis que les Streams, les calculs. Nous expliquons cette idée plus en détail dans les prochaines sections.
  • Source – Les Streams sont consommés à partir d’une source fournissant des données, telles que des collections, des matrices ou des ressources d’E / S. Notez que la génération d’un flux à partir d’une collection ordonnée préserve l’ordre. En gros les éléments d’une Stream provenant d’une liste auront le même ordre que la liste.
  • Opérations de traitement des données : les Streams prennent en charge des opérations de type base de données et des opérations courantes généralement inhérente à la programmation fonctionnelle pour manipuler des données, comme filtrer, mapper, réduire, rechercher, assortir, trier, etc. Les opérations de Stream peuvent être exécutées séquentiellement ou en parallèle.

De plus, les opérations de Stream ont deux caractéristiques importantes :

  • Pipelining– De nombreuses opérations de Stream retournent elles-mêmes une Stream, permettant aux opérations d’être enchaînées et de former un pipeline plus grand. Cela permet certaines optimisations que nous expliquons dans le prochain chapitre, comme la paresse lors de l’évaluation et les courts-circuits. Un pipeline d’opérations peut être considéré comme une requête de type base de données sur la source de données.
  • Itération interne – Contrairement aux collections, qui sont itérées explicitement à l’aide d’un itérateur, les opérations de Stream effectuent l’itération dans les coulisses pour nous. Nous avons brièvement mentionné cette idée au chapitre 1 et y reviendrons plus tard dans la section suivante. Regardons un exemple de code pour expliquer toutes ces idées:

Dans cet exemple, vous obtenez d’abord une Stream de la liste des plats en appelant la méthode stream() sur le menu. La source de données est la liste des plats (le menu). C’est elle qui fournit une séquence d’éléments à la Stream. Ensuite, vous appliquez une série d’opérations sur le flux : filtrer, mapper, limiter et collecter. Toutes ces opérations, sauf collecter, renvoient un autre flux afin qu’elles puissent être connectées pour former un pipeline, qui peut être considéré comme une requête sur la source. Enfin, l’opération de collecte commence à traiter le pipeline pour renvoyer un résultat (c’est différent car il renvoie autre chose qu’un flux-ici, une liste). Aucun résultat n’est produit et aucun élément du menu n’est sélectionné, jusqu’à ce que la collecte soit invoquée. Vous pouvez penser à cela comme si les invocations de méthode dans la chaîne étaient en file d’attente jusqu’à ce que la collecte soit appelée. La figure 4.2 montre la séquence des opérations sur la Stream : filtre, mapping, limit et collecte, dont chacune est brièvement décrite ici:

  • Filter – Prend une expression lambda pour exclure certains éléments du flux. Dans ce cas, vous sélectionnez les plats qui ont plus de 300 calories en passant le lambda d-> d.getCalories>300.
  • Map – Prend une expression lambda pour transformer un élément en un autre ou pour extraire des informations. Dans ce cas, vous extrayez le nom pour chaque plat en passant la référence de méthode Dish::getName, qui est équivalente à d -> d.getName ().
  • Limit – Tronque un flux pour ne contenir qu’un nombre d’éléments donnés.
  • Collect – Convertit un flux en une structure de données. Dans ce cas, vous convertissez le flux en une liste. Cela ressemble un peu à de la magie ; nous décrivons comment Collect fonctionne plus en détail au chapitre 6. À l’heure actuelle, vous pouvez voir ce mot clé comme une opération qui prend comme argument diverses recettes pour accumuler les éléments d’un flux dans une structure de donnée adéquate. Ici, toList () décrit une recette pour convertir un flux en une liste.

Remarquez comment le code que nous venons de décrire est très différent de ce que vous écririez si vous deviez traiter la liste des éléments du menu étape par étape. Tout d’abord, vous utilisez un style beaucoup plus déclaratif pour traiter les données dans le menu où vous dites ce qui doit être fait : « Trouver les noms de trois plats hyper-caloriques. » Vous n’implémentez pas le filtrage, ni l’extraction, ou même des fonctionnalités de tronquage (limite); elles sont toutes les deux disponibles via la bibliothèque Streams. En conséquence, l’API Streams a plus de flexibilité pour décider comment optimiser ce pipeline. Par exemple, les étapes de filtrage, d’extraction et de troncature peuvent être fusionnées en un seul passage et s’arrêter dès que trois plats sont trouvés. Nous verrons un exemple pour le démontrer dans le prochain chapitre.

4.3. Streams vs. collections

Tant la notion de collections existante Java, que la nouvelle notion de Stream fournissent des interfaces aux structures de données représentant un ensemble séquencé d’élément. Par séquencé, nous entendons généralement passer les valeurs à leur tour plutôt que d’y accéder au hasard dans n’importe quel ordre. Alors, quelle est la différence ?

Nous allons commencer avec une métaphore visuelle. Considérez un film stocké sur un DVD. Ceci est une collection (peut-être des octets ou des frames. Peu importe) car il contient toute la structure de données du DVD. Maintenant, envisagez de regarder la même vidéo lorsqu’elle est diffusée sur Internet. C’est maintenant un flux, une Stream (d’octets ou d’images). Le lecteur vidéo en continu doit avoir téléchargé seulement quelques images en avance, pour que vous puissiez commencer à afficher les images depuis le début du flux et ceci avant même que la plupart des images du flux n’aient été traitées. Notez en particulier que le lecteur vidéo peut manquer de mémoire pour mettre en mémoire tampon le flux entier comme avec une collection. Dans ce cas, le temps de démarrage serait épouvantable si vous deviez attendre l’affichage de la dernière image avant de pouvoir commencer à afficher la vidéo.

En quelques mots, la différence entre les collections et les Stream apparait lorsqu’on regarde le moment où les éléments sont traités. Une collection est une structure de données en mémoire contenant toutes les valeurs de la structure de données. Chaque élément de la collection doit être calculé et traité avant de pouvoir être ajouté à la collection. (Vous pouvez ajouter des éléments à la collection et les supprimer de la collection, mais à chaque instant, chaque élément de la collection est stocké en mémoire, les éléments doivent être calculés avant de faire partie de la collection).

En revanche, un flux est conceptuellement une structure de données fixe (vous ne pouvez pas ajouter ou supprimer des éléments) dont les éléments sont calculés à la demande. Cela donne lieu à d’importants avantages de programmation. Dans le chapitre 6, nous montrons combien il est simple de construire une Stream contenant tous les nombres premiers (2,3,5,7,11, …) même s’il y en a un nombre infini. L’idée est qu’un utilisateur n’extraira que les valeurs requises d’un flux, et ces éléments sont produits de manière invisible à l’utilisateur, et au besoin. C’est une forme de relation producteur-consommateur. Un autre point de vue est qu’un flux est en fait une collection construite de façon pareusseuse : les valeurs sont calculées lorsqu’elles sont sollicitées par un consommateur (en gestion, il s’agit d’une production axée exclusivement sur la demande).

En revanche, une collection est ardemment construite (axée sur le fournisseur : remplissez votre entrepôt avant de commencer à vendre, comme une nouveauté de Noël qui a une durée de vie limitée). En appliquant ceci à l’exemple des nombres premiers, c’est à dire, tenter de construire une collection de tous les nombres premiers. Cela conduirait à une boucle qui calculerait contunuellement un nombre premier, l’ajouterait à la collection, mais qui bien sûr ne pourrait jamais se terminer. En gros la construction de la collection ne serait jamais complète (« remplissez votre entrepôt à l’infini avant de commencer à vendre –> La vente ne débutera jamais »).

La figure 4.3 illustre la différence entre un flux et une collection appliquée à notre exemple de streaming DVD / Internet.

Un autre exemple est une recherche sur Internet par navigateur. Supposons que vous recherchiez une phrase avec de nombreuses correspondances dans Google ou dans une boutique en ligne. Au lieu d’attendre toute la collection de résultats avec leurs photos à télécharger, vous obtenez un flux dont les éléments sont les 0 ou 20 meilleurs, avec un bouton pour cliquer sur les 10 ou 20 prochains. Lorsque vous, le consommateur, cliquez pour les 10 suivants, le fournisseur les calcule à la demande, avant de les retourner à votre navigateur pour les afficher.

4.3.1. Traversable seulement une fois

Notez que, de manière similaire aux itérateurs, une Stream ne peut être parcourue qu’une seule fois. Après cela, le flux est dit être consommé. Vous pouvez obtenir un nouveau flux à partir de la source de données initiale pour le parcourir à nouveau comme pour un itérateur (en supposant que c’est une source reproductible comme une collection, si c’est un canal d’E / S, vous n’avez pas de chance). Par exemple, le code suivant déclencherait une exception indiquant que le flux a été consommé:

Alors gardez à l’esprit que vous ne pouvez consommer un flux qu’une seule fois!



Streams et collections d’un point de vue philosophique

Pour les lecteurs qui aiment les points de vue philosophiques, vous pouvez voir un flux comme un ensemble de valeurs réparties dans le temps. En revanche, une collection est un ensemble de valeurs réparties dans l’espace (ici, la mémoire de l’ordinateur), qui existent toutes à un moment donné, et auxquelles vous accédez en utilisant un itérateur pour accéder aux membres à l’intérieur d’une boucle.



Une autre différence essentielle entre les collections et les flux est la façon dont ils gèrent l’itération sur les données.

4.3.2. Itération externe ou interne

L’utilisation de l’interface de Collection nécessite une itération par l’utilisateur (par exemple, en utilisant for-each) ; c’est ce qu’on appelle l’itération externe. La bibliothèque Streams utilise en revanche une itération interne : elle effectue l’itération pour vous et prend soin de stocker la valeur de flux résultante quelque part ; vous fournissez simplement une fonction disant ce qui doit être fait. Les figures de code suivantes illustrent cette différence.

Notez que le for-each cache une partie de la complexité de l’itération. La construction for-each est une astuce syntactique qui se traduit par quelque chose de beaucoup plus laid en utilisant un objet Iterator.

Utilisons une analogie pour comprendre les différences et les avantages de l’itération interne. Disons que vous parlez à votre fille de deux ans, Sofia, et que vous voulez qu’elle range ses jouets :

  • Vous : « Sofia, ranges tes jouets. Y a-t-il un jouet par terre ?
    • Sofia : « Oui, le ballon. »
    • Vous : « Ok, mets la balle dans la boîte. Y a-t-il autre chose ?
    • Sofia : « Oui, il y a ma poupée. »
    • Vous : « Ok, mets la poupée dans la boîte. Y a-t-il autre chose ?
    • Sofia : « Oui, il y a mon livre. »
    • Vous : « Ok, mets le livre dans la boîte. Y a-t-il autre chose ?
    • Sofia : « Non, rien d’autre. »
    • Vous : « Bien, nous avons fini. »

C’est exactement ce que vous faites tous les jours avec vos collections Java. Vous parcourez une collection en externe, en tirant explicitement et en traitant les éléments un par un. Il serait bien mieux de dire à Sofia : « Mets tous les jouets qui se trouvent à l’intérieur de la boîte». Il y a deux autres raisons pour lesquelles une itération interne est préférable : d’abord, Sofia pourrait choisir de prendre en même temps la poupée avec une main et la balle avec l’autre, et ensuite, elle pourrait décider de prendre les objets les plus proches de la boîte d’abord, puis les autres. De la même façon, en utilisant une itération interne, le traitement des éléments peut être effectué de manière transparente en parallèle ou dans un ordre différent qui peut être plus optimisé. Ces optimisations sont difficiles si vous faites une itération externe de la collection comme vous le faisiez en Java. Cela peut sembler un peu difficile à comprendre au début, mais c’est la raison d’être de l’introduction des flux par Java 8 – l’itération interne dans la bibliothèque Streams peut automatiquement choisir une représentation des données et une mise en œuvre du parallélisme correspondant à votre matériel. En revanche, une fois que vous avez choisi l’itération externe en écrivant pour chacun, vous vous êtes essentiellement engagé à gérer tout le parallélisme. (Autogestion dans la pratique signifie soit « un beau jour, nous allons paralléliser cela » ou « commencer la longue et difficile bataille impliquant des tâches synchronisées(Synchronized)».) Java 8 avait besoin d’une interface comme Collection mais sans itérateurs, d’où l’interface Stream ! La figure 4.4 illustre la différence entre un flux (itération interne) et une collection (itération externe).

Nous avons décrit les différences conceptuelles entre les collections et les flux. Plus précisément, les flux utilisent l’itération interne : l’itération est prise en charge pour vous. Mais ceci n’est utile que si vous avez une liste d’opérations prédéfinies pour travailler (par exemple, un filtre ou un mapping) qui masquent l’itération. La plupart de ces opérations utilisent des expressions lambda comme arguments pour pouvoir paramétrer leur comportement comme nous l’avons montré dans le chapitre précédent. Les concepteurs du langage Java ont livré l’API Streams avec une liste étendue d’opérations que vous pouvez utiliser pour exprimer des requêtes complexes de traitement de données. Nous allons examiner brièvement cette liste d’opérations maintenant et les explorer plus en détail avec des exemples dans le chapitre suivant.

4.4. Opérations Streams

L’interface Stream dans java.util.stream.Stream définit de nombreuses opérations. Elles peuvent être classés en deux catégories. Regardons notre exemple précédent une fois de plus :

Vous pouvez voir deux groupes d’opérations :

  • filter, map et limit peuvent être connectés ensemble pour former un pipeline.
    collect provoque l’exécution du pipeline et le ferme.

Les opérations de flux pouvant être connectées sont appelées opérations intermédiaires et les opérations de fermeture de flux sont appelées opérations terminales. La figure 4.5 met en évidence ces deux groupes. Alors, pourquoi la distinction est-elle importante ?

4.4.1. Opérations intermédiaires

Les opérations intermédiaires telles que filtre ou tri retournent un autre flux en tant que type de retour. Cela permet aux opérations d’être connectées pour former une requête. Ce qui est important, c’est que les opérations intermédiaires n’effectuent aucun traitement jusqu’à ce qu’une opération terminale soit invoquée sur le pipeline de flux. En effet, les opérations intermédiaires peuvent généralement être fusionnées et traitées comme une seule instruction par l’opération terminale.

Pour comprendre ce qui se passe dans le pipeline de flux, modifiez le code afin que chaque lambda imprime également le plat en cours de traitement (comme de nombreuses techniques de démonstration et de débogage, c’est un style de programmation épouvantable pour le code de production) :

 

Ce code lors de l’exécution imprimera ce qui suit:

Vous pouvez remarquer plusieurs optimisations en raison de la nature paresseuse des flux. Tout d’abord, malgré le fait que de nombreux plats ont plus de 300 calories, seuls les trois premiers sont sélectionnés ! C’est à cause de l’opération limit et d’une technique appelée short-circuiting, comme nous l’expliquerons dans le chapitre suivant. Deuxièmement, malgré le fait que filter et map sont deux opérations distinctes, elles ont été fusionnées dans la même instruction (cette technique est appelée loop-fusion).

4.4.2. Opérations terminales

Les opérations terminales produisent un résultat d’un pipeline de flux. Un résultat est une valeur nonstream tel qu’une liste, un entier, ou même void. Par exemple, dans le pipeline suivant, for-each est une opération terminale qui renvoie void et applique une expression lambda à chaque plat de la source de données. Le passage de System.out.println à for-each lui demande d’imprimer chaque plat du flux créé à partir du menu :

Pour vérifier votre compréhension des opérations intermédiaires par rapport aux opérations terminales, essayez le Questionnaire 4.1.



Questionnaire 4.1: Opérations intermédiaires et terminales

Dans le pipeline qui suit, pouvez-vous identifier les opérations intermédiaires et terminales ?

Réponse :

La dernière opération du pipeline de flux, count, renvoie un long, qui est une valeur non-Stream. C’est donc une opération terminale. Toutes les opérations précédentes, filter, distinct, limit, sont connectées et renvoient un flux. Ce sont donc des opérations intermédiaires.



4.4.3. Travailler avec des flux

En résumé, travailler avec les flux en général implique trois éléments :

  • Une source de données (telle qu’une collection) pour effectuer une requête
    • Une chaîne d’opérations intermédiaires qui forment un pipeline de flux
    • Une opération terminale qui exécute le pipeline de flux et produit un résultat

L’idée derrière un pipeline de flux est similaire au builder pattern. Dans le pattern builder, il existe une chaîne d’appels pour mettre en place une configuration (pour les flux, c’est une chaîne d’opérations intermédiaires), suivie d’un appel à une méthode build(), (pour les flux, il s’agit d’une opération de terminal).

Pour plus de commodité, les tableaux 4.1 et 4.2 récapitulent les opérations de flux intermédiaires et terminales que vous avez vues dans les exemples de code jusqu’à présent. Notez qu’il s’agit d’une liste incomplète des opérations fournies par l’API Streams ; vous verrez plusieurs autres dans le prochain chapitre.

Dans le chapitre suivant, nous détaillons les opérations de flux disponibles avec des cas d’utilisation afin de découvrir les types de requêtes que vous pouvez exprimer avec elles. Nous examinons de nombreux patterns tels que le filtrage, le découpage, la recherche, l’appariement, le mapping et la réduction, qui peuvent être utilisés pour exprimer des requêtes sophistiquées de traitement de données.

Parce que le chapitre 6 traite des collecteurs avec beaucoup de détails, la seule utilisation que ce chapitre et le suivant font de l’opération terminale collect() sur les flux est le cas particulier de collect(toList ()) qui crée une liste dont les éléments sont les mêmes comme ceux du flux auquel il est appliqué.

4.5. Résumé

Voici quelques concepts clés à retirer de ce chapitre :

  • Une Stream est une séquence d’éléments provenant d’une source qui prend en charge les opérations de traitement de données.
  • Les flux utilisent l’itération interne : l’itération est rendue abstraite lors de l’utilisation des opérations telles que filtre, mapping et le tri.
  • Il existe deux types d’opérations de flux : les opérations intermédiaires et les opérations terminales.
  • Les opérations intermédiaires telles que le filtre et le mapping renvoient un flux et peuvent être chaînées ensemble. Elles sont utilisées pour mettre en place un pipeline d’opérations, mais ne produisent aucun résultat.
  • Les opérations terminales telles que for-Each et count renvoient une valeur nonstream et traitent un pipeline de flux pour renvoyer un résultat.
  • Les éléments d’un flux sont calculés à la demande.