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 :
PredicateFunctionConsumerSupplier

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.

2. Les quatre interfaces majeures
A. Predicate<T> : pour tester une condition
- Rôle : Représente une fonction qui prend un objet de type
Tet retourne unboolean. 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
Tet le transforme en un objet de typeR. 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
Tpour 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
forEachque vous avez déjà utilisée attend unConsumer.
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, utilisezUUID.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, retourneboolean. (.test())Function<T, R>: Transforme unTenR. (.apply())Consumer<T>: Effectue une action sur unT, retournevoid. (.accept())Supplier<T>: Fournit unTsans 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.