CodeGym /Cours /JAVA 25 SELF /Interfaces fonctionnelles : Predicate, Consumer, Sup...

Interfaces fonctionnelles : Predicate, Consumer, Supplier, Function

JAVA 25 SELF
Niveau 49, Leçon 0
Disponible

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
Predicate<T>
T
boolean
Vérification d’une condition (filtrage)
Consumer<T>
T
void
Exécuter une action sur l’objet
Supplier<T>
rien
T
Obtenir/générer un objet
Function<T, R>
T
R
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)
Predicate<T>
filter(u -> u.getAge() > 18)
map (Stream)
Function<T, R>
map(u -> u.getName())
forEach (Stream, List)
Consumer<T>
forEach(u -> System.out.println(u))
generate (Stream)
Supplier<T>
Stream.generate(() -> ...)

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.

1
Mission
JAVA 25 SELF, niveau 49, leçon 0
Bloqué
Analyse des titres de livres à la Bibliothèque de Science-Fiction 📚
Analyse des titres de livres à la Bibliothèque de Science-Fiction 📚
1
Mission
JAVA 25 SELF, niveau 49, leçon 0
Bloqué
Gestion des indicateurs de jeu dans "Royaume Virtuel" 🎮
Gestion des indicateurs de jeu dans "Royaume Virtuel" 🎮
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION