20. Programmation Fonctionnelle en Java
Table des matières
- 20.1 Interfaces Fonctionnelles
- 20.2 Expressions Lambda
- 20.3 Références de Méthodes
- 20.3.1 Référence à une Méthode Statique
- 20.3.2 Référence à une Méthode d’Instance d’un Objet Particulier
- 20.3.3 Référence à une Méthode d’Instance d’un Objet Arbitraire d’un Type Donné
- 20.3.4 Référence à un Constructeur
- 20.3.5 Tableau Récapitulatif des Types de Method Reference
- 20.3.6 Pièges Fréquents
La programmation fonctionnelle est un paradigme qui se concentre sur ce qui doit être fait plutôt que sur la manière de le faire.
À partir de Java 8, le langage a ajouté plusieurs fonctionnalités qui permettent un style “fonctionnel” : lambda expressions, functional interfaces et method references.
Ces fonctionnalités permettent d’écrire du code plus expressif, concis et réutilisable, en particulier lorsqu’on travaille avec des collections, des API de concurrence et des systèmes event-driven.
20.1 Interfaces Fonctionnelles
En Java, une interface fonctionnelle est une interface qui contient exactement une méthode abstraite.
Les interfaces fonctionnelles permettent les Lambda Expressions et les Method References, et constituent le cœur du modèle de programmation fonctionnelle de Java.
Note
Java considère automatiquement comme interface fonctionnelle toute interface ayant une seule méthode abstraite. L’annotation @FunctionalInterface est optionnelle mais recommandée.
20.1.1 Règles pour les Interfaces Fonctionnelles
- Exactement une méthode abstraite (SAM = Single Abstract Method).
- Une interface peut déclarer un nombre quelconque de méthodes default, static ou private.
- Elle peut redéfinir des méthodes de
Object(toString(),equals(Object),hashCode()) sans affecter le décompte SAM. - La méthode fonctionnelle peut provenir d’une super-interface.
Exemple :
@FunctionalInterface
interface Adder {
int add(int a, int b); // single abstract method
static void info() {}
default void log() {}
}
20.1.2 Interfaces Fonctionnelles Courantes (java.util.function)
Ci-dessous, un résumé des interfaces fonctionnelles les plus importantes.
| Functional Interface | Returns | Method | Parameters |
|---|---|---|---|
Supplier<T> |
T | get() | 0 |
Consumer<T> |
void | accept(T) | 1 |
BiConsumer<T,U> |
void | accept(T,U) | 2 |
Function<T,R> |
R | apply(T) | 1 |
BiFunction<T,U,R> |
R | apply(T,U) | 2 |
UnaryOperator<T> |
T | apply(T) | 1 (mêmes types) |
BinaryOperator<T> |
T | apply(T,T) | 2 (mêmes types) |
Predicate<T> |
boolean | test(T) | 1 |
BiPredicate<T,U> |
boolean | test(T,U) | 2 |
- Exemples
Supplier<String> sup = () -> "Hello!";
Consumer<String> printer = s -> System.out.println(s);
Function<String, Integer> length = s -> s.length();
UnaryOperator<Integer> square = x -> x * x;
Predicate<Integer> positive = x -> x > 0;
20.1.3 Méthodes de Commodité sur les Interfaces Fonctionnelles
De nombreuses interfaces fonctionnelles proposent des méthodes utilitaires permettant l’enchaînement et la composition.
| Interface | Method | Description |
|---|---|---|
| Function | andThen() | applique la fonction, puis l’autre spécifiée dans cette méthode additionnelle |
| Function | compose() | applique la fonction spécifiée dans cette méthode additionnelle, puis la fonction |
| Function | identity() | renvoie une fonction x -> x |
| Predicate | and() | ET logique |
| Predicate | or() | OU logique |
| Predicate | negate() | NON logique |
| Consumer | andThen() | chaîne des consumers |
| BinaryOperator | minBy() | minimum basé sur un comparator |
| BinaryOperator | maxBy() | maximum basé sur un comparator |
- Exemples
Function<Integer, Integer> times2 = x -> x * 2;
Function<Integer, Integer> plus3 = x -> x + 3;
var result1 = times2.andThen(plus3).apply(5); // (5*2)+3 = 13
var result2 = times2.compose(plus3).apply(5); // (5+3)*2 = 16
Predicate<String> longString = s -> s.length() > 5;
Predicate<String> startsWithA = s -> s.startsWith("A");
boolean ok = longString.and(startsWithA).test("Amazing"); // true
20.1.4 Interfaces Fonctionnelles Primitives
Java fournit des versions spécialisées des interfaces fonctionnelles pour les types primitifs afin d’éviter le coût du boxing/unboxing.
| Functional Interface | Return Type | Single Abstract Method | # Parameters |
|---|---|---|---|
| IntSupplier | int | getAsInt() | 0 |
| LongSupplier | long | getAsLong() | 0 |
| DoubleSupplier | double | getAsDouble() | 0 |
| BooleanSupplier | boolean | getAsBoolean() | 0 |
| IntConsumer | void | accept(int) | 1 (int) |
| LongConsumer | void | accept(long) | 1 (long) |
| DoubleConsumer | void | accept(double) | 1 (double) |
| IntPredicate | boolean | test(int) | 1 (int) |
| LongPredicate | boolean | test(long) | 1 (long) |
| DoublePredicate | boolean | test(double) | 1 (double) |
| IntUnaryOperator | int | applyAsInt(int) | 1 (int) |
| LongUnaryOperator | long | applyAsLong(long) | 1 (long) |
| DoubleUnaryOperator | double | applyAsDouble(double) | 1 (double) |
| IntBinaryOperator | int | applyAsInt(int, int) | 2 (int,int) |
| LongBinaryOperator | long | applyAsLong(long, long) | 2 (long,long) |
| DoubleBinaryOperator | double | applyAsDouble(double,double) | 2 |
| IntFunction |
R | apply(int) | 1 (int) |
| LongFunction |
R | apply(long) | 1 (long) |
| DoubleFunction |
R | apply(double) | 1 (double) |
| ToIntFunction |
int | applyAsInt(T) | 1 (T) |
| ToLongFunction |
long | applyAsLong(T) | 1 (T) |
| ToDoubleFunction |
double | applyAsDouble(T) | 1 (T) |
| ToIntBiFunction |
int | applyAsInt(T,U) | 2 (T,U) |
| ToLongBiFunction |
long | applyAsLong(T,U) | 2 (T,U) |
| ToDoubleBiFunction |
double | applyAsDouble(T,U) | 2 (T,U) |
| ObjIntConsumer |
void | accept(T,int) | 2 (T,int) |
| ObjLongConsumer |
void | accept(T,long) | 2 (T,long) |
| ObjDoubleConsumer |
void | accept(T,double) | 2 (T,double) |
| DoubleToIntFunction | int | applyAsInt(double) | 1 |
| DoubleToLongFunction | long | applyAsLong(double) | 1 |
| IntToDoubleFunction | double | applyAsDouble(int) | 1 |
| IntToLongFunction | long | applyAsLong(int) | 1 |
| LongToDoubleFunction | double | applyAsDouble(long) | 1 |
| LongToIntFunction | int | applyAsInt(long) | 1 |
- Exemple
IntSupplier dice = () -> (int)(Math.random() * 6) + 1;
IntPredicate even = x -> x % 2 == 0;
IntUnaryOperator doubleIt = x -> x * 2;
20.1.5 Résumé
- Les interfaces fonctionnelles contiennent exactement une méthode abstraite (SAM).
- Elles sont le support des Lambdas et des Method References.
- Java propose de nombreuses FI intégrées dans
java.util.function. - Les variantes primitives améliorent les performances en supprimant le boxing.
20.2 Expressions Lambda
Une expression lambda est une manière compacte d’écrire une fonction.
Les expressions lambda offrent une façon concise de définir des implémentations d’interfaces fonctionnelles.
Une lambda est essentiellement un petit bloc de code qui prend des paramètres et renvoie une valeur, sans nécessiter une déclaration complète de méthode.
Elles représentent le comportement comme une donnée et constituent un élément clé du modèle de programmation fonctionnelle en Java.
20.2.1 Syntaxe des Expressions Lambda
La syntaxe générale est :
(parameters) -> expression
ou
(parameters) -> { statements }
Important
Une expression lambda n’introduit pas un nouveau scope pour les variables. Par conséquent, les noms de variables déjà présents dans le contexte environnant ne peuvent pas être redéclarés comme paramètres de l’expression lambda.
20.2.2 Exemples de Syntaxe Lambda
Zéro paramètre
Runnable r = () -> System.out.println("Hello");
Un paramètre (parenthèses optionnelles)
Consumer<String> c = s -> System.out.println(s);
Plusieurs paramètres
BinaryOperator<Integer> add = (a, b) -> a + b;
Avec un corps en bloc
Function<Integer, String> f = (x) -> {
int doubled = x * 2;
return "Value: " + doubled;
};
20.2.3 Règles pour les Expressions Lambda
- Les types des paramètres peuvent être omis (inférence de type).
- Si un paramètre a un type, alors tous les paramètres doivent spécifier un type.
- Un seul paramètre ne nécessite pas de parenthèses.
- Plusieurs paramètres nécessitent des parenthèses.
- Si le corps est une seule expression (sans
{ }),returnest interdit ; l’expression elle-même est la valeur de retour. - Si le corps utilise
{ }(un bloc),returndoit apparaître si une valeur est renvoyée. - Les expressions lambda ne peuvent être assignées qu’à des interfaces fonctionnelles (types SAM).
20.2.4 Inférence de Type
Le compilateur déduit le type de la lambda à partir du contexte de l’interface fonctionnelle cible.
Predicate<String> p = s -> s.isEmpty(); // s déduit comme String
Si le compilateur ne peut pas déduire le type, il faut le préciser explicitement.
BiFunction<Integer, Integer, Integer> f = (Integer a, Integer b) -> a * b;
20.2.5 Restrictions dans les Corps des Lambda
Les lambdas ne peuvent capturer que des variables locales final ou effectively final (non réassignées).
int x = 10;
Runnable r = () -> {
// x++; // ❌ erreur de compilation — x doit être effectively final
System.out.println(x);
};
Elles peuvent en revanche modifier l’état d’un objet (seules les références doivent être effectively final).
var list = new ArrayList<>();
Runnable r2 = () -> list.add("OK"); // autorisé
20.2.6 Règles de Type de Retour
Si le corps est une expression : l’expression est la valeur de retour.
Function<Integer, Integer> f = x -> x * 2;
Si le corps est un bloc : il faut inclure return.
Function<Integer, Integer> g = x -> {
return x * 2;
};
20.2.7 Lambdas vs Classes Anonymes
- Les lambdas ne créent PAS une nouvelle portée : elles partagent la portée englobante.
thisdans une lambda fait référence à l’objet englobant, pas à la lambda.
class Test {
void run() {
Runnable r = () -> System.out.println(this.toString());
}
}
Dans une classe anonyme, this fait référence à l’instance de la classe anonyme.
20.2.8 Erreurs Courantes
Types de retour incohérents
x -> { if (x > 0) return 1; } // ❌ manque un return pour le cas négatif
Mélanger paramètres typés et non typés
(a, int b) -> a + b // ❌ illégal
Renvoyer une valeur pour une lambda ciblant void
Runnable r = () -> 5; // ❌ Runnable.run() retourne void
Résolution d’overload ambiguë
void m(IntFunction<Integer> f) {}
void m(Function<Integer, Integer> f) {}
m(x -> x + 1); // ❌ ambigu
20.3 Références de Méthodes
Les références de méthodes (method references) fournissent une syntaxe abrégée pour utiliser une méthode existante comme implémentation d’une interface fonctionnelle.
Elles sont équivalentes aux expressions lambda, mais plus concises, plus lisibles, et souvent préférées lorsque la méthode cible existe déjà.
Il existe quatre catégories de références de méthodes en Java :
- Référence à une méthode statique (
ClassName::staticMethod) - Référence à une méthode d’instance d’un objet particulier (
instance::method) - Référence à une méthode d’instance d’un objet arbitraire d’un type donné (
ClassName::instanceMethod) - Référence à un constructeur (
ClassName::new)
20.3.1 Référence à une Méthode Statique
Une référence à méthode statique remplace une lambda qui appelle une méthode statique.
class Utils {
static int square(int x) { return x * x; }
}
Function<Integer, Integer> f1 = x -> Utils.square(x);
Function<Integer, Integer> f2 = Utils::square; // method reference
f1 et f2 se comportent de manière identique.
20.3.2 Référence à une Méthode d’Instance d’un Objet Particulier
Utilisée lorsque vous avez déjà une instance d’objet et que vous voulez référencer l’une de ses méthodes.
String prefix = "Hello, ";
UnaryOperator<String> op1 = s -> prefix.concat(s);
UnaryOperator<String> op2 = prefix::concat; // method reference
System.out.println(op2.apply("World"));
La référence prefix::concat lie concat à cet objet spécifique.
20.3.3 Référence à une Méthode d’Instance d’un Objet Arbitraire d’un Type Donné
C’est la forme la plus “piégeuse”.
Le premier paramètre de l’interface fonctionnelle devient le receiver de la méthode (this).
BiPredicate<String, String> p1 = (s1, s2) -> s1.equals(s2);
BiPredicate<String, String> p2 = String::equals; // method reference
System.out.println(p2.test("abc", "abc")); // true
Note
Cette forme applique la méthode au premier argument de la lambda.
20.3.4 Référence à un Constructeur
Les références de constructeurs remplacent des lambdas qui appellent new.
Supplier<ArrayList<String>> sup1 = () -> new ArrayList<>();
Supplier<ArrayList<String>> sup2 = ArrayList::new; // method reference
Function<Integer, ArrayList<String>> sup3 = ArrayList::new;
// appelle le constructeur ArrayList(int capacity)
20.3.5 Tableau Récapitulatif des Types de Method Reference
Le tableau ci-dessous résume toutes les catégories de références de méthodes.
| Type | Syntax Example | Equivalent Lambda |
|---|---|---|
| Static method | Class::staticMethod | x -> Class.staticMethod(x) |
| Instance method of specific object | instance::method | x -> instance.method(x) |
| Instance method of arbitrary object | Class::method | (obj, x) -> obj.method(x) |
| Constructor | Class::new | () -> new Class() |
20.3.6 Pièges Fréquents
- Une référence de méthode doit correspondre exactement à la signature de l’interface fonctionnelle.
- Les overloads peuvent rendre une référence de méthode ambiguë.
- La référence à méthode d’instance (
Class::method) décale le receiver sur le paramètre 1. - Une référence de constructeur échoue s’il n’existe pas de constructeur compatible.
// ❌ Ambigu : quel println()? (println(int), println(String)...)
Consumer<String> c = System.out::println; // OK uniquement parce que le paramètre FI est String
// ❌ Constructeur non compatible : mauvaise interface fonctionnelle
Supplier<Integer> s = Integer::new; // ✔ OK : appelle Integer()
Function<String, Long> f = Integer::new; // ❌ ERREUR : le constructeur retourne Integer, pas Long
En cas de doute, réécrivez la method reference en lambda : si la lambda fonctionne mais pas la method reference, le problème est généralement un mismatch de signature.