1. Approfondissons l’interface fonctionnelle
Une interface fonctionnelle — c’est une interface qui contient EXACTEMENT une méthode abstraite (c’est-à-dire non implémentée). C’est précisément grâce à cela que Java comprend : « ah, on peut passer une lambda ici ! » ou une référence de méthode.
Pour éviter toute erreur, ces interfaces portent généralement l’annotation @FunctionalInterface. Elle n’est pas obligatoire, mais si vous l’ajoutez et écrivez par inadvertance une deuxième méthode abstraite, le compilateur s’en plaindra immédiatement.
Exemple :
@FunctionalInterface
interface MyAction {
void run();
}
MyAction action = () -> System.out.println("Bonjour depuis une lambda !");
action.run(); // Affichera : Bonjour depuis une lambda !
À quoi ça sert ?
- Permet d’utiliser des expressions lambda et des références de méthode au lieu de créer des classes anonymes (moins de boilerplate !).
- Indique au compilateur que l’interface est destinée à la programmation fonctionnelle.
Fait intéressant : La bibliothèque standard de Java contient déjà des dizaines de telles interfaces — pas besoin de réinventer la roue !
2. Tour d’horizon des interfaces fonctionnelles standard
Le package java.util.function contient des dizaines d’interfaces fonctionnelles. Examinons les quatre plus populaires (celles qui ont la plus forte « fréquentation » parmi toutes les interfaces Java).
| Interface | Paramètre(s) | Renvoie | Usage courant |
|---|---|---|---|
|
|
|
Vérification d’une condition (filtrage) |
|
|
|
Exécuter une action sur l’objet |
|
rien | |
Obtenir/générer un objet |
|
|
|
Transformer T en R |
Predicate<T>
Description : Une fonction qui prend un objet de type T et renvoie true ou false. Exemple typique : filtrage d’une liste. Méthode clé — test.
Predicate<String> isLong = s -> s.length() > 5;
System.out.println(isLong.test("Java")); // false
System.out.println(isLong.test("Functional")); // true
Consumer<T>
Description : Prend un objet de type T et exécute une action, ne renvoie rien. Méthode clé — accept.
Consumer<String> printer = s -> System.out.println("J'imprime : " + s);
printer.accept("Hello, world!"); // J'imprime : Hello, world!
Supplier<T>
Description : Ne prend rien, renvoie un objet de type T. On peut le voir comme un « générateur » de valeurs. Méthode clé — get.
Supplier<Double> randomSupplier = () -> Math.random();
System.out.println(randomSupplier.get()); // Par exemple, 0.1234567
Function<T, R>
Description : Prend un objet de type T et renvoie un objet de type R. Exemple typique : transformation de données. Méthode clé — apply.
Function<String, Integer> stringToLength = s -> s.length();
System.out.println(stringToLength.apply("Java")); // 4
En bref : UnaryOperator, BinaryOperator, BiFunction
- UnaryOperator<T> — équivaut à Function<T, T> : prend et renvoie le même type.
- BinaryOperator<T> — équivaut à BiFunction<T, T, T> : prend deux T, renvoie un T.
- BiFunction<T, U, R> — prend deux types différents, renvoie un troisième.
UnaryOperator<Integer> square = x -> x * x;
BinaryOperator<Integer> sum = (a, b) -> a + b;
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);
3. Exemples d’utilisation
Voyons comment ces interfaces se rencontrent dans des tâches réelles, en particulier dans les collections et le Stream API.
Passage aux méthodes des collections et du Stream API
Exemple 1 : Predicate et filtrage
List<String> words = List.of("java", "stream", "lambda", "code");
List<String> longWords = words.stream()
.filter(word -> word.length() > 4) // Predicate<String>
.toList();
System.out.println(longWords); // [stream, lambda]
Exemple 2 : Consumer et forEach
words.forEach(word -> System.out.println("Mot : " + word)); // Consumer<String>
Exemple 3 : Function et map
List<Integer> lengths = words.stream()
.map(word -> word.length()) // Function<String, Integer>
.toList();
System.out.println(lengths); // [4, 6, 6, 4]
Exemple 4 : Supplier et génération de valeurs
Supplier<String> greetingSupplier = () -> "Bonjour, Java !";
System.out.println(greetingSupplier.get()); // Bonjour, Java !
Comparaison avec les classes anonymes
Avant, on devait écrire comme ceci :
Predicate<String> isShort = new Predicate<String>() {
@Override
public boolean test(String s) {
return s.length() < 5;
}
};
Avec les lambdas, c’est bien plus agréable :
Predicate<String> isShort = s -> s.length() < 5;
4. Pratique : écrire des expressions lambda pour chaque interface
Implémentons une petite application — une liste d’utilisateurs. Chaque utilisateur sera représenté par la classe User :
public class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() {
return name + " (" + age + ")";
}
}
Créons une liste d’utilisateurs :
List<User> users = List.of(
new User("Anna", 23),
new User("Boris", 17),
new User("Victoria", 31),
new User("George", 15)
);
Predicate : filtrer les adultes
Predicate<User> isAdult = user -> user.getAge() >= 18;
List<User> adults = users.stream()
.filter(isAdult)
.toList();
System.out.println("Adultes : " + adults); // Adultes : [Anna (23), Victoria (31)]
Consumer : afficher les utilisateurs
Consumer<User> printUser = user -> System.out.println("Utilisateur : " + user);
adults.forEach(printUser);
Supplier : générer des utilisateurs
Supplier<User> randomUserSupplier = () -> {
String[] names = {"Dale", "Kate", "Alex"};
int randomAge = 10 + (int)(Math.random() * 30);
String randomName = names[(int)(Math.random() * names.length)];
return new User(randomName, randomAge);
};
User randomUser = randomUserSupplier.get();
System.out.println("Utilisateur aléatoire : " + randomUser);
Function : obtenir le nom de l’utilisateur
Function<User, String> getName = user -> user.getName();
List<String> names = users.stream()
.map(getName)
.toList();
System.out.println("Noms : " + names); // Noms : [Anna, Boris, Victoria, George]
5. Points utiles
Utilisation dans le Stream API : filter, map, forEach et autres
Rassemblons tout et écrivons une chaîne de transformations :
users.stream()
.filter(user -> user.getAge() >= 18) // Predicate<User>
.map(user -> user.getName().toUpperCase()) // Function<User, String>
.forEach(name -> System.out.println("Adulte : " + name)); // Consumer<String>
Résultat :
Adulte : ANNA
Adulte : VIKA
Tableau mémo : quoi utiliser où
| Où c’est utilisé | Quelle interface | Exemple d’utilisation |
|---|---|---|
| filter (Stream) | |
|
| map (Stream) | |
|
| forEach (Stream, List) | |
|
| generate (Stream) | |
|
Pourquoi est-il important de connaître les interfaces fonctionnelles ?
- Elles sont à la base de toutes les expressions lambda en Java.
- Elles permettent d’écrire du code universel, réutilisable et concis.
- Elles simplifient le travail avec les collections, les flux et les tâches asynchrones.
Quand utiliser quelle interface ?
- Predicate — lorsqu’il faut vérifier une condition (filtrer, rechercher).
- Consumer — lorsqu’il faut faire quelque chose avec l’objet (afficher, écrire, envoyer).
- Supplier — lorsqu’il faut obtenir ou générer un objet (fabriques, générateurs).
- Function — lorsqu’il faut transformer un objet d’un type vers un autre.
6. Erreurs typiques lors du travail avec les interfaces fonctionnelles
Erreur n° 1 : Mauvais choix d’interface. Les débutants confondent parfois Predicate et Function — par exemple, ils essaient de renvoyer un boolean depuis une Function au lieu d’un Predicate. Retenez bien : Predicate renvoie toujours un boolean, Function — tout autre type.
Erreur n° 2 : Ne pas utiliser les interfaces standard. On écrit souvent ses propres interfaces du genre « Checker » avec une méthode boolean check(T t) au lieu d’utiliser Predicate. Il vaut mieux utiliser les standard — elles sont supportées partout et rendent le code plus clair pour les autres développeurs.
Erreur n° 3 : Lambda trop complexe. Si une lambda se transforme en un mini-roman de 10 lignes, mieux vaut l’extraire dans une méthode ou une classe séparée. Une lambda, c’est pour la concision et la lisibilité.
Erreur n° 4 : Annotation @FunctionalInterface oubliée. Si vous écrivez votre propre interface fonctionnelle — n’oubliez pas l’annotation. Elle vous protège des erreurs accidentelles (comme l’ajout d’une seconde méthode abstraite).
Erreur n° 5 : Utilisation d’un état mutable à l’intérieur d’une lambda. Si une lambda modifie des variables externes ou des collections, cela peut conduire à des bugs inattendus, surtout dans un contexte concurrent. Mieux vaut éviter les effets de bord.
GO TO FULL VERSION