Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 40

Interfaces Fonctionnelles Standards : Prédicat, Fonction, Consommateur, Fournisseur

Recommended resources

Bonjour ! Dans la leçon précédente, vous avez découvert les expressions lambda et appris à les utiliser pour implémenter des interfaces fonctionnelles, notamment pour trier des listes avec Comparator. Vous avez vu comment cette syntaxe concise peut remplacer la verbosité des classes anonymes.

Aujourd'hui, nous allons nous appuyer sur cette connaissance pour explorer un ensemble d'outils essentiels en Java moderne. L'objectif est d'apprendre à utiliser les interfaces fonctionnelles standards fournies par Java dans le package java.util.function. Plutôt que de créer nos propres interfaces pour chaque besoin, nous utiliserons un jeu d'interfaces prédéfinies pour les scénarios les plus courants. La maîtrise de ces interfaces est la dernière étape avant de pouvoir exploiter la pleine puissance de l'API Stream, que nous aborderons dans la prochaine leçon.

Nous nous concentrerons sur les quatre "piliers" de ce package :

  • Predicate
  • Function
  • Consumer
  • Supplier
Ce diagramme illustre les quatre catégories principales d'interfaces fonctionnelles standards que nous allons étudier aujourd'hui.

1. Le package java.util.function : une boîte à outils standard

Depuis Java 8, le JDK inclut le package java.util.function. Il contient une quarantaine d'interfaces fonctionnelles prêtes à l'emploi. Cette standardisation est très importante car elle permet aux développeurs de partager et de comprendre le code plus facilement. Par exemple, si une méthode accepte un Predicate, tout le monde sait qu'elle attend une fonction de test qui retourne un booléen.

Ces interfaces suivent une convention de nommage claire qui vous aidera à vous y retrouver.

Programmation fonctionnelle et lambdas — Le langage Java

Cet article de Gayerie.dev offre un excellent résumé des conventions de nommage.

Lisez la partie qui décrit les noms des interfaces : Function, Operator, Consumer, Supplier et Predicate. Cela vous donnera une carte mentale pour naviguer dans ce package. Ne vous souciez pas de tout retenir, nous allons détailler les plus importantes.

Concentrons-nous maintenant sur les quatre interfaces fondamentales que vous utiliserez 90% du temps. L'image suivante résume parfaitement leurs rôles et leurs méthodes.

Ce tableau récapitule la signature et la méthode de chaque interface fonctionnelle de base : Consumer, Predicate, Function et Supplier. Gardez-le en tête comme référence tout au long de cette leçon.

2. Les quatre interfaces majeures

A. Predicate<T> : pour tester une condition

  • Rôle : Représente une fonction qui prend un objet de type T et retourne un boolean. Son nom "Predicate" (prédicat en français) vient de la logique : il affirme quelque chose sur l'objet.
  • Méthode abstraite : boolean test(T t)
  • Usage typique : Filtrage de données, validation d'entrées. En backend, vous l'utiliserez pour vérifier si un utilisateur a les droits, si un produit est en stock, ou si un email est valide.

Exemple : Vérifier si un produit est cher.

import java.util.function.Predicate;

// Reprenons notre classe Product
class Product {
    private String name;
    private double price;
    // ... constructeur, getters, toString ...
    public Product(String name, double price) { this.name = name; this.price = price; }
    public String getName() { return name; }
    public double getPrice() { return price; }
    @Override
    public String toString() { return "Product{" + "name='" + name + '\'' + ", price=" + price + '}'; }
}

public class Main {
    public static void main(String[] args) {
        // Déclaration d'un Predicate
        Predicate<Product> isExpensive = product -> product.getPrice() > 100.0;

        Product screen = new Product("Écran 27 pouces", 349.00);
        Product mouse = new Product("Souris Gamer", 49.99);

        // Utilisation du Predicate
        System.out.println("L'écran est-il cher ? " + isExpensive.test(screen));   // Affiche true
        System.out.println("La souris est-elle chère ? " + isExpensive.test(mouse)); // Affiche false
    }
}

La vidéo suivante montre comment utiliser un Predicate dans un contexte un peu plus avancé, pour filtrer une collection. C'est une excellente transition vers l'API Stream.

TUTO Java : Expressions Lambdas et références sur méthodes

Dans cette vidéo, Dominique Liard utilise des lambdas et des interfaces fonctionnelles pour manipuler des collections.

Regardez la section qui commence par la manipulation d'une collection de langages. Cette partie montre un exemple concret d'utilisation de Predicate pour filtrer des chaînes de caractères. Observez bien comment le prédicat est d'abord défini, puis passé à la méthode filter.

