/**
POGL : programmation objet et génie logiciel
Cours du 5 février 2018, lambda-expressions
@author Thibaut Balabonski @ Université Paris-Sud
*/
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import java.util.function.*;
/**
Passer une méthode en paramètre.
On cherche à définir une méthode [forEach] attendant deux paramètres :
- une collection d'objets de type [T], implémentant [Iterable<T>]
- une méthode [void f(T elt)] à appliquer à chaque élément de la collection
Comme on ne peut pas passer de méthode en paramètre à une autre méthode,
on peut à la place utiliser un objet possédant la méthode [f] souhaitée.
Nous définissons ci-dessous une interface [FProvider<T>] qui caractérise les
classes fournissant une méthode [f] avec la bonne signature.
*/
interface FProvider<T> {
void f(T elt);
}
class ForEach {
public static <T> void forEach(Iterable<T> c, FProvider<T> fProvider) {
Iterator<T> it = c.iterator();
while (it.hasNext()) {
T elt = it.next();
fProvider.f(elt);
}
}
/**
Remarque : les structures implémentant l'interface [Iterable] donnent
accès à la deuxième forme des boucles [for] de [Java]. La définition
suivante est équivalente à la première (plus précisément, la suivante
est traduite en la première).
public static <T> void forEach(Iterable<T> c, FProvider<T> fProvider) {
for(T elt : c) {
fProvider.f(elt);
}
}
*/
}
/**
Pour utiliser cette méthode [forEach], il faut encore :
- définir une classe fournissant la méthode [f] voulue, et donc implémentant
l'interface [FProvider],
- créer un nouvel objet de cette classe, à passer en paramètre à [forEach].
*/
class Printer implements FProvider<String> {
public void f(String s) {
// Ici, au milieu de la définition de classe,
// gît la seule ligne de code réellement utile.
System.out.print(s);
}
}
class FProviderTest {
public static void main(String[] args) {
List<String> c = new ArrayList<>();
c.add("À l'origine fut la vitesse,");
c.add("le pur mouvement furtif,");
c.add("le << vent-foudre >>.\n");
FProvider<String> printer = new Printer();
ForEach.forEach(c, printer);
// Sans donner de nom à l'objet [printer], les deux lignes
// précédentes auraient également pu être réduites à
// ForEach.forEach(c, new Printer())
}
}
/**
Définir une classe dans l'unique but de fournir une méthode à usage
unique peut paraître dommage. Plus grave, cela éloigne la définition
de la méthode [f] de son utilisation, ce qui obscurcit la présentation.
Avant Java 8, on pouvait faire un peu mieux avec le mécanisme des
classes anonymes, mais cela reste lourd. Depuis Java 8, les
lambda-expressions donnent une solution plus satisfaisante.
Notre appel à [forEach] peut être réduit à la ligne suivante :
ForEach.forEach(c, (String s) -> { System.out.print(s); })
Ce code très compact réalise à la fois :
- la définition de la méthode [f], sans nommer cette méthode ni même
la classe à laquelle elle appartient,
- la création de l'objet fournissant la méthode [f]
La syntaxe des lambda-expressions est en trois parties :
1/ Un n-uplet de paramètres avec leurs types, comme
(String s)
ou
(int n, boolean b)
2/ Le symbole flèche
->
3/ Un bloc de code entre accolades
{ ... }
*/
class ForEachLambdaTest {
public static void main(String[] args) {
List<String> c = new ArrayList<>();
c.add("À l'origine fut la vitesse,");
c.add("le pur mouvement furtif,");
c.add("le << vent-foudre >>.\n");
ForEach.forEach(c, (String s) -> { System.out.print(s); });
ForEach.forEach(c, (String s) -> {
String vent = s.replaceAll("[a-zÀ]", " ");
System.out.print(vent);
});
}
}
/**
Petites simplifications possibles :
- Il n'est généralement pas nécessaire de préciser les types des paramètres,
qui peuvent être déduits du contexte (inférence de type, ou en Java
"context typing").
- Si le bloc de code est réduit à :
{ return e; }
pour une certaine expression [e], on peut n'écrire que
e
Autrement dit, la lambda-expression
(int i) -> { return i*i; }
sera généralement écrite
i -> i*i
*/
/**
Une lambda-expression peut être reconnue comme instanciant n'importe quelle
interface fonctionnelle, pour peu qu'elle soit cohérente avec le type de
l'unique méthode abstraite de cette interface.
Le nom de la méthode n'a aucune importance, et elle automatiquement définie
par la lambda-expression.
On a par exemple les interfaces fonctionnelles suivantes dans le paquet
java.util.function de la bibliothèque standard.
*/
// Consomme un paramètre de type [T], sans renvoyer de résultat
interface Consumer<T> {
void accept(T elt);
}
// Prend un paramètre de type [T] et renvoie un résultat de type [R]
interface Function<T, R> {
R apply(T elt);
}
// Teste un paramètre de type [T] et renvoie un booléen
interface Predicate<T> {
boolean test(T elt);
}
class LambdaTest {
public static void main(String[] args) {
// Définition d'une variable de type [Predicate<Integer>] par une
// lambda-expression
Predicate<Integer> even = i -> i%2 == 0;
// La méthode [test] a été définie automatiquement
System.out.println(even.test(3));
// Remarquez que la même lambda-expression aurait pu instancier un
// autre type
Function<Integer, Boolean> stillEven = i -> i%2 == 0;
// Dans ce cas c'est la méthode [apply] qui est définie
System.out.println(stillEven.apply(3));
// La commande suivante aurait été rejetée, car la méthode [test]
// n'existe pas pour [stillEven] de type [Function].
// System.out.println(stillEven.test(3));
}
}
/**
Pointeurs de méthodes
Les lambda-expressions permettent d'écrire des expressions qui représentent
des méthodes anonymes. On peut de même écrire des expressions représentant
des méthodes définies dans une classe avec la notation
Class::method
*/
class PointerMethodTest {
public static void main(String[] args) {
List<String> c = new ArrayList<>();
c.add("À l'origine fut la vitesse,");
c.add("le pur mouvement furtif,");
c.add("le << vent-foudre >>.\n");
ForEach.forEach(c, System.out::print);
}
}