Chapitre 5. Travailler avec des flux
Ce chapitre couvre :
• Filtrage, découpage et assortiment
• Trouver, assortir et réduire
• Utiliser des flux numériques tels que des plages de nombres
• Création de flux provenant de plusieurs sources
• Flux infinis
Dans le chapitre précédent, vous avez vu que les flux vous permettent de passer d’une itération externe à une itération interne. Au lieu d’écrire du code comme suit, lorsque vous gérez explicitement l’itération sur une collection de données (itération externe),
Vous pouvez utiliser l’API Streams (itération interne), qui prend en charge les opérations de filtrage et de collecte, pour gérer l’itération sur la collecte de données pour vous. Tout ce que vous avez à faire est de passer le comportement du filtre comme argument à la méthode de filtre :
Cette façon différente de travailler avec les données est meilleure car vous laissez à l’API Streams le soin de gérer le traitement des données. En conséquence, l’API Streams peut effectuer plusieurs optimisations dans les coulisses. De plus, en utilisant l’itération interne, l’API Streams peut décider d’exécuter votre code en parallèle. L’utilisation d’une itération externe n’est pas possible, car vous vous engagez à effectuer une itération séquentielle à un seul thread.
Dans ce chapitre, vous aurez un aperçu approfondi des différentes opérations prises en charge par l’API Streams. Ces opérations vous permettent d’exprimer des requêtes de traitement de données complexes telles que le filtrage, le découpage, le mapping, la recherche, l’appariement et la réduction. Ensuite, nous explorerons des cas particuliers de flux: les flux numériques, les flux construits à partir de sources multiples telles que les fichiers et les tableaux, et enfin les flux infinis.
5.1. Filtre et troncature
Dans cette section, nous examinons comment sélectionner les éléments d’un flux : filtrer avec un prédicat, filtrer seulement des éléments uniques, ignorer les premiers éléments d’un flux ou tronquer un flux à une taille donnée.
5.1.1. Filtre avec un prédicat
L’interface Streams prend en charge une méthode de filtre (que vous devriez connaître maintenant). Cette opération prend comme argument un prédicat (une fonction retournant un booléen) et retourne un flux incluant tous les éléments qui correspondent au prédicat. Par exemple, vous pouvez créer un menu végétarien en filtrant tous les plats végétariens comme suit et comme illustré à la figure 5.1:
5.1.2. Filtre d’éléments uniques
Les flux supportent également une méthode appelée distinct qui renvoie un flux avec des éléments uniques (selon la mise en œuvre du hashCode et des méthodes equal des objets produits par le flux). Par exemple, le code suivant filtre tous les nombres pairs d’une liste et s’assure qu’il n’y a pas de doublons. La figure 5.2 montre ceci visuellement :
5.1.3. Tronquer un flux
Les flux prennent en charge la méthode limit(n), qui renvoie un autre flux qui ne dépasse pas une taille donnée. La taille demandée est passée en argument à la méthode limit(). Si le flux est ordonné, les premiers éléments sont retournés jusqu’à un maximum de n. Par exemple, vous pouvez créer une liste en sélectionnant les trois premiers plats ayant plus de 300 calories comme suit :
Mettez en pratique ce que vous avez appris dans cette section avec le Quiz 5.1 avant de passer aux opérations de mapping.
Quiz 5.1: Filtrage
Comment utiliseriez-vous les flux pour filtrer les deux premiers plats de viande ?
Réponse :
Vous pouvez résoudre ce problème en composant les méthodes filter et limit ensemble et en utilisant collect (toList ()) pour convertir le flux en une liste comme suit :
5.2. Mapping
Un idiome de traitement de données très commun est de sélectionner des informations provenant de certains objets. Par exemple, dans SQL, vous pouvez sélectionner une colonne particulière dans une table. L’API Streams fournit des fonctionnalités similaires à travers les méthodes map et flatMap.
5.2.1. Application d’une fonction à chaque élément d’un flux
Les flux supportent la méthode map, qui prend une fonction comme argument. La fonction est appliquée à chaque élément, le transformant en un nouvel élément. Par exemple, dans le code suivant, vous passez une référence de méthode Dish::getName à la méthode map pour extraire les noms des plats dans le flux :
Comme la méthode getName renvoie une String, le flux sorti par la méthode map est de type Stream <String>.
Prenons un exemple légèrement différent pour solidifier votre compréhension du mapping. Avec une liste de mots, vous souhaitez renvoyer une liste du nombre de caractères pour chaque mot. Comment les feriez-vous ? Vous auriez besoin d’appliquer une fonction à chaque élément de la liste. Cela ressemble à un travail pour la méthode map(). La fonction à appliquer doit prendre un mot et renvoyer sa longueur. Vous pouvez résoudre ce problème en passant la référence de méthode String::length à la méthode de mapping :
Revenons maintenant à l’exemple où vous avez extrait le nom de chaque plat. Et si vous vouliez connaître la longueur du nom de chaque plat ? Vous pourriez le faire en enchaînant une autre méthode map comme ceci:
5.2.2. Flux d’aplatissement
Vous avez vu comment renvoyer la longueur de chaque mot dans une liste en utilisant la méthode. Étendons cette idée un peu plus loin : comment pourriez-vous renvoyer une liste de tous les caractères uniques pour une liste de mots ? Par exemple, étant donné la liste des mots [« Hello », « World »], vous souhaitez retourner la liste [« H », « e », « l », « o », « W », « r », « d »].
Vous pourriez penser que c’est facile, que vous pouvez simplement mapper chaque mot dans une liste de caractères, puis appeler distinct pour filtrer les caractères en double. Un premier essai pourrait donner ceci :
Le problème avec cette approche est que l’expression lambda transmise à la méthode map renvoie une String[] (un tableau de chaîne) pour chaque mot. Ainsi, le flux renvoyé par la méthode map est en fait de type Stream <String []>. Ce que vous voulez vraiment, c’est une Stream <String> représentant un flux de caractères. La figure 5.5 illustre le problème.
Heureusement, il existe une solution à ce problème en utilisant la méthode flatMap ! Voyons voir étape par étape comment le résoudre.
Essai avec map et Arrays.stream
D’abord, vous avez besoin d’un flux de caractères au lieu d’un flux de tableaux. Il existe une méthode appelée Arrays.stream () qui prend un tableau et produit un flux, par exemple :
Utilisez-le dans le pipeline précédent pour voir ce qui se passe:
La solution actuelle ne fonctionne toujours pas ! C’est parce que vous vous retrouvez maintenant avec une liste de flux (plus précisément, Stream <Stream <String >>) ! En effet, vous devez d’abord convertir chaque mot en un tableau de lettres individuelles, puis transformer chaque tableau en un flux séparé.
Utilisation de flatMap
Vous pouvez résoudre ce problème en utilisant flatMap comme ceci:
L’utilisation de la méthode flatMap a pour effet de mapper chaque tableau non pas avec un flux, mais avec le contenu de ce flux. Tous les flux séparés qui ont été générés lors de l’utilisation de map (Arrays::stream) sont fusionnés en un seul flux. La Figure 5.6 illustre l’effet de l’utilisation de la méthode flatMap. Comparez-le avec ce que fait la méthode map dans la figure 5.5.
En résumé, la méthode flatMap vous permet de remplacer chaque valeur d’un flux par un autre flux, puis de concaténer tous les flux générés en un seul flux.
Nous reviendrons sur cette méthode dans le chapitre 10 lorsque nous discuterons de pattern Java 8 plus avancés, tels que l’utilisation de la nouvelle classe Optional pour la vérification des valeurs nulles. Pour consolider votre compréhension de la map et flatMap, essayez Quiz 5.2.
Quiz 5.2: Mapping
- Étant donné une liste de nombres, comment retourneriez-vous une liste du carré de chaque nombre ? Par exemple, étant donné [1, 2, 3, 4, 5] vous devriez retourner [1, 4, 9, 16, 25].
Réponse :
Vous pouvez résoudre ce problème en utilisant map avec un lambda qui prend un nombre et retourne le carré du nombre :
- Etant donné deux listes de nombres, comment renverrais-tu tous les binômes possibles ? Par exemple, si vous avez une liste [1, 2, 3] et une liste [3, 4], vous devez retourner [(1, 3), (1, 4), (2, 3), (2, 4) 3, 3), (3, 4)]. Pour plus de simplicité, vous pouvez représenter une paire sous forme de tableau avec deux éléments.
Réponse :
Vous pouvez utiliser deux map pour parcourir les deux listes et générer les paires. Mais cela renverrait un flux <Stream <Integer [] >>. Ce que vous devez faire est d’aplatir les flux générés pour obtenir une Stream<Integer []>. C’est ce que FlatMap nous permet de faire :
- Comment étendriez-vous l’exemple précédent pour renvoyer seulement les paires dont la somme est divisible par 3 ? Par exemple, (2, 4) et (3, 3) sont valides.
Réponse :
Vous avez vu plus tôt que le filtre peut être utilisé avec un prédicat pour filtrer les éléments d’un flux. Etant donné qu’après l’opération flatMap vous obtenez un flux de int [] qui représente une paire, vous avez juste besoin d’un prédicat pour vérifier si la somme est divisible par 3 :
Le résultat est [(2, 4), (3, 3)].
5.3. Trouver et assortir
Un autre idiome commun de traitement de données consiste à déterminer si certains éléments d’un ensemble de données correspondent à une propriété donnée. L’API Streams fournit de telles fonctionnalités via les méthodes allMatch, anyMatch, noneMatch, findFirst et findAny.
5.3.1. Vérification pour voir si un prédicat correspond à au moins un élément
La méthode anyMatch peut être utilisée pour répondre à la question « Y at-il un élément dans le flux correspondant au prédicat donné ? » Vous pouvez par exemple l’utiliser pour savoir si dans le menu, il y a des plats qui sont végétariens.
La méthode anyMatch renvoie un booléen et est donc une opération terminale.
5.3.2. Vérification pour voir si un prédicat correspond à tous les éléments
La méthode allMatch fonctionne de la même façon que anyMatch mais vérifie si tous les éléments du flux correspondent au prédicat donné. Par exemple, vous pouvez l’utiliser pour savoir si le menu est sain (c’est-à-dire que tous les plats sont en dessous de 1000 calories) :
Le contraire de allMatch est noneMatch. Cela garantit qu’aucun élément du flux ne correspond au prédicat donné. Par exemple, vous pouvez réécrire l’exemple précédent comme suit à l’aide de noneMatch :
Ces trois opérations, anyMatch, allMatch et noneMatch, utilisent ce que nous appelons short-circuiting, une version de flux du fameux court-circuit en Java pour les opérateurs && et ||.
Évaluation en court-circuit
Certaines opérations n’ont pas besoin de traiter le flux entier pour produire un résultat. Par exemple, supposons que vous devez évaluer une expression booléenne assez large et enchaînée uniquement avec des opérateurs and. Il suffit q’une expression soit fausse pour déduire que l’expression entière retournera faux, peu importe à quel point l’expression est longue ; il n’est pas nécessaire d’évaluer l’expression entière. C’est ça le short-circuiting.
En ce qui concerne les flux, certaines opérations telles que allMatch, noneMatch, findFirst et findAny n’ont pas besoin de traiter le flux entier pour produire un résultat. Dès qu’un élément est trouvé, un résultat peut être produit. De même, limit est aussi une opération de court-circuit : l’opération n’a besoin de créer qu’un flux d’une taille donnée sans traiter tous les éléments. De telles opérations sont utiles, par exemple, lorsque vous devez gérer des flux de taille infinie, car ils peuvent transformer un flux infini en un flux de taille finie. Nous montrerons des exemples de flux infinis dans la section 5.7.
5.3.3. Trouver un élément
La méthode findAny renvoie un élément arbitraire du flux actuel. Il peut être utilisé conjointement avec d’autres opérations de flux. Par exemple, vous chercher un plat végétarien. Vous pouvez combiner la méthode filter et findAny pour exprimer cette requête :
Le pipeline de flux sera optimisé dans les coulisses pour effectuer une seule passe et terminer dès qu’un résultat est trouvé en utilisant un court-circuit. Mais attendez une minute ; Quelle est cette chose Optional dans le code vous dites-vous ?
La classe Optional<T> (java.util.Optional) est une classe de conteneur pour représenter l’existence ou l’absence d’une valeur. Dans le code précédent, il est possible que findAny ne trouve aucun élément. Au lieu de renvoyer null, ce qui est bien connu pour être source d’erreurs, les concepteurs de la librairie Java 8 ont introduit Optional <T>. Nous n’entrerons pas dans les détails ici car nous verrons en détail dans le chapitre 10 comment votre code peut bénéficier de l’utilisation d’Optional pour éviter les bogues liés à la vérification de null. Mais pour l’instant, il est bon de savoir qu’il existe quelques méthodes disponibles dans Optional qui vous forcent à vérifier explicitement la présence d’une valeur ou à traiter l’absence d’une valeur :
- isPresent () renvoie true si Optional contient une valeur, false dans le cas contraire.
• ifPresent (Bloc Consumer <T>) exécute le bloc donné si une valeur est présente. Nous avons introduit l’interface fonctionnelle Consumer dans le chapitre 3 ; il vous laisse passer une expression lambda qui prend un argument de type T et renvoie void.
• T get() renvoie la valeur si elle est présente ; sinon, il lance une NoSuchElementException.
• T orElse(T other) renvoie la valeur si elle est présente; sinon, il renvoie une valeur par défaut.
Par exemple, dans le code précédent, vous devez vérifier explicitement la présence d’un plat dans l’objet Optional pour accéder à son nom :
5.3.4. Trouver le premier élément
Certains flux ont un ordre de rencontre qui spécifie l’ordre dans lequel les éléments apparaissent logiquement dans le flux (par exemple, un flux généré à partir d’une liste ou d’une séquence de données triée). Pour de tels flux, vous souhaiterez peut-être trouver le premier élément. Il y a la méthode findFirst pour cela, qui fonctionne de manière similaire à findAny. Par exemple, le code suivant, avec une liste de nombres, trouve le premier carré divisible par 3 :
Quand utiliser findFirst et findAny?
Vous pouvez vous demander pourquoi nous avons à la fois findFirst et findAny. La réponse est le parallélisme. Trouver le premier élément est plus contraignant en parallèle. Si vous ne vous souciez pas de l’élément renvoyé, utilisez findAny car il est moins contraignant lors de l’utilisation de flux parallèles.
5.4. Réduire
Jusqu’à présent, les opérations terminales que vous avez vues renvoient une valeur booléenne (allMatch et ainsi de suite), void (forEach) ou un objet Optional (findAny et ainsi de suite). Vous avez également utilisé collect pour combiner tous les éléments d’un flux dans une liste.
Dans cette section, vous verrez comment vous pouvez combiner des éléments d’un flux pour exprimer des requêtes plus complexes telles que « Calculer la somme de toutes les calories dans le menu » ou « Quel est le plat le plus calorique du menu ? » en utilisant l’opération reduce. Ces requêtes combinent tous les éléments du flux de manière répétée pour produire une valeur unique telle qu’un entier. Ces requêtes peuvent être classées comme des opérations de réduction (le flux est réduit à une valeur). Dans le jargon du langage de programmation fonctionnel, cela s’appelle un pli parce que vous pouvez voir cette opération comme le pliage répété d’un long morceau de papier (votre flux) jusqu’à ce qu’il forme un petit carré, qui est le résultat de l’opération de pliage.
5.4.1. Sommer les éléments
Avant d’étudier comment utiliser la méthode reduce, il est utile de voir d’abord comment vous additionneriez les éléments d’une liste de nombres à l’aide d’une boucle for-each :
Chaque élément de la liste est combiné itérativement avec l’opérateur d’addition pour former un résultat. Vous réduisez la liste des nombres en un nombre en utilisant de manière répétée l’opération d’addition. Il y a deux paramètres ici :
- La valeur initiale de la variable sum, dans ce cas 0
- L’opération utilisée, pour combiner tous les éléments de la liste, dans ce cas +
Ne serait-il pas génial si vous pouviez également multiplier tous les nombres sans avoir à copier et coller ce code à plusieurs reprises ? C’est là que l’opération reduce, qui résume ce pattern d’opération répétée, peut aider. Vous pouvez sommer tous les éléments d’un flux comme suit :
Reduce prend deux arguments :
- Une valeur initiale, ici 0.
• Un BinaryOperator <T> pour combiner deux éléments et produire une nouvelle valeur ; ici vous utilisez l’expression lambda (a, b) -> a + b.
Vous pouvez tout aussi bien multiplier tous les éléments en passant une expression lambda différente, (a, b) -> a * b, à l’opération de réduction :
La figure 5.7 illustre comment l’opération reduce fonctionne sur un flux : l’expression combine chaque élément à plusieurs reprises jusqu’à ce que le flux soit réduit à une seule valeur.
Jetons un regard en profondeur sur la façon dont l’opération reduce arrive à sommer un flux de nombres. Tout d’abord, 0 est utilisé comme premier paramètre de l’expression lambda (a), et 4 est consommé dans le flux et utilisé comme deuxième paramètre (b). 0 + 4 produit 4, et devient la nouvelle valeur accumulée. Ensuite, l’expression lambda est appelée à nouveau avec la valeur accumulée et l’élément suivant du flux, 5, qui produit la nouvelle valeur accumulée 9. En se déplaçant vers l’avant, l’expression est appelée à nouveau avec la valeur accumulée et l’élément suivant 3 qui produit 12. Enfin, l’expression est appelée avec 12 et le dernier élément du flux, 9, qui produit la valeur finale, 21.
Vous pouvez rendre ce code plus concis en utilisant une référence de méthode. Dans Java 8, la classe Integer est maintenant fournie avec une méthode de somme statique pour ajouter deux nombres :
Il existe également une variante de réduction surchargée qui ne prend pas de valeur initiale, mais renvoie un objet Optional :
Pourquoi renvoie-t-il une Optional<Integer> ? Considérez le cas où le flux ne contient aucun élément. L’opération de réduction ne peut pas renvoyer une somme car elle n’a pas de valeur initiale. C’est pourquoi le résultat est enveloppé dans un objet Optional pour indiquer que la somme peut être absente. Maintenant, voyez ce que vous pouvez faire d’autre avec reduce.
5.4.2. Maximum et minimum
Il s’avère que la réduction est tout ce dont vous avez besoin pour calculer les maximums et minimums aussi. Voyons comment vous pouvez appliquer ce que vous venez de découvrir sur la réduction pour calculer l’élément maximum ou minimum d’un flux. Comme vous l’avez vu, la réduction prend deux paramètres :
- Une valeur initiale
• Une lambda pour combiner deux éléments de flux et produire une nouvelle valeur
L’expression lambda est appliquée pas à pas à chaque élément du flux avec l’opérateur d’addition, comme illustré sur la figure 5.7. Vous avez donc besoin d’une expression lambda qui, compte tenu de deux éléments, renvoie le maximum d’entre eux. L’opération reduce utilisera la nouvelle valeur avec l’élément suivant du flux pour produire un nouveau maximum jusqu’à ce que tout le flux soit consommé. Vous pouvez utiliser reduce comme suit pour calculer le maximum dans un flux ; ceci est illustré dans la figure 5.8:
Pour calculer le minimum, vous devez passer Integer.min à l’opération reduce au lieu de Integer.max :
Vous auriez pu aussi bien utiliser le lambda (x, y) -> x <y ? X : y au lieu de Integer :: min, mais ce dernier est plus facile à lire.
Pour tester votre compréhension de l’opération de réduction, essayez le Quiz 5.3.
Quiz 5.3: Réduire
Comment compteriez-vous le nombre de plats dans un cours en utilisant les méthodes map et reduce ?
Réponse :
Vous pouvez résoudre ce problème en mappant chaque élément d’un flux dans le numéro 1 puis en les additionnant en utilisant reduce. Cela équivaut à compter dans l’ordre le nombre d’éléments dans le flux.
Une chaîne de map et reduce est communément connu comme le pattern map-reduce, rendu célèbre par l’utilisation de Google pour la recherche sur le web, car il peut être facilement parallélisé. Notez que dans le chapitre 4, vous avez vu le nombre de méthodes intégré pour compter le nombre d’éléments dans un flux :
Avantage de la méthode reduce et du parallélisme
L’avantage de l’utilisation de reduce par rapport à la sommation d’itération pas à pas que vous avez écrite précédemment est que l’itération est abstraite en utilisant une itération interne, ce qui permet à l’implémentation interne de choisir d’effectuer l’opération de réduction en parallèle. L’exemple de sommation itérative implique des mises à jour partagées d’une variable de somme, qui ne se parallélise pas gracieusement. Si vous ajoutez la synchronisation nécessaire, vous découvrirez probablement que la contention des threads vous prive de toutes les performances que le parallélisme était censé vous donner. Paralléliser ce calcul nécessite une approche différente : partitionner l’entrée, additionner les partitions et combiner les sommes. Mais maintenant, le code commence à être vraiment différent. Vous verrez à quoi cela ressemble dans le chapitre 7 en utilisant le framework fork / join. Mais pour l’instant, il est important de réaliser que le modèle d’accumulation mutable est une impasse pour la parallélisation. Vous avez besoin d’un nouveau modèle, et c’est ce que reduce vous offre. Vous verrez aussi dans le chapitre 7 que pour sommer tous les éléments en parallèle à l’aide de flux, il n’y a presque pas de modification à faire dans votre code : stream() devient parallelStream():
Mais il y a un prix à payer pour exécuter ce code en parallèle, comme nous l’expliquons plus tard : l’expression lambda passée à reduce ne peut pas changer d’état (par exemple, les variables d’instance) et l’opération doit être associative pour pouvoir être exécuté dans n’importe quel ordre.
Jusqu’à présent, vous avez vu des exemples de réduction qui ont produit un entier : la somme d’un flux, le maximum d’un flux ou le nombre d’éléments dans un flux. Vous verrez dans la section 5.6 que des méthodes intégrées telles que sum et max sont également disponibles pour vous aider à écrire un code un peu plus concis pour les modèles de réduction courants. Nous étudierons une forme de réduction plus complexe en utilisant la méthode collect dans le chapitre suivant. Par exemple, au lieu de réduire en un flux d’Integer, vous pouvez également le réduire en une Map si vous souhaitez regrouper les plats par types.
Opérations de flux: stateless vs. stateful
Vous avez vu beaucoup d’opérations sur les flux. Tout fonctionne plûtot simplement, et vous obtenez le parallélisme gratuitement lorsque vous utilisez parallelStream au lieu de stream.
C’est certainement le cas pour de nombreuses applications, comme vous l’avez vu dans les exemples précédents. Vous pouvez transformer une liste de plats en un flux, filtrer pour sélectionner différents plats d’un certain type, puis mapper le flux résultant obtenir le nombre de calories, puis réduire afin de produire le nombre total de calories du menu. Vous pouvez même faire de tels calculs de flux en parallèle. Mais ces opérations ont des caractéristiques différentes. Il y a des problèmes concernant l’état interne dont elles ont besoin pour fonctionner.
Les opérations comme map()et filter() prennent chaque élément du flux d’entrée et produisent zéro ou un résultat dans le flux de sortie. Ces opérations sont donc en général sans état : elles n’ont pas d’état interne (en supposant que la référence lambda ou la référence de méthode fournie par l’utilisateur n’a pas d’état mutable interne).
Mais les opérations comme reduce, sum et max doivent avoir un état interne pour accumuler le résultat. Dans ce cas, l’état interne est petit. Dans notre exemple, il s’agissait d’un int ou d’un double. L’état interne est de taille limitée, quel que soit le nombre d’éléments dans le flux en cours de traitement.
En revanche, certaines opérations telles que sort()ou distinct() semblent d’abord se comporter comme un filter ou map – toutes prennent un flux et produisent un autre flux (une opération intermédiaire), mais il y a une différence cruciale. Le tri et la suppression des doublons d’un flux nécessitent de connaître l’historique précédent pour faire leur travail. Par exemple, le tri nécessite que tous les éléments soient tamponnés avant qu’un seul élément puisse être ajouté au flux de sortie ; l’exigence de stockage de l’opération est illimitée. Cela peut être problématique si le flux de données est grand ou infini. On dit que ces opérations sont Statefull.
Vous avez maintenant vu beaucoup d’opérations de flux que vous pouvez utiliser pour exprimer des requêtes sophistiquées de traitement de données. Le tableau 5.1 résume les opérations observées jusqu’à présent. Vous pourrez les pratiquer dans la section suivante à travers un exercice.
5.5. Tout mettre en pratique
Dans cette section, vous pratiquerez sur tout ce que vous avez appris sur les Streams jusqu’à maintenant. Nous donnons un domain différent : les traders exécutant des transactions. Votre manager vous demande de trouver des réponses à huit requêtes. Pouvez-vous le faire ? Nous donnons les solutions dans la section 5.5.2, mais vous devriez d’abord les essayer vous-même pour vous entraîner.
- Trouvez toutes les transactions en 2011 et triez-les en fonction de leur valeur (petite à haute).
- Quelles sont toutes les villes distinctes où les commerçants travaillent ?
- Trouver tous les commerçants de Cambridge et les trier par nom.
- Renvoie une String des noms de tous les traders triés par ordre alphabétique.
- Y a-t-il des commerçants basés à Milan ?
- Imprimer toutes les valeurs des transactions des commerçants vivant à Cambridge.
- Quelle est la valeur la plus élevée de toutes les transactions ?
- Trouvez la transaction avec la plus petite valeur.
5.5.1. Le domaine : Traders and Transactions
Voici le domaine avec lequel vous allez travailler, une liste de traders et de transactions :
Les classes Traders et Transactions sont définies comme ceci:
5.5.2. Solutions
Nous fournissons maintenant les solutions dans les figures de codes suivantes, afin que vous puissiez vérifier votre compréhension de ce que vous avez appris jusqu’à présent :
Vous ne l’avez pas encore vu, mais vous pouvez également supprimer distinct() et utiliser toSet() à la place, ce qui convertira le flux en Set. Vous en apprendrez plus à ce sujet au chapitre 6.
Notez que cette solution n’est pas très efficace (toutes les chaînes sont concaténées à plusieurs reprises, ce qui crée un nouvel objet String à chaque itération). Dans le chapitre suivant, vous verrez une solution plus efficace utilisant join() comme suit (qui utilise en interne un StringBuilder) :
Vous pouvez faire mieux. Un flux prend en charge les méthodes min et max qui utilisent un comparateur comme argument pour spécifier la clé à comparer lors du calcul du minimum ou du maximum :
5.6. Flux numériques
Vous avez vu précédemment que vous pouviez utiliser la méthode reduce pour calculer la somme des éléments d’un flux. Par exemple, vous pouvez calculer le nombre de calories dans le menu comme suit :
Le problème avec ce code est qu’il y a un coût d’auto boxing qui est non négligeable. Dans les coulisses, chaque Integer doit être converti en une primitive avant d’effectuer la sommation. En outre, ne serait-il pas plus agréable si vous pouviez appeler une méthode de somme directement comme ceci?
Mais ce n’est pas possible. Le problème est que la méthode map() génère une Stream<T>. Même si les éléments du flux sont de type Integer, l’interface Streams ne définit pas une méthode sum. Pourquoi pas ? Disons que vous aviez une Stream<Dish> ; cela n’aurait aucun sens de pouvoir additionner des plats. Mais ne vous inquiétez pas, l’API Streams fournit également des spécialisations de flux primitifs qui prennent en charge des méthodes spécialisées pour travailler avec des flux de nombres.
5.6.1. Spécialisations de flux primitifs
Java 8 introduit trois interfaces de flux spécialisées pour les primitives afin de résoudre ce problème, IntStream, DoubleStream et LongStream, qui permettent respectivement de spécialiser les éléments d’un flux en int, long et double, évitant ainsi les coûts cachés de l’auto boxing. Chacune de ces interfaces apporte de nouvelles méthodes pour effectuer des réductions numériques communes telles que sum pour calculer la somme d’un flux numérique et max pour trouver l’élément maximum. En outre, ils ont des méthodes pour reconvertir à un flux d’objets si nécessaire.
Mapping vers un flux numérique
Les méthodes les plus courantes que vous utiliserez pour convertir un flux en une version spécialisée sont mapToInt, mapToDouble et mapToLong. Ces méthodes fonctionnent exactement comme map() que vous avez vue précédemment, mais retournent un flux spécialisé au lieu d’un Stream<T>. Par exemple, vous pouvez utiliser mapToInt comme suit pour calculer la somme des calories dans le menu :
Ici, la méthode mapToInt extrait toutes les calories de chaque plat (représentée par un Integer) et retourne un IntStream comme résultat (plutôt qu’un Stream <Integer>). Vous pouvez ensuite appeler la méthode de sum définie sur l’interface IntStream pour calculer la somme des calories. Notez que si le flux était vide, la somme retournerait 0 par défaut. IntStream prend également en charge d’autres méthodes pratiques telles que max, min et average.
Conversion en un flux d’objets
De même, une fois que vous avez un flux numérique, vous pouvez être intéressé par le convertir à un flux non spécialisé. Par exemple, les opérations d’un IntStream sont restreintes pour produire des entiers primitifs : l’opération de mapping d’un IntStream prend une expression lambda qui prend elle-même un int et produit un int (IntUnaryOperator). Mais vous pouvez vouloir produire une valeur différente telle qu’un plat. Pour cela, vous devez accéder aux opérations définies dans l’interface Streams qui sont plus générales. Pour convertir un flux primitif en un flux général (chaque entier sera wrappé dans un Integer), vous pouvez utiliser la méthode boxed suivante :
Vous allez apprendre dans la section suivante que boxed() est particulièrement utile lorsque vous traitez des plages numériques qui doivent être encapsulées dans un flux général.
Valeurs par défaut : OptionalInt
L’exemple de somme était pratique car il a une valeur par défau t: 0. Mais si vous voulez calculer l’élément maximum dans un IntStream, vous avez besoin de quelque chose de différent parce que 0 est un mauvais résultat. Comment pouvez-vous différencier que le flux n’a pas d’élément et que le maximum réel est 0 ? Plus tôt, nous avons introduit la classe Optional, qui est un conteneur qui indique la présence ou l’absence d’une valeur. Optional peut être paramétré avec des types de référence tels que Integer, String, etc. Il existe également une version spécialisée pour les trois spécialisations de flux primitives : OptionalInt, OptionalDouble et OptionalLong.
Par exemple, vous pouvez trouver l’élément maximal d’un IntStream en appelant la méthode max, qui renvoie un OptionalInt :
Vous pouvez maintenant traiter explicitement OptionalInt pour définir une valeur par défaut s’il n’y a pas de maximum :
5.6.2. Plage numériques
Un cas d’utilisation courant lorsque vous traitez avec des nombres est de travailler avec des plages de valeurs numériques. Par exemple, supposons que vous souhaitiez générer tous les nombres compris entre 1 et 100. Java 8 présente deux méthodes statiques disponibles sur IntStream et LongStream pour vous aider à générer ces plages : range et rangeClosed. Les deux méthodes prennent la valeur initiale de la plage comme premier paramètre et la valeur finale de la plage comme deuxième paramètre. Mais range est exclusif, alors que rangeClosed est inclusif. Regardons un exemple :
Ici, vous utilisez la méthode rangeClosed pour générer une plage de tous les nombres de 1 à 100. Elle produit un flux de sorte que vous pouvez la chaîner avec la méthode de filtrage pour sélectionner uniquement les nombres pairs. À ce stade, aucun calcul n’a été fait. Enfin, vous appelez la méthode count sur le flux résultant. Parce que count est une opération terminale, elle traitera le flux et renverra le résultat 50, qui est le nombre de nombres pairs compris entre 1 et 100 inclus. Notez que par comparaison, si vous utilisiez IntStream.range (1, 100) à la place, le résultat serait de 49 nombres pairs car la plage est exclusive.
5.6.3. Mise en pratique des flux numériques : les triplets de Pythagore
Nous examinons maintenant un exemple plus difficile pour vous permettre de consolider ce que vous avez appris sur les flux numériques et toutes les opérations de flux que vous avez apprises jusqu’ici. Votre mission, si vous choisissez de l’accepter, est de créer un flux de triplets de Pythagore. Vous trouverez le code sur mon repo github
Alors qu’est-ce qu’un triplet de Pythagore ? Nous devons retourner quelques années dans le passé. Dans l’une de vos passionnantes classes de mathématiques, vous avez appris que le célèbre mathématicien grec Pythagore a découvert que certains triplets de nombres (a, b, c) satisfont à la formule a * a + b * b = c * c où a, b et c sont des entiers. Par exemple, (3, 4, 5) est un triple de Pythagore valide parce que 3 * 3 + 4 * 4 = 5 * 5 ou 9 + 16 = 25. Il y a un nombre infini de tels triplets. Par exemple, (5, 12, 13), (6, 8, 10) et (7, 24, 25) sont tous des triplets de Pythagore valides. De tels triplets sont utiles car ils décrivent les trois côtés d’un triangle rectangle, comme l’illustre la figure 5.9.
Alors, par où allez-vous commencer ? La première étape consiste à définir un triplet. Au lieu de (plus correctement) définir une nouvelle classe pour représenter un triple, vous pouvez utiliser un tableau de int avec trois éléments, par exemple, new int [] {3, 4, 5} pour représenter le tuple (3, 4, 5). Vous pouvez maintenant accéder à chaque composant individuel du tuple en utilisant l’indexation de tableau.
Filtrer les bonnes combinaisons
Supposons que quelqu’un vous fournisse les deux premiers nombres du triple : a et b. Comment savez-vous s’ils vont former une bonne combinaison ? Vous devez tester si la racine carrée d’un a*a + b*b est un nombre entier ; c’est-à-dire qu’il n’ait pas de partie fractionnelle. Ce qui peut être exprimée en Java en utilisant expr% 1.0. Si ce n’est pas le cas, cela signifie que c n’est pas un nombre entier. Vous pouvez exprimer cette exigence comme une opération de filtrage (vous verrez comment la connecter plus tard pour former un code valide) :
En supposant que le code environnant a donné une valeur pour a et supposant que le flux fournit des valeurs possibles pour b, le filtre sélectionnera uniquement les valeurs pour b qui peuvent former un triplet de Pythagore avec a. Vous vous demandez peut-être à quoi correspond la ligne Math.sqrt (a * a + b * b)% 1 == 0. C’est fondamentalement un moyen de tester si Math.sqrt (a * a + b * b) retourne un résultat entier. La condition échouera si le résultat de la racine carrée produit un nombre avec un nombre décimal tel que 9.1 (9.0 est valide).
Générer des tuples
Après le filtre, vous savez que a et b peuvent former une combinaison correcte. Vous devez maintenant créer un triple. Vous pouvez utiliser l’opération map pour transformer chaque élément en un triple de Pythagore comme ceci:
Générer des valeurs b
Vous vous rapprochez. Vous devez maintenant générer des valeurs pour b. Vous avez vu que Stream.rangeClosed vous permet de générer un flux de nombres dans un intervalle donné. Vous pouvez l’utiliser pour fournir des valeurs numériques pour b, ici de 1 à 100 :
Notez que vous appelez boxed () après le filtre pour générer un flux <Integer> à partir de IntStream retourné par rangeClosed. C’est parce que la méthode map retourne un tableau d’int pour chaque élément du flux. La méthode map d’un IntStream ne peut retourner q’un autre int pour chaque élément du flux, ce qui n’est pas ce que vous voulez. Vous pouvez réécrire ceci en utilisant la méthode mapToObj d’un IntStream, qui retourne un flux de valeurs d’objets :
Générer les valeurs de a
Il y a un élément crucial que nous avons supposé avéré : la valeur pour a. Vous avez maintenant un flux qui produit des triplets de Pythagore à condition que la valeur a soit connue. Comment pouvez-vous résoudre ce problème ? Tout comme avec b, vous devez générer des valeurs numériques pour a. La solution finale est la suivante :
D’accord, c’est quoi le flatmap ? D’abord, vous créez une plage numérique de 1 à 100 pour générer des valeurs pour a. Pour chaque valeur donnée de a, vous créez un flux de triplets. Mapper une valeur de a à un flux de triplets se traduirait par un flux de flux (Stream<Stream<T>>). La méthode flatMap effectue le mapping et aplatit également tous les flux de triplets générés en un seul flux. En conséquence, vous produisez un flux de triplets. Notez également que vous modifiez la plage de valeurs de b. Elle sera de a à 100. Il n’est pas nécessaire de démarrer la plage à la valeur 1 car cela créerait des triplets en double (par exemple, (3, 4, 5) et (4, 3, 5)).
Exécuter le code
Vous pouvez maintenant exécuter votre solution et sélectionner explicitement le nombre de triplets que vous souhaitez renvoyer du flux généré à l’aide de l’opération limit que vous avez déjà vue :
Ceci imprimera:
Pouvez-vous faire mieux?
La solution actuelle n’est pas optimale car vous calculez deux fois la racine carrée. Une façon possible de rendre votre code plus compact est de générer tous les triplets du formulaire (a * a, b * b, a * a + b * b) et ensuite filtrer ceux qui correspondent à vos critères :
5.7. Construire des streams
J’espère que maintenant vous êtes convaincu que les flux sont très puissants et utiles pour exprimer des requêtes de traitement de données. Jusqu’à présent, vous pouviez obtenir un flux à partir d’une collection en utilisant la méthode stream. En outre, nous vous avons montré comment créer des flux numériques à partir d’une série de nombres. Mais vous pouvez créer des flux de bien d’autres façons. Cette section montre comment vous pouvez créer un flux à partir d’une séquence de valeurs, d’un tableau, d’un fichier, et même d’une fonction générative pour créer des flux infinis.
5.7.1. Flux à partir de valeurs
Vous pouvez créer un flux avec des valeurs explicites en utilisant la méthode statique Stream.of, qui peut prendre n’importe quel nombre de paramètres. Par exemple, dans le code suivant, vous créez un flux de String directement à l’aide de Stream.of. Vous convertissez ensuite les Strings en majuscules avant de les imprimer une par une :
Vous pouvez obtenir un flux vide en utilisant la méthode empty comme ceci:
5.7.2. Stream à partir de tableaux
Vous pouvez créer un flux à partir d’un tableau à l’aide de la méthode statique Arrays.stream, qui prend un tableau en tant que paramètre. Par exemple, vous pouvez convertir un tableau de nombres primitifs en un IntStream comme ceci:
5.7.3. Streams à partir de fichiers
L’API NIO de Java (E / S non bloquantes), qui est utilisée pour les opérations d’E / S telles que le traitement d’un fichier, a été mise à jour pour tirer parti de l’API Streams. De nombreuses méthodes statiques dans java.nio.file.Files renvoient un flux. Par exemple, une méthode utile est Files.lines, qui renvoie un flux de lignes en tant que chaînes à partir d’un fichier donné. En utilisant ce que vous avez appris jusqu’à présent, vous pouvez utiliser cette méthode pour trouver le nombre de mots uniques dans un fichier comme ceci :
Vous utilisez Files.lines pour retourner un flux où chaque élément est une ligne dans le fichier donné. Vous divisez ensuite chaque ligne en mots en appelant la méthode split sur chaque ligne. Remarquez comment vous utilisez flatMap pour produire un flux de mots aplatis au lieu de plusieurs flux de mots pour chaque ligne. Enfin, vous comptez chaque mot distinct dans le flux en enchaînant les méthodes distinct et count.
5.7.4. Des flux à partir de fonctions : créer des flux infinis
L’API Streams fournit deux méthodes statiques pour générer un flux à partir d’une fonction : Stream.iterate et Stream.generate. Ces deux opérations vous permettent de créer ce que nous appelons un flux infini : un flux qui n’a pas de taille fixe comme lorsque vous créez un flux à partir d’une collection fixe. Les flux produits par itération et génération créent des valeurs à la demande à partir d’une fonction et peuvent donc calculer des valeurs indéfiniment. Il est généralement judicieux d’utiliser limit(n) sur ces flux pour éviter d’imprimer un nombre infini de valeurs.
Regardons un exemple simple d’utilisation de iterate avant de l’expliquer :
La méthode iterate prend une valeur initiale, ici 0, et une lambda (de type Unaryoperator<T>) à appliquer successivement sur chaque nouvelle valeur produite. Ici, vous retournez l’élément précédent, ensuite vous y ajoutez 2 en utilisant l’expression lambda n -> n + 2. Par conséquent, la méthode itérative produit un flux de tous les nombres pairs : le premier élément du flux est la valeur initiale 0. Ensuite, il ajoute 2 pour produire la nouvelle valeur 2 ; il ajoute encore 2 pour produire la nouvelle valeur 4 et ainsi de suite. La méthode iterate est fondamentalement séquentielle car le résultat dépend de l’application précédente. Notez que cette opération produit un flux infini – le flux n’a pas de fin car les valeurs sont calculées à la demande et peuvent être calculées pour toujours. Nous disons que le flux est illimité. Comme nous l’avons déjà mentionné, il s’agit d’une différence clé entre un flux et une collection. Vous utilisez la méthode limit pour limiter explicitement la taille du flux. Ici, vous sélectionnez uniquement les 10 premiers nombres pairs. Vous appelez ensuite l’opération terminale forEach pour consommer le flux et imprimer chaque élément individuellement.
En général, vous devez utiliser iterate lorsque vous devez produire une séquence de valeurs successives, par exemple une date suivie de la date suivante : 31 janvier, 1er février, etc. Pour voir un exemple plus difficile de cette fonctionnalité, essayez le Quiz 5.4.
Quiz 5.4: Série de tuples de Fibonacci
La série Fibonacci est célèbre comme exercice de programmation classique. Les nombres dans la séquence suivante font partie de la série Fibonacci : 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 …. Les deux premiers nombres de la série sont 0 et 1, et chaque nombre suivant est la somme des deux précédents.
La série de tuples de Fibonacci est similaire ; vous avez une suite d’un nombre et son successeur dans la série : (0, 1), (1, 1), (1, 2), (2, 3), (3, 5), (5, 8), (8, 13), (13, 21) ….
Votre tâche est de générer les 20 premiers éléments de la série de tuples Fibonacci en utilisant la méthode iterate.
Laissez-nous vous aider à démarrer. Le premier problème est que la méthode iterate prend un UnaryOperator <T> comme argument et que vous avez besoin d’un flux de tuples tel que (0, 1). Vous pouvez, encore une fois comme plutôt, utiliser un tableau de deux éléments pour représenter un tuple. Par exemple, new int [] {0, 1} représente le premier élément de la série Fibonacci (0, 1). Ce sera la valeur initiale de la méthode iterate :
Dans ce quiz, vous devez comprendre le code en surbrillance avec le ??? Souvenez-vous que la méthode iterate appliquera l’expression lambda reçu en paramètres successivement.
Réponse :
Comment ça marche ? iterate a besoin d’une expression lambda pour spécifier l’élément successeur. Dans le cas du tuple (3, 5), le successeur est (5, 3 + 5) = (5, 8). Le prochain est (8, 5 + 8). Arrivez-vous à distinguer le pattern ? Étant donné un tuple, le successeur est (t [1], t [0] + t [1]). C’est ce que la lambda expression suivante spécifie : t -> new int [] {t [1], t [0] + t [1]}. En exécutant ce code, vous obtiendrez les séries (0, 1), (1, 1), (1, 2), (2, 3), (3, 5), (5, 8), (8, 13), (13, 21) …. Notez que si vous vouliez simplement imprimer la série normale de Fibonacci, vous pouvez utiliser une map pour extraire seulement le premier élément de chaque ligne :
Ce code produira la série Fibonacci: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 ….
Générer
Comme la méthode iterate, la méthode generate permet de produire un flux infini de valeurs calculées à la demande. Mais generate n’applique pas successivement une fonction sur chaque nouvelle valeur produite. Il faut une expression lambda de type Supplier<T> pour fournir de nouvelles valeurs. Regardons un exemple sur la façon de l’utiliser :
Ce code va générer un flux de cinq nombres doubles aléatoires de 0 à 1. Par exemple, une exécution donne ce qui suit:
La méthode statique Math.random est utilisée comme générateur de nouvelles valeurs. Encore une fois vous limitez la taille du flux explicitement en utilisant la méthode limit ; sinon le flux serait illimité.
Vous vous demandez peut-être s’il y a quelque chose d’utile que vous pouvez faire avec la méthode generate. Le Supplier que nous avons utilisé (une référence de méthode à Math.random) était sans état : il n’enregistrait aucune valeur quelque part qui puisse être utilisée dans des calculs ultérieurs. Mais un Supplier n’a pas besoin d’être apatride. Vous pouvez créer un Supplier qui stocke l’état qu’il peut ensuite modifier et utiliser lors de la génération de la prochaine valeur du flux. A titre d’exemple, nous montrerons comment vous pouvez aussi créer la série Fibonacci à partir du Quiz 5.4 en utilisant generate de sorte que vous puissiez la comparer avec l’approche utilisant la méthode itérative. Mais il est important de noter qu’un Supplier avec état n’est pas prudent à utiliser dans un code parallèle. Donc, ce qui suit est montré juste pour l’exhaustivité, mais devrait être évité. Nous discuterons plus en détail du problème des opérations avec effets secondaires et flux parallèles dans le chapitre 7.
Nous utiliserons une IntStream dans notre exemple pour illustrer un code conçu pour éviter les opérations d’auto boxing. La méthode generate sur IntStream prend un IntSupplier au lieu d’un Supplier <T>. Par exemple, voici comment générer un flux infini de 1 :
Vous avez vu dans le chapitre 3 que les expressions lambdas vous permettaient de créer une instance d’une interface fonctionnelle en fournissant l’implémentation de la méthode directement en ligne. Vous pouvez également passer un objet explicite comme suit en implémentant la méthode getAsInt définie dans l’interface IntSupplier :
La méthode generate utilisera le fournisseur donné et appellera à plusieurs reprises la méthode getAsInt, qui renvoie toujours 2. Mais la différence entre la classe anonyme utilisée ici et une lambda expression est que la classe anonyme peut définir l’état via des champs que la méthode getAsInt peut modifier. Ceci est un exemple d’effet secondaire. Toutes les lambdas que vous avez vus jusqu’ici étaient sans effets secondaires; elles n’ont changé aucun état.
Pour revenir à nos tâches Fibonacci, ce que vous devez faire maintenant est de créer un IntSupplier qui maintient dans son état la valeur précédente dans la série, donc getAsInt peut l’utiliser pour calculer l’élément suivant. En outre, il peut mettre à jour l’état de l’IntSupplier pour la prochaine fois qu’il sera appelé. Le code suivant montre comment créer un IntSupplier qui retournera l’élément suivant de Fibonacci :
Dans le code précédent, vous créez une instance de IntSupplier. Cet objet a un état mutable : il stocke l’élément de Fibonacci précédent et celui actuel dans deux variables d’instance. La méthode getAsInt modifie l’état de l’objet lorsqu’il est appelé afin qu’il produise de nouvelles valeurs à chaque appel. En comparaison, notre approche utilisant iterate était purement immuable : vous n’aviez pas à modifier l’état existant mais créiez plutôt de nouveaux tuples à chaque itération. Vous apprendrez au chapitre 7 que vous devriez toujours préférer une approche immuable pour traiter un flux en parallèle.
Notez que parce que vous traitez avec un flux de taille infinie, vous devez limiter sa taille explicitement en utilisant la méthode limit ; sinon, l’opération terminale (dans ce cas forEach) sera calculée indéfiniment. De même, vous ne pouvez pas trier ou réduire un flux infini, car tous les éléments doivent être traités et cela prendrait une éternité car le flux est infini.
5.8. Résumé
Ce fut un chapitre long mais enrichissant. Vous pouvez maintenant travailler avec les collections plus efficacement. En effet, les flux vous permettent d’exprimer des requêtes sophistiquées de traitement de données de manière concise. De plus, les flux peuvent être parallélisés de manière transparente. Voici quelques concepts clés à retenir de ce chapitre :
- L’API Streams vous permet d’exprimer des requêtes de traitement de données complexes. Les opérations communes de Stream sont résumées dans le tableau 5.1.
- Vous pouvez filtrer et découper un flux à l’aide des méthodes filter, distinct, skip et limit.
- Vous pouvez extraire ou transformer des éléments d’un flux à l’aide des méthodes map et flatMap.
- Vous pouvez trouver des éléments dans un flux en utilisant les méthodes findFirst et findAny. Vous pouvez faire correspondre un prédicat donné dans un flux à l’aide des méthodes allMatch, noneMatch et anyMatch.
- Ces méthodes utilisent le short-circuiting : un calcul s’arrête dès qu’un résultat est trouvé; il n’y a pas besoin de traiter tout le flux.
- Vous pouvez combiner itérativement tous les éléments d’un flux pour produire un résultat en utilisant la méthode reduce, par exemple, pour calculer la somme ou trouver le maximum d’un flux.
- Certaines opérations telles que filter et map sont sans état ; elles ne stockent aucun état. Certaines opérations telles que reduce stocke l’état afin de calculer une valeur. Certaines opérations telles que sort et distinct stockent également l’état car elles doivent mettre en mémoire tampon tous les éléments d’un flux avant de renvoyer un nouveau flux. De telles opérations sont appelées opérations statefull.
- Il existe trois spécialisations primitives de flux : IntStream, DoubleStream et LongStream. Leurs opérations sont également spécialisées en conséquence.
- Les flux peuvent être créés non seulement à partir d’une collection, mais également à partir de valeurs, de tableaux, de fichiers et de méthodes spécifiques telles que iterate et genrate.
- Un flux infini est un flux qui n’a pas de taille fixe.