B. Function<T, R> : pour transformer un objet

  • Rôle : Représente une fonction qui prend un objet de type T et le transforme en un objet de type R. C'est le pilier de la transformation de données.
  • Méthode abstraite : R apply(T t)
  • Usage typique : Extraire une information d'un objet, convertir un objet d'un type à un autre. En backend, c'est l'outil parfait pour mapper une entité de base de données en un DTO (Data Transfer Object) pour une API REST, ou pour extraire l'email de tous les utilisateurs d'une liste.

Exemple : Extraire le nom d'un produit.

import java.util.function.Function;

// ...

public class Main {
    public static void main(String[] args) {
        // Déclaration d'une Function
        // Prend un Product et retourne son nom (String)
        Function<Product, String> getNameFunction = product -> product.getName();
        
        Product keyboard = new Product("Clavier Mécanique", 129.50);

        // Utilisation de la Function
        String productName = getNameFunction.apply(keyboard);
        System.out.println("Nom du produit : " + productName); // Affiche "Nom du produit : Clavier Mécanique"
    }
}

C. Consumer<T> : pour effectuer une action

  • Rôle : Représente une opération qui "consomme" un objet de type T pour effectuer une action, sans rien retourner (void). On l'utilise pour ses effets de bord (side effects).
  • Méthode abstraite : void accept(T t)
  • Usage typique : Afficher un objet, l'enregistrer dans un fichier ou une base de données, l'envoyer par email. La méthode forEach que vous avez déjà utilisée attend un Consumer.

Exemple : Afficher les détails d'un produit en majuscules.

import java.util.function.Consumer;

// ...

public class Main {
    public static void main(String[] args) {
        // Déclaration d'un Consumer
        Consumer<Product> printInUpperCase = product -> 
            System.out.println(product.toString().toUpperCase());

        Product mousePad = new Product("Tapis de souris XL", 24.90);
        
        // Utilisation du Consumer
        printInUpperCase.accept(mousePad); // Affiche "PRODUCT{NAME='TAPIS DE SOURIS XL', PRICE=24.9}"
    }
}

D. Supplier<T> : pour fournir un objet

  • Rôle : Représente une fonction qui ne prend aucun argument mais qui "fournit" (supplies) un objet de type T. C'est une fabrique d'objets.
  • Méthode abstraite : T get()
  • Usage typique : Créer de nouvelles instances d'objets, générer des valeurs par défaut, fournir une connexion à une base de données d'un pool.

Exemple : Créer une instance par défaut d'un produit.

import java.util.function.Supplier;

// ...

public class Main {
    public static void main(String[] args) {
        // Déclaration d'un Supplier
        Supplier<Product> defaultProductSupplier = () -> new Product("Produit Inconnu", 0.0);

        // Utilisation du Supplier
        Product newProduct = defaultProductSupplier.get();
        System.out.println("Nouveau produit : " + newProduct); // Affiche "Nouveau produit : Product{name='Produit Inconnu', price=0.0}"
    }
}

Pour approfondir et voir plus d'exemples de code pour chacune de ces interfaces, vous pouvez consulter la ressource suivante.

Développons en Java - Les expressions lambda

Cet article de "Développons en Java" est très détaillé. Il couvre en profondeur le package java.util.function.

Parcourez les sections suivantes pour consolider ce que nous venons de voir : 12.4.4 : Lisez l'introduction et examinez le tableau qui liste de nombreuses interfaces. Repérez celles que nous étudions. 12.4.4.1. Les interfaces fonctionnelles de types Consumer 12.4.4.2. Les interfaces fonctionnelles de type Function 12.4.4.3. Les interfaces fonctionnelles de type Predicate 12.4.4.4. Les interfaces fonctionnelles de type Supplier Ne vous sentez pas obligé de comprendre chaque exemple de code complexe, mais concentrez-vous sur les exemples simples qui illustrent l'usage de base de chaque interface.


3. Exercice pratique : Traitement d'une commande

Mettons maintenant ces quatre interfaces en pratique. Imaginez que vous développez un système de e-commerce.

Votre mission :
Dans une nouvelle classe Main, complétez le code ci-dessous en créant des implémentations (avec des lambdas) pour les quatre interfaces fonctionnelles demandées.

Code de départ :

import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.UUID; // Pour générer un ID unique

class Order {
    private String orderId;
    private double amount;
    private boolean isPaid;

    public Order(String orderId, double amount, boolean isPaid) {
        this.orderId = orderId;
        this.amount = amount;
        this.isPaid = isPaid;
    }
    
    public double getAmount() { return amount; }
    public boolean isPaid() { return isPaid; }

    @Override
    public String toString() {
        return "Order{" + "orderId='" + orderId + '\'' + ", amount=" + amount + ", isPaid=" + isPaid + '}';
    }
}

public class Main {
    public static void main(String[] args) {
        // 1. Un prédicat pour vérifier si une commande est payée ET si son montant est supérieur à 50.0
        Predicate<Order> isEligibleForShipping = /* VOTRE LAMBDA ICI */;

        // 2. Une fonction pour extraire l'ID de la commande sous forme de message "ID de commande : XXX"
        Function<Order, String> getOrderConfirmationMessage = /* VOTRE LAMBDA ICI */;

        // 3. Un consommateur pour simuler l'envoi d'un email de notification
        Consumer<String> sendEmailNotification = /* VOTRE LAMBDA ICI (doit afficher le message dans la console) */;

        // 4. Un fournisseur pour créer une nouvelle commande avec un ID unique et un montant de 99.99, non payée
        Supplier<Order> createNewOrder = /* VOTRE LAMBDA ICI */;

        // --- Tests ---
        Order order1 = new Order("abc-123", 75.50, true);  // Éligible
        Order order2 = new Order("def-456", 25.00, true);  // Non éligible (montant trop bas)
        Order order3 = new Order("ghi-789", 150.00, false); // Non éligible (non payée)

        System.out.println("Test du Predicate :");
        processOrder(order1, isEligibleForShipping, getOrderConfirmationMessage, sendEmailNotification);
        processOrder(order2, isEligibleForShipping, getOrderConfirmationMessage, sendEmailNotification);
        processOrder(order3, isEligibleForShipping, getOrderConfirmationMessage, sendEmailNotification);
        
        System.out.println("\nTest du Supplier :");
        Order generatedOrder = createNewOrder.get();
        System.out.println("Commande générée : " + generatedOrder);

    }

    public static void processOrder(Order order, Predicate<Order> predicate, Function<Order, String> function, Consumer<String> consumer) {
        if (predicate.test(order)) {
            String message = function.apply(order);
            consumer.accept(message);
        } else {
            System.out.println("La commande " + order.getOrderId() + " n'est pas éligible à l'expédition.");
        }
    }
}

Indices :

  • Pour le Predicate, utilisez l'opérateur && (ET logique).
  • Pour le Supplier, utilisez UUID.randomUUID().toString() pour obtenir un ID unique sous forme de chaîne.
Cliquez ici pour voir la solution
// 1. Un prédicat pour vérifier si une commande est payée ET si son montant est supérieur à 50.0
Predicate<Order> isEligibleForShipping = order -> order.isPaid() && order.getAmount() > 50.0;

// 2. Une fonction pour extraire l'ID de la commande sous forme de message "ID de commande : XXX"
Function<Order, String> getOrderConfirmationMessage = order -> "ID de commande : " + order.getOrderId();

// 3. Un consommateur pour simuler l'envoi d'un email de notification
Consumer<String> sendEmailNotification = message -> System.out.println("Email envoyé pour -> " + message);

// 4. Un fournisseur pour créer une nouvelle commande avec un ID unique et un montant de 99.99, non payée
Supplier<Order> createNewOrder = () -> new Order(UUID.randomUUID().toString(), 99.99, false);

Conclusion

Félicitations ! Vous maîtrisez maintenant les quatre interfaces fonctionnelles les plus importantes de Java. Elles sont les briques de base de la programmation fonctionnelle en Java.

Points clés à retenir :

  • Predicate<T> : Teste un objet, retourne boolean. (.test())
  • Function<T, R> : Transforme un T en R. (.apply())
  • Consumer<T> : Effectue une action sur un T, retourne void. (.accept())
  • Supplier<T> : Fournit un T sans prendre d'argument. (.get())

Ces interfaces, combinées aux expressions lambda, permettent d'écrire un code plus déclaratif et expressif. Vous ne décrivez plus comment faire une boucle, mais ce que vous voulez faire (filtrer, transformer, consommer).

Dans la prochaine leçon, nous allons enfin assembler toutes ces pièces. Vous utiliserez ces interfaces pour manipuler des collections de manière élégante et efficace avec l'API Stream, en enchaînant des opérations comme filter (qui prend un Predicate), map (qui prend une Function) et forEach (qui prend un Consumer). Préparez-vous, c'est l'une des fonctionnalités les plus puissantes et les plus utilisées du Java moderne.