Programmation fonctionnelle en Java
Partie 2 : Java
Table des matières
1. Première séance : interface et généricité.............................................................................................................3
1.1. Notion d’interface........................................................................................................................................3
1.1.1. Problématique......................................................................................................................................3
1.1.2. Solution = interfaces............................................................................................................................4
1.1.3. Mise en œuvre......................................................................................................................................4
1.2. Classe générique, cas de ArrayList..............................................................................................................4
1.2.1. Motivation :..........................................................................................................................................4
1.2.2. Principe................................................................................................................................................5
1.2.3. Application simple de ArrayList..........................................................................................................5
1.2.4. Optionnel : Application plus complexe : Iterateur...............................................................................5
1.2.5. Autre exemple de collection générique : HashSet...............................................................................5
1.2.6. Interfaces génériques et implantation d’interfaces génériques............................................................5
1.2.7. Influence sur le langage : la boucle « for each »..................................................................................5
1.2.8. Optionnel : implanter de façon non générique une interface générique..............................................6
1.2.9. Précision...............................................................................................................................................6
2. Deuxième séance : lambda-expressions..............................................................................................................7
2.1. Introduction..................................................................................................................................................7
2.2. Plus formellement :......................................................................................................................................8
2.3. Généraliser les interfaces fonctionnelles.....................................................................................................8
2.4. Les 4 interfaces fonctionnelles de base.....................................................................................................10
2.4.1. Function<T,R>...................................................................................................................................10
2.4.2. Predicate<T>......................................................................................................................................10
2.4.3. Consumer<T>....................................................................................................................................10
2.4.4. Supplier<R>.......................................................................................................................................11
2.5. Optionnel : les versions à 2 paramètres.....................................................................................................12
2.6. Optionnel : les versions plus contraintes...................................................................................................12
2.7. Optionnel : les versions adaptées aux types simples.................................................................................12
3. Troisième séance...............................................................................................................................................13
3.1. Construction d’un Optional.......................................................................................................................13
3.2. Récupération de la donnée dans un Optional............................................................................................13
3.3. Savoir si un Optional est vide ou pas.........................................................................................................13
3.4. map et filter sur un Optional......................................................................................................................13
3.4.1. Optional est un foncteur : méthode map............................................................................................13
3.4.2. filter sur un Optional..........................................................................................................................14
3.5. Optional pour éviter de multiples tests à null............................................................................................14
3.6. Modifier une donnée encapsulée dans un Optional...................................................................................15
3.7. Optional est une monade...........................................................................................................................16
3.8. Optionnel : orElse, évaluation paresseuse et orElseGet............................................................................17
3.9. Optionnel : Traitement différencié selon la présence ou non d’une donnée encapsulée...........................17
3.10. Optionnel : classes Optional… pour les types simples............................................................................17
4. Quatrième séance...............................................................................................................................................18
4.1. Comment créer un flux ?...........................................................................................................................18
4.2. Un flux est un foncteur..............................................................................................................................18
1/26
4.3. Propriétés générales des flux en Java........................................................................................................19
4.4. Analogie avec les listes de Haskell............................................................................................................19
4.5. Quelques exemples de méthodes terminales.............................................................................................20
4.6. Flux infinis et évaluation paresseuse.........................................................................................................21
4.6.1. la méthode iterate...............................................................................................................................21
4.6.2. Optionnel : La méthode generate.......................................................................................................21
4.7. Tris sur les flux..........................................................................................................................................21
4.7.1. La méthode sorted()...........................................................................................................................21
4.7.2. Optionnel : L’interface Comparator<T>............................................................................................22
4.7.3. Optionnel : La méthode sorted(Comparator<? super T> comparator)..............................................22
5. Cinquième séance..............................................................................................................................................23
5.1. Réduction...................................................................................................................................................23
5.1.1. Version de base...................................................................................................................................23
5.1.2. Version sans initialisation...................................................................................................................23
5.1.3. Optionnel : version générale..............................................................................................................24
5.2. Optionnel : Rappel sur concaténer les chaînes d’un tableau.....................................................................24
5.2.1. Version naïve, peu efficace :..............................................................................................................24
5.3. Limites de la méthode reduce....................................................................................................................24
5.4. Collectes....................................................................................................................................................24
5.4.1. Motivation et premier exemple..........................................................................................................24
5.4.2. Deuxième exemple : mettre les données d’un flux dans une collection............................................25
5.4.3. Collecteurs prédéfinis.......................................................................................................................25
5.4.4. Optionnel : Créer son propre collecteur : l’interface Collector........................................................25
5.4.5. Autres exemples de collecteurs prédéfinis.........................................................................................25
5.4.6. Optionnel : Collecteurs avancés.........................................................................................................26
5.5. Stream est une monade..............................................................................................................................26
2/26
1. Première séance : interface et généricité
1.1. Notion d’interface
[Link]ématique
Un logiciel doit gérer un haras. On doit donc manipuler des humains (nom, prénom, age) et des chevaux
(nom, age) :
public class Humain {
private String nom;
private String prenom;
private int age;
public Humain(String leNom, String lePrenom, int lAge) {
nom = leNom;
prenom = lePrenom;
age = lAge;
}
public int getAge() {
return age;
}
public void nommer(String leNom) {
String[] decomposition = [Link](" ");
prenom = decomposition[0];
nom = decomposition[1];
}
public void setPrenom(String p) {
prenom = p;
}
private void setAge(int nouvelAge) {
age = nouvelAge;
}
public void vieillir() {
setAge(age+1);
}
public String toString() {
return "Humain [nom=" + nom + ", prenom=" + prenom + ", age=" + age + "]";
}
}
public class Cheval {
private String nom;
private int age;
public Cheval(String leNom, int age) {
nom = nom;
[Link] = age;
}
public int getAge() {
return age;
}
public void nommer(String leNom) {
nom = leNom;
}
public void vieillir() {
age += 3;
}
public String toString() {
return "Cheval [nom=" + nom + ", age=" + age + "]";
}
}
Les 2 classes proposent plusieurs méthodes communes (nommer, vieillir, getAge). Si on souhaite
chaque année faire vieillir tout le monde. 3 solutions a priori :
• 2 tableaux différents :
3/26
Inconvénient : devoir systématiquement manipuler 2 tableaux (1 par type) → lourd si beaucoup de
types
• 1 seul tableau de type Object, donc un if avec un cast :
Inconvénient : compliqué si beaucoup de types, surtout que c’est pour faire la même chose
• 1 même super-classe
Inconvénient : pas d’héritage multiple en Java, donc bloque toute autre possibilité d’héritage
[Link] = interfaces
• un nouveau genre de type, qui ne fait que déclarer des méthodes
• déclaration qu’une classe implante une interface grâce au mot-clef implements
• une classe C qui implante une interface I est un sous-type de cette interface, donc une donnée peut être
stockée dans une variable de type I
• une classe peut implanter plusieurs interfaces, tout en héritant d’une autre classe
[Link] en œuvre
• écrire l’interface EtreVivant, la faire implanter aux classes Personne et Animal.
• dans le main, mettre des données de type Personne et Animal dans un EtreVivant[].
Exercice :
• Rajouter des arbres (nom, age, espèce), qui sont aussi des êtres vivants
1.2. Classe générique, cas de ArrayList
[Link] :
dans l’exemple précédent, inconvénient du tableau : taille fixe. Du coup, on préférerait utiliser des listes :
On souhaiterait non seulement avoir une liste pour gérer des Humain, mais également une liste pour gérer des
mots (pour construire une phrase progressivement). Comment déclarer (avec quel type) les méthodes add et
get ? A priori, 2 solutions :
• Créer 2 classes : une ListeMots et une ListeHumains
Inconvénient : 2 fois le même travail ; à refaire en plus pour chaque nouveau type
• Choisir Object comme type
Inconvénient : une même liste peut contenir aussi bien des String que des Humain.
4/26
[Link]
Solution : dire que c’est un type quelconque, E, mais que ce sera le même pour add et get. E est alors un
paramètre de type ou variable de type de la classe Liste. On dit alors que Liste est une classe générique.
⚠ Un type simple ne peut jamais être utilisé pour un type générique. On utilise les types Wrapper à la place :
Liste<int> → Liste<Integer>.
Existe déjà en Java : classe ArrayList
→ Regarder la javadoc de la classe et notamment quelques méthodes génériques et non génériques :
déclaration de la classe
add ; set ; get
size ; isEmpty
Remarque : la classe ArrayList<E>, générique, implante l’interface List<E>, générique également.
[Link] simple de ArrayList
Exemple :
[Link] : Application plus complexe : Iterateur
[Link] exemple de collection générique : HashSet
Ensemble : pas de multi-occurrence d’une donnée :
[Link] génériques et implantation d’interfaces génériques
Dans la javadoc, on constate que ArrayList<E> et HashSet<E> implantent l’interface
Collection<E>, qui déclare les méthodes add, size et iterator. On peut donc reprendre les
exemples précédents avec une variable de type Collection : ArrayList<E> est un sous-type de
Collection<E> :
[Link] sur le langage : la boucle « for each »
L’interface Iterable est un super-type de Collection. Toute instance d’une classe implantant
Iterable peut être utilisée avec un for type « for each » :
Reprendre l’exemple précédent avec une boucle « for each » :
5/26
[Link] : implanter de façon non générique une interface générique
[Link]écision
Une interface peut aussi contenir des méthodes définies avec leur corps. Ces méthodes sont déclarées avec les
mots-clefs static ou final, et seront étudiées plus tard.
6/26
2. Deuxième séance : lambda-expressions
2.1. Introduction
Soit l’interface suivante :
public interface Operation {
int calcul(int a, int b);
}
Et soit les 2 classes suivantes implantant cette interface :
public class Addition implements Operation {
public int calcul(int a, int b) {
return a + b;
}
}
public class Multiplication implements Operation {
public int calcul(int a, int b) {
return a * b;
}
}
On peut alors les utiliser dans le programme suivant :
public class Main1 {
private static void traitement(int p1, int p2, Operation op) {
[Link]("------------------------------");
[Link]("Application d'une opération : ");
[Link](" 1er paramètre = " + p1);
[Link](" 2ème paramètre = " + p2);
int resultat = [Link](p1, p2);
[Link](" --> résultat = " + resultat);
[Link]("------------------------------");
}
public static void main(String[] args) {
int a = 1;
int b = 2;
Operation fois = new Multiplication();
Operation plus = new Addition();
traitement(a, b, fois);
traitement(a, b, plus);
}
}
Dans la définition des classes Addition et Multiplication, un certain nombre d’informations sont
inutiles, car :
• elles ne font que définir une méthode d’une interface (Operation). Notamment : pas d’autres
méthodes, pas de variables d’instance, pas de constructeur
• l’interface qu’elles implantent (Operation) contient une seule méthode
Donc :
• on sait le nom de la méthode implantée (calcul)
• on connaît les types des paramètres (int, int)
7/26
• on connaît le type de retour (int).
Par ailleurs, elles sont utilisées dans un contexte qui permet de savoir l’interface qu’elles implantent (elles
sont passées en paramètre à traitement, là où une donnée de type Operation est attendue). Donc le
nom même de l’interface peut être inféré par le compilateur.
Aussi, depuis Java 8, on peut écrire cela plus simplement :
Définir les classes Addition et Multiplication n’est ainsi plus nécessaire (mais l’interface
Operation reste indispensable).
On peut ainsi utiliser en Java des lambda-expressions, comme en Haskell.
2.2. Plus formellement :
Une lambda-expression est une instance d’une classe anonyme implantant une interface fonctionnelle.
Une interface fonctionnelle est une interface qui, une fois enlevées les méthodes static, les méthodes
default, et celle existant dans la classe Object, comporte une et une seule autre méthode.
→ illustrer avec l’interface Comparator, qui est fonctionnelle malgré ses nombreuses méthodes
Syntaxe :
• paramètres :
◦ 0 : ()
◦ 1 : x ou (x) ou (typeX x)
◦ 2 ou plus : (x, y, …) ou (typeX x, typeY y, … )
• corps :
◦ cas général : {…}
◦ si doit juste renvoyer une valeur : expr
Une lambda-expression peut être utilisée partout où une interface fonctionnelle est attendue.
N.B. : il est possible d’annoter une classe avec @FunctionalInterface pour préciser clairement que
c’est une interface fonctionnelle, mais ce n’est pas indispensable.
2.3. Généraliser les interfaces fonctionnelles
Par rapport à Haskell, le type n’est pas inféré à partir de ce qui est fait dans le corps de la lambda-expression,
mais à partir de l’interface qui type son utilisation → nécessité d’avoir défini la bonne interface → Lourd.
Exemple :
public class Personne {
private String nom;
private String prenom;
private int age;
8/26
public Personne(String leNom, String lePrenom, int lAge) {
nom = leNom;
prenom = lePrenom;
age = lAge;
}
public int getAge() {
return age;
}
public void setAge(int unAge) {
age = unAge;
}
public String toString() {
return "Humain [nom=" + nom + ", prenom=" + prenom + ", age=" + age + "]";
}
public String getNom() {
return nom;
}
public String getPrenom() {
return prenom;
}
public void setPrenom(String lePrenom) {
prenom = lePrenom;
}
}
public interface TransPersonneInteger {
public Integer trans(Personne p);
}
public interface TransPersonneString {
public String trans(Personne p);
}
public class Main {
public static void abregePersonne(Personne p, TransPersonneString abregeur) {
[Link]("-- " + [Link](p) + " --");
}
public static void anonymisePersonne(Personne p, TransPersonneInteger abregeur) {
[Link]("** " + ([Link](p) % 2) + " **");
}
public static void main(String[] args) {
Personne p1 = new Personne("Duhamel", "Christophe", 37);
abregePersonne(p1, p -> [Link]());
abregePersonne(p1, p -> [Link]());
anonymisePersonne(p1, p -> [Link]());
}
}
Dans l’exemple ci-dessus :
• TransPersonneInteger : Personne → Integer
• TransPesonneString : Personne → String
On aurait pu définir une seule interface, générique :
TransPersonne<R> : Personne → R
Et réécrire notre main ainsi :
N.B. : l’écriture des lambda-expressions n’a pas changé
Et même penser plus général encore :
9/26
N.B. : l’écriture des lambda-expressions n’a toujours pas changé
Ce cas général est très fréquent → intégré dans l’API standard : [Link]. La
fonction de l’interface s’appelle par contre apply.
Du coup, pas besoin de définir l’interface Transformeur :
2.4. Les 4 interfaces fonctionnelles de base
Le package [Link] propose de nombreuses interfaces fonctionnelles afin de répondre à la
grande majorité des situations où passer une lambda-expression pourrait être envisagé, sans devoir écrire
systématiquement une interface fonctionnelle ad hoc. Elles sont essentiellement toutes des déclinaisons des 4
interfaces fonctionnelles suivantes.
2.4.1. Function<T,R>
Vue précédemment : quand on a besoin d’obtenir une donnée de type R à partir d’une donnée de type T
2.4.2. Predicate<T>
On a souvent besoin de savoir si une donnée vérifie une certaine propriété, donc d’avoir un résultat booléen à
partir d’une donnée d’un type quelconque (c.f. la fonction passée en paramètre à filter en Haskell par
exemple). Cela pourrait être défini par un Function<T,Boolean>, mais comme le cas est fréquent et un
peu particulier, nouveau type : Predicate<T>.
Exemple + exercice :
Soit le programme suivant :
public class Main {
public static void selectionne(Personne p, Predicate<Personne> critere) {
if ([Link](p)) {
[Link](p);
} else {
[Link]("Personne masquée");
}
}
public static void main(String[] args) {
Personne p1 = new Personne("Duhamel", "Christophe", 37);
Personne p2 = new Personne("Jay", "Véronique", 16);
selectionne(p1, pers -> [Link]() >= 18);
selectionne(p2, pers -> [Link]() >= 18);
}
}
Rajouter le code qui permet, pour chacune des variables p1 et p2, de ne les afficher que si leur nom fait 5
lettres ou moins.
2.4.3. Consumer<T>
Quand on a besoin d’une donnée, mais sans produire de résultat. 2 cas de figure :
10/26
• On veut modifier directement la donnée
• On veut récupérer les infos de la donnée mais sans rien renvoyer (pour un affichage par exemple)
Exemple + exercice :
Soit le code suivant :
public class Main {
public static void modifieur(Personne p, Consumer<Personne> consommateur) {
[Link]("AVANT " + p);
[Link](p);
[Link]("APRES " + p);
}
static class ModifAge implements Consumer<Personne> {
@Override
public void accept(Personne p) {
[Link](50);
}
}
public static void main(String[] args) {
Personne p1 = new Personne ("Duhamel", "Christophe", 37);
modifieur(p1, p -> [Link](10));
}
}
Rajouter les lignes qui permettent de :
• modifier le prénom de p1 en « Véronique »
• afficher la personne p1.
2.4.4. Supplier<R>
Quand on n’est pas sûr d’avoir besoin de construire une donnée.
Exemple : on simule le fait qu’une construction d’objet prend du temps (ici, obtenir une version avec nom en
majuscule pour une personne).
public class Personne {
private String nom;
private String prenom;
private int age;
public Personne(String leNom, String lePrenom, int lAge) {
nom = leNom;
prenom = lePrenom;
age = lAge;
}
public Personne(Personne p) {
// pause de 5 secondes pour simuler un temps de calcul
try {
[Link](5000);
} catch (InterruptedException ie) {};
nom = [Link]();
prenom = [Link];
age = [Link];
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Humain [nom=" + nom + ", prenom=" + prenom + ", age=" + age + "]";
}
public String getNom() {
return nom;
}
11/26
public String getPrenom() {
return prenom;
}
}
public class Main0 {
//booléen permettant de simuler le fait qu’on a besoin des données ou pas
private static boolean debug = true;
//Version sans passer par un supplier
public static void afficheSimple(Personne personne) {
[Link]("Dans affiche Simple");
if (affichage) {
[Link](personne);
}
}
public static void main(String[] args) {
Personne p1 = new Personne("Duhamel", "Christophe", 37);
Personne p2 = new Personne("Jay", "Véronique", 16);
Personne p3 = new Personne("Mermet","Bruno", 27);
List<Personne> enseignants = [Link](p1, p2, p3);
//Version sans passer par un supplier
for (Personne p: enseignants) {
afficheSimple(new Personne(p));
}
//Version avec supplier
for (Personne p: enseignants) {
affiche(() -> new Personne(p));
}
}
}
• Avec le booléen debug à true, les 2 traitements prennent le même temps (5 secondes par personne,
car on crée systématiquement les nouvelles personnes).
• Avec le booléen debug à false, le 1er traitement continue de prendre 5 secondes par personne, car on
les crée systématiquement, mais le 2è traitement est quasi instantané : comme ce n’est pas nécessaire,
on ne crée pas les nouvelles personnes.
C’est le principe d’évaluation paresseuse : on diffère le calcul d’une donnée jusqu’à son utilisation. Si pas
d’utilisation, le calcul n’est jamais effectué.
2.5. Optionnel : les versions à 2 paramètres
2.6. Optionnel : les versions plus contraintes
2.7. Optionnel : les versions adaptées aux types simples
12/26
3. Troisième séance
En Haskell, on pouvait représenter qu’une fonction pouvait renvoyer un résultat ou pas grâce au type Maybe.
En Java, la classe Optional permet de faire la même chose.
Maybe est un type paramétré par un autre type : par exemple, Just ‘a’ est un Maybe Char. En Java, il
en est de même pour Optional<T>: c’est une classe générique.
3.1. Construction d’un Optional
3 façons de faire :
• construire un Optional représentant l’absence de donnée (Nothing de Haskell) :
[Link]()
• construire un Optional encapsulant une donnée (Just de Haskell) : [Link](donnee)
• construire l’un des 2 à partir d’une référence pouvant valoir null :
[Link](ref)
N.B. : dans le cas 2, si la référence est à null, une exception est levée
Exemple :
3.2. Récupération de la donnée dans un Optional
On peut récupérer une donnée encapsulée dans un Optional grâce à la méthode get. Cette méthode lève
une exception s’il n’y a pas de donnée. La méthode orElse(valeurParDefaut) s’applique à tout
Optional et renvoie la donnée encapsulée dans le Optional s’il n’est pas vide, mais
valeurParDefaut si le Optional est vide :
Optional<T> :
orElse : T orElse(T valeurPaDefaut).
Exemple (Main1 complété) :
3.3. Savoir si un Optional est vide ou pas
2 méthodes : isPresent() et isEmpty().
Exemple (Main1 complété) :
3.4. map et filter sur un Optional
[Link] est un foncteur : méthode map
En Haskell, on avait vu que Maybe était un foncteur, c’est à dire qu’on pouvait utiliser fmap :
13/26
fmap : Functor f => (a -> b) -> f a -> f b
En Java, Optional présente les mêmes propriétés avec la méthode map :
Optional<A> :
Optional<B> map(Function<A,B>)
Remarque : en fait, map est plus générale :
Optional<T> :
Optional<U> map(Function< ? Super T, ? extends U>
Exemple :
[Link] sur un Optional
La méthode filter permet d’obtenir un nouveau Optional à partir d’un Optional avec le
fonctionnement suivant :
• Si le Optional de départ est vide, on obtient un Optional vide ;
• Si la donnée encapsulée dans le Optional de départ ne vérifie pas le prédicat passé en paramètre à
filter, on obtient un Optional vide ;
• Si la donnée encapsulée dans le Optional de départ vérifie le prédicat passé en paramètre à filter,
on récupère le Optional de départ.
3.5. Optional pour éviter de multiples tests à null
Comme en Haskell, un map sur un Optional vide renvoie un Optional vide sans provoquer d’erreur. Le
code suivant, sans Optional :
public static void traitement(Personne pers) {
[Link]("Début traitement...");
String nomFinal;
if (pers != null) {
String nom = [Link]();
String nomMaj = [Link]();
if ([Link]("V")) {
nomFinal = nomMaj;
} else {
nomFinal = "pas de personne";
}
} else {
nomFinal = "pas de personne";
}
[Link](nomFinal);
[Link]("Fin du traitement");
}
Peut être avantageusement remplacé par :
Et pour alléger la déclaration des variables, on peut utiliser le mot-clef var :
14/26
conditions d’utilisation de var :
• obligatoires :
◦ Le type doit pouvoir être inféré par le compilateur Java dès la déclaration de la variable, ce qui
impose qu’une valeur lui soit affectée en même temps ;
◦ C’est réservé aux variables locales
• conseillées :
◦ à limiter aux noms de type long
◦ à ne mettre que pour une utilisation quasi-immédiate de la variable
Exemple complet avec la classe Personne :
public class Personne {
private String nom;
private String prenom;
public Personne(String nom, String prenom) {
super();
[Link] = nom;
[Link] = prenom;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
[Link] = nom;
}
public String getPrenom() {
return prenom;
}
public void setPrenom(String prenom) {
[Link] = prenom;
}
@Override
public String toString() {
return "Personne [nom=" + nom + ", prenom=" + prenom + "]";
}
}
3.6. Modifier une donnée encapsulée dans un Optional
Contrairement à ce qui se passe dans les langages fonctionnels comme Haskell, les données ne sont pas des
objets non mutables. On peut donc vouloir modifier une donnée encapsulée dans un Optional, où
n’effectuer une action que si un Optional contient une donnée.
Méthode à utiliser : ifPresent (à ne pas confondre avec isPresent) :
Optional<T> :
void ifPresent(Consumer< ? Super T> action)
Exemple :
15/26
Exercice :
Complétez le programme précédent pour que les noms des personnes soient changés en majuscule avant
d’être affichés
public class Main5bis {
public static void main(String[] args) {
Personne p1 = new Personne("Jay", "Véronique");
Personne p2 = null;
Optional<Personne> op1 = [Link](p1);
[Link]("------------");
[Link](p -> [Link](p));
[Link]("------------");
Optional<Personne> op2 = [Link](p2);
[Link]("------------");
[Link](p -> [Link](p));
[Link]("------------");
[Link](pers -> [Link]("Ponty"));
[Link](pers -> [Link]("Ponty"));
[Link](pers -> [Link](pers));
[Link](pers -> [Link](pers));
}
}
3.7. Optional est une monade
Rappel sur Maybe en tant que monade :
>>= : Maybe a → (a → Maybe b) → Maybe b
Alors qu’avec un map, on aurait eu : Maybe a→ (a → Maybe b) → Maybe (Maybe b)
Pour Optional, c’est flatMap qui joue le rôle de l’opérateur >>= :
Typage simplifié :
Optional<T> :
Optional<U> flatMap(Function<T, Optional<U>> mapper)
Typage réel (plus général au niveau de la fonction) :
Optional<T> :
Optional<U> flatmap(Function< ? super T, ? extends Optional< ?
extends U>>)
Exercice :
Soit la méthode suivante permettant de calculer le prédécesseur d’un naturel. 0 n’ayant pas de prédécesseur,
elle renvoie un Optional<Integer> :
// int -> Optional<int>
public static Optional<Integer> pred(int x) {
if (x > 0) {
return [Link](x - 1);
} else {
return [Link]();
}
}
Écrire un programme qui, sans if, calcule :
• pred(2),
16/26
• pred(pred(2)),
• pred(pred(pred(2))),
• pred(pred(pred(pred(2)))),
• pred(pred(pred(pred(pred(2))))).
3.8. Optionnel : orElse, évaluation paresseuse et orElseGet
3.9. Optionnel : Traitement différencié selon la présence ou non d’une
donnée encapsulée
[Link] : classes Optional… pour les types simples
17/26
4. Quatrième séance
En Haskell, on a vu qu’on pouvait traiter des masses de données d’un type précis grâce aux listes. En Java,
c’est également possible grâce aux flux (Stream).
Tout comme le type list en haskell est paramétré par un type, le type Stream est en fait un type
paramétré : Stream<T>.
Pourquoi le type Stream<T> et pas le type List<E> ?
• Pour assurer la rétro-compatibilité ;
• Parce que le type List n’était pas conçu pour l’évaluation paresseuse
4.1. Comment créer un flux ?
Stream<T> est une interface, pas une classe. Il n’y a donc pas de constructeur.
On peut notamment créer un flux :
• grâce à la méthode stream() de l’interface Collection pour toutes les collections
• grâce à la méthode de classe stream(T[] tableau) de la classe Arrays pour les tableaux
• grâce à la méthode d’interface of(...) de l’interface Stream (pour un tableau ou un ensemble fini
d’éléments connu à la compilation)
• grâce à la méthode empty() de l’interface Stream pour créer un flux vide
Exemples (Main1) :
Mais il existe de nombreuses classes permettant également de créer des flux :
• Méthode stream() de la classe Optional (flux vide ou à 1 élément suivant que le Optional
encapsule une donnée ou pas)
• Méthode lines(Path p) de la classe [Link] (flux des lignes d’un fichier)
• Méthode list(Path dir) de la classe [Link] (flux des fichiers du répertoire)
Un flux est destiné à être utilisé pour produire une autre donnée ou pour produire un effet de bord, grâce à une
méthode « terminale ». Ainsi, pour afficher le contenu d’un flux, on peut utiliser la méthode :
void forEach(Consumer< ? Super T> traitement)
Exemple :
4.2. Un flux est un foncteur
Un flux en Java, comme une liste en Haskell, est un foncteur. On y trouve donc la méthode :
18/26
Stream<T>
<R> Stream<R> map(Function<? super T,? extends R> mapper)
Que l’on peut dans un premier temps interpréter comme :
<R> Stream<R> map(Function<T,R> mapper)
⚠ L’indentation proposée ci-dessus (points alignés) n’est pas obligatoire, mais c’est une convention à
respecter
N.B. : on trouve aussi sur les flux Java la méthode filter qu’on avait sur les listes Haskell :
Stream<T>
Stream<T> filter(Predicate<? super T> predicate)
Exemple :
⚠ Les méthodes comme map et filter renvoient de nouveaux flux. Il est donc ainsi possible de chaîner
les traitements. On peut ainsi traiter des flux de données sans écrire de boucle (écriture plus lisible) et avec
une mise en œuvre efficace (l’évaluation paresseuse évite de calculer des éléments qui ne serviront peut-être
pas).
4.3. Propriétés générales des flux en Java
Un flux est à usage unique : après avoir utilisé un flux pour un traitement, si on veut faire un autre traitement
à partir des mêmes données, il faut recréer un flux.
Un flux est évalué paresseusement : les données d’un flux ne sont calculées que lorsqu’elles sont requises.
La deuxième propriété permet d’accepter sereinement la première : « créer » un flux a un coût en général
négligeable.
Il existe des classes de flux spécifiques plus efficaces pour certains type simples : DoubleStream,
IntStream et LongStream.
Exemples :
Remarque : La méthode stream de Arrays crée un flux de types simples si le tableau passé en paramètre
contient des valeurs de type simple.
Attention : sur ces classes de flux spécifiques, le map renvoie aussi un flux du même type simple.
4.4. Analogie avec les listes de Haskell
La méthode Optional<T> findFirst() permet de récupérer le premier élément d’un flux (si le flux
n’est pas vide). Analogie avec head en Haskell
19/26
La méthode Stream<T> limit(long n) permet de ne garder que les n premiers éléments d’un flux.
Analogie avec take en Haskell.
La méthode Stream<T> skip(long n) permet de sauter les n premiers éléments d’un flux. Analogie
avec drop en Haskell.
La méthode Stream<T> takeWhile(Predicate<? super T> predicate) permet de garder les
premiers éléments d’un flux tant qu’ils vérifient un prédicat donné. qu’ils vérifient un prédicat donné.
Analogie avec takeWhile en Haskell.
La méthode Stream<T> dropWhile(Predicate<? super T> predicate) permet de jeter les
premiers éléments d’un flux tant qu’ils vérifient un prédicat donné. Analogie avec dropWhile en Haskell.
⚠Comme map et filter, ces méthodes (sauf findFirst) renvoient des flux, et peuvent donc être
intégrées à une chaîne de traitements.
Exercice :
À partir du début de programme suivant et en passant par un flux à chaque fois :
public class Main2 {
public static void main(String[] args) {
List<String> mots = [Link]("Bonjour", "tout", "le", "monde");
}
}
afficher :
• Les 3 premiers mots
• tous les mots sauf les 3 premiers
• les premiers mots tant qu’ils contiennent un « o »
• tous les mots à partir du moment où on rencontre un mot de 3 caractères ou moins.
4.5. Quelques exemples de méthodes terminales
On a vu jusqu’à présent la méthode forEeach, mais il existe d’autres méthodes terminales sur les flux :
• long count() : nombre d’éléments dans le flux
• boolean allMatch(Predicate<? super T> predicate) : tous les éléments du flux
vérifient-ils un prédicat ?
• boolean anyMatch(Predicate<? super T> predicate) : y a-t-il au moins un élément
du flux vérifiant un prédicat ?
• boolean noneMatch(Predicate<? super T> predicate) : tous les éléments du flux ne
vérifient-ils pas un prédicat ?
• Optional<T> max(Comparator<? super T> comparator) : quel est le plus grand
élément du flux (pour le Comparator passé en paramètre) ?
• Optional<T> min(Comparator<? super T> comparator) : quel est le plus petit élément
du flux ?
Quelques exemples :
20/26
Il existe aussi des méthodes terminales permettant de stocker les éléments du flux dans un tableau ou dans une
liste non modifiable :
• Object[] toArray() : stocke les éléments dans un tableau de Object
• <A> A[] toArray(IntFunction<A[]> createurTableau) : stocke les éléments du flux
dans un tableau de A. Le paramètre à passer est la lambda-expression qui permet, à partir d’un entier n
donné, de créer un tableau de A de n cases. C’est donc en général une lambda expression de la forme :
n -> new A[n]
• List<T> toList() : stocke les éléments du flux dans une liste non modifiable
4.6. Flux infinis et évaluation paresseuse
On avait vu en Haskell qu’on pouvait définir des listes infinies grâce à l’évaluation paresseuse :
• [0..] : liste des naturels
• [0,2,..] liste des nombres pairs
La même chose est possible pour les flux en Java.
[Link] méthode iterate
La méthode d’interface iterate(T seed, UnaryOperator<T> f) permet de générer un flux infini :
• seed (graine) est la première valeur
• f est la fonction qui détermine comment, à partir d’une valeur, calculer la valeur suivante.
L’évaluation des flux étant paresseuse, seuls les éléments requis au moment de la méthode terminale liée au
traitement du flux seront calculés.
Exemple : concrétisation de l’évaluation paresseuse
Exercice : afficher les carrés inférieurs ou égaux à 5000 des naturels pairs
[Link] : La méthode generate
4.7. Tris sur les flux
Il est également possible d’effectuer des opérations de tri sur les flux.
[Link] méthode sorted()
Cette méthode, sans paramètre, utilise la méthode compareTo du type des données du flux.
Exemple :
21/26
Inconvénient : un seul type de tri possible, figé.
[Link] : L’interface Comparator<T>
[Link] : La méthode sorted(Comparator<? super T> comparator)
22/26
5. Cinquième séance
5.1. Réduction
5.1.1. Version de base
En Haskell, on avait vu qu’il était possible d’effectuer un pliage (ou un réduction) sur une liste grâce à la
fonction foldl :
Rappel Haskell :
foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b
En Java, la méthode reduce permet de faire la même chose sur un flux :
Stream<T> :
T reduce(T identity, BinaryOperator<T> accumulator)
Interprétation :
• identity : valeur de départ
• accumulator : fonction donnant la valeur suivante de l’accumulateur en fonction de la valeur
précédente et de la nouvelle valeur rencontrée dans la liste.
Remarque : dans cette version, le type du résultat doit être le même que celui des éléments de la liste. C’est un
peu comme si foldl était de type :
foldl :: Foldable t => (a -> a -> a) -> a -> t a -> a
Exemple : somme des entiers de 0 à 1000 :
Version avec Stream<Integer> :
Version avec IntStream :
[Link] sans initialisation
En Haskell, foldl1 permettait d’initialiser l’accumulateur avec la valeur du premier élément de la liste. En
Java, l existe une variante de reduce qui fonctionne de la même façon. Mais pour gérer le cas où le flux
serait vide, elle renvoie un Optional<T> :
Stream<T>
Optional<T> reduce(BinaryOperator<T> accumulator)
Exercice : réécrire les 2 exemples précédent avec cette version de reduce.
23/26
[Link] : version générale
5.2. Optionnel : Rappel sur concaténer les chaînes d’un tableau
[Link] naïve, peu efficace :
5.3. Limites de la méthode reduce
Avec reduce, un nouvel objet est construit à chaque itération, ce qui peut rapidement s’avérer inefficace.
Ainsi, le programme suivant prend une dizaine de secondes :
public class Main2 {
public static void main(String[] args) {
Stream<String> flux = [Link](0, n -> n + 1)
.limit(200_000)
.map(n -> [Link](n));
[Link]("Avant collecte");
Optional<String> res1 = [Link]((acc, n) -> acc + ", " + n);
String res2 = "[" + [Link]("") + "]";
//affichage non réalisé pour pouvoir lire les messages précédents
//et garder un temps ne dépendant que du calcul et pas de l'affichage
//[Link](res2);
}
}
En effet, de par son format, reduce construit systématiquement un nouvel objet à chaque itération. Pourtant,
il existe des moyens plus efficaces pour concaténer des chaînes de caractères, comme vu précédemment.
5.4. Collectes
[Link] et premier exemple
La méthode collect peut avantageusement être utilisée à la place de reduce lorsqu’il existe un type
mutable pour l’accumulateur servant au pliage.
Stream<T> :
<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T>
accumulator, BiConsumer<R,R> combiner)
• supplier : la fonction permettant de créer l’objet assurant la collecte
• accumulator : la fonction permettant de modifier l’objet assurant la collecte pour chaque donnée
du flux (alors qu’avec un reduce, on créait un nouvel objet)
• combiner : la fonction permettant de combiner 2 collectes effectuées séparément (pour permettre des
calculs en parallèle).
Exemple : reprise du traitement précédent
Cette fois-ci, le calcul de la chaîne est quasi instantané.
24/26
[Link]ème exemple : mettre les données d’un flux dans une collection
On a vu précédemment la méthode toList() de la classe Stream. Cette méthode crée une liste (on ne sait
pas exactement laquelle), et uniquement une liste, non mutable. Comment faire si on veut créer une autre
collection : un Set ou bien une liste mutable comme une ArrayList par exemple ? On peut utiliser la
méthode collect :
Exemple : mettre dans une ArrayList les multiples de 7 inférieurs à 1 000 000
Remarque : il suffit de changer le premier paramètre de collect pour changer la collection créée, les
méthodes add et addAll étant similaires pour toutes les collections.
5.4.3. Collecteurs prédéfinis
Certains types de collectes sont suffisamment classiques pour pouvoir être intégré dans l’API standard Java.
C’est ce qu’on trouve dans la classe Collectors. C’est une interface proposant de nombreuses méthodes
de classe (static) renvoyant un collecteur (objet de type Collector, type sur lequel nous reviendrons
plus tard) que l’on peut passer en paramètre à cette version de la méthode collect :
<R, A> R collect(Collector<? super T,A,R> collector)
Concaténation de chaînes :
static Collector<CharSequence,?,String> joining(CharSequence
delimiter, CharSequence prefix, CharSequence suffix)
Création d’une liste modifiable :
static <T> Collector<T,?,List<T>> toList()
Création d’un ensemble :
static <T> Collector<T,?,Set<T>> toSet()
Création d’une collection quelconque :
static <T, C extends Collection<T>> Collector<T,?,C>
toCollection(Supplier<C> collectionFactory)
Quelques exemples :
5.4.4. Optionnel : Créer son propre collecteur : l’interface Collector
[Link] exemples de collecteurs prédéfinis
Calcul de sommes :
Soit la classe Personne suivante :
class Personne {
private String nom;
private String prenom;
private int age;
public Personne(String nom, String prenom, int age) {
25/26
[Link] = nom;
[Link] = prenom;
[Link] = age;
}
public String getNom() {
return nom;
}
public String getPrenom() {
return prenom;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Personne [nom=" + nom + ", prenom=" + prenom + ", age=" + age + "]";
}
}
Calcul de la somme des âges d’une liste de personnes et de la longueur totale des noms :
Récolter plusieurs statistiques en 1 seul parcours :
[Link] : Collecteurs avancés
5.5. Stream est une monade
Dans le cas où une méthode produirait un flux à partir d’un élément, appliquer cette méthode à un flux
renverrait un flux de flux. Pour aplatir le résultat, on peut utiliser flatMap:
Stream<T>
<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends
R>> mapper)
Exemple :
26/26