1. Passer un comportement plutôt que des données
Pourquoi passer une fonction en paramètre ?
Imaginez : vous avez une méthode qui trie une liste. Mais comment sait-elle exactement comment trier ? Par ordre alphabétique ? Par longueur ? Par date de naissance ? On pourrait écrire une méthode séparée pour chaque cas, mais cela deviendrait vite un enfer de copier-coller.
À la place, Java permet de passer un comportement (c’est-à-dire une fonction ou une lambda) qui définit comment trier, filtrer, transformer, etc. Cela rend le code flexible et réutilisable.
Exemple : tri avec un comparateur
List<String> names = List.of("Anna", "Boris", "Vika");
// On passe le comportement : comment comparer les éléments (par longueur de chaîne)
names.stream()
.sorted((a, b) -> a.length() - b.length())
.forEach(System.out::println);
Exemple : filtrage avec un prédicat
List<String> names = List.of("Anna", "Boris", "Vika");
// On passe le comportement : qui conserver (nom de plus de 4 caractères)
names.stream()
.filter(name -> name.length() > 4)
.forEach(System.out::println);
Dans les deux cas, vous n’avez pas « codé en dur » la logique dans la méthode : vous lui avez fourni un morceau de comportement qu’elle applique à chaque élément.
Bénéfices : moins de duplication, plus de flexibilité
Au lieu de dizaines de méthodes avec un code similaire mais des logiques différentes, vous écrivez une méthode universelle qui accepte le « quoi faire » sous forme de fonction. Cela fait gagner du temps, réduit les erreurs et rend le code plus facile à tester.
2. Syntaxe du passage de fonctions
Expressions lambda comme arguments
La façon la plus courante consiste à écrire directement, à l’endroit de l’appel, une lambda avec la flèche -> :
list.forEach(item -> System.out.println(item));
Ou plus court encore, si une méthode adaptée existe déjà : une référence de méthode (::) :
list.forEach(System.out::println);
Références de méthode (method references)
Si vous avez déjà une méthode appropriée, vous pouvez la passer par référence :: :
// Méthode classique
public static boolean isLongName(String name) {
return name.length() > 4;
}
// Passer une référence de méthode
names.stream()
.filter(MyClass::isLongName)
.forEach(System.out::println);
Cela fonctionne si la signature de la méthode correspond à celle attendue par l’interface fonctionnelle (Predicate<T>, Function<T, R>, Comparator<T>, etc.).
3. Exemples de la bibliothèque standard
Collections.sort et Comparator
List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Vika"));
// Tri par longueur du nom
names.sort((a, b) -> a.length() - b.length());
System.out.println(names); // [Vika, Anna, Boris]
Stream.filter et Predicate
List<String> names = List.of("Anna", "Boris", "Vika");
// Conserver uniquement les noms commençant par 'V'
names.stream()
.filter(name -> name.startsWith("V"))
.forEach(System.out::println); // Vika
Stream.map et Function
List<String> names = List.of("Anna", "Boris", "Vika");
// Convertir les noms en majuscules
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
Optional.ifPresent et Consumer
Optional<String> opt = Optional.of("Bonjour !");
// Si une valeur est présente, l'imprimer
opt.ifPresent(s -> System.out.println("Dans la chaîne: " + s));
4. Pratique : écrire nos propres méthodes avec des fonctions-paramètres
Il est temps de mettre en pratique ! Écrivons quelques méthodes qui acceptent une fonction en paramètre et l’utilisent en interne.
Exemple 1 : Méthode pour traiter les éléments d’une liste (forEach à notre manière)
Supposons que nous ayons une liste d’utilisateurs (User). Nous voulons exécuter une action sur chaque utilisateur — par exemple, afficher le nom, envoyer un e-mail, créditer des bonus, etc. Au lieu de « câbler » l’action, passons-la en paramètre — un Consumer<User> !
import java.util.List;
import java.util.function.Consumer;
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
}
public class UserProcessor {
// Méthode acceptant un Consumer<User>
public static void processUsers(List<User> users, Consumer<User> action) {
for (User user : users) {
action.accept(user);
}
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 30),
new User("Vika", 22)
);
// Afficher les noms de tous les utilisateurs
processUsers(users, user -> System.out.println("Nom: " + user.name));
// Créditer un bonus (pour l’exemple, on se contente d’afficher)
processUsers(users, user -> System.out.println(user.name + " a reçu un bonus !"));
}
}
Exemple 2 : Méthode pour filtrer des éléments (Predicate)
Écrivons une méthode qui ne renvoie que les utilisateurs qui satisfont une condition (par exemple, uniquement les majeurs) :
import java.util.List;
import java.util.ArrayList;
import java.util.function.Predicate;
public class UserFilter {
public static List<User> filterUsers(List<User> users, Predicate<User> condition) {
List<User> result = new ArrayList<>();
for (User user : users) {
if (condition.test(user)) {
result.add(user);
}
}
return result;
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 17),
new User("Vika", 22)
);
// Filtrer uniquement les majeurs
List<User> adults = filterUsers(users, user -> user.age >= 18);
adults.forEach(user -> System.out.println(user.name)); // Anna, Vika
}
}
Exemple 3 : Méthode de transformation (Function)
Et maintenant une méthode qui, à partir d’une liste d’utilisateurs, produit une liste de leurs noms (ou toute autre transformation) :
import java.util.List;
import java.util.ArrayList;
import java.util.function.Function;
public class UserMapper {
public static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
List<R> result = new ArrayList<>();
for (User user : users) {
result.add(mapper.apply(user));
}
return result;
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 17),
new User("Vika", 22)
);
// Obtenir la liste des noms
List<String> names = mapUsers(users, user -> user.name);
System.out.println(names); // [Anna, Boris, Vika]
// Obtenir la liste des âges
List<Integer> ages = mapUsers(users, user -> user.age);
System.out.println(ages); // [25, 17, 22]
}
}
Exemple 4 : Méthode génératrice (Supplier)
Parfois, on a besoin d’obtenir une valeur « à la demande » — par exemple générer un nombre aléatoire, créer un objet, obtenir l’heure courante. Pour cela, l’interface Supplier<T> convient.
import java.util.function.Supplier;
public class ValueGenerator {
public static int getValue(Supplier<Integer> supplier) {
return supplier.get();
}
public static void main(String[] args) {
// Obtenir un nombre aléatoire
int random = getValue(() -> (int)(Math.random() * 100));
System.out.println("Nombre aléatoire: " + random);
// Obtenir l’heure actuelle en millisecondes
long time = getValue(System::currentTimeMillis);
System.out.println("Temps: " + time);
}
}
5. Application unifiée : relier les exemples
Imaginons que nous développions une simple application « Liste des utilisateurs ». Nous savons déjà :
- Filtrer les utilisateurs selon une condition (par exemple, uniquement les majeurs) ;
- Transformer les utilisateurs en autre chose (noms, e-mail, âge) ;
- Exécuter des actions sur chaque utilisateur (impression, crédit de bonus) ;
- Générer des valeurs à la demande (par exemple, créer de nouveaux utilisateurs).
À présent, en utilisant toutes ces méthodes, nous pouvons construire des scénarios de traitement de données flexibles, sans réécrire le code à chaque nouvelle tâche.
import java.util.*;
import java.util.function.*;
public class UserApp {
static class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " (" + age + ")";
}
}
// Méthode universelle pour traiter les utilisateurs
static void processUsers(List<User> users, Consumer<User> action) {
for (User user : users) action.accept(user);
}
// Filtre universel
static List<User> filterUsers(List<User> users, Predicate<User> condition) {
List<User> result = new ArrayList<>();
for (User user : users) if (condition.test(user)) result.add(user);
return result;
}
// Transformateur universel
static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
List<R> result = new ArrayList<>();
for (User user : users) result.add(mapper.apply(user));
return result;
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 17),
new User("Vika", 22)
);
// 1. Afficher tous les utilisateurs
processUsers(users, user -> System.out.println("Utilisateur: " + user));
// 2. Ne sélectionner que les adultes
List<User> adults = filterUsers(users, user -> user.age >= 18);
System.out.println("Majeurs: " + adults);
// 3. Obtenir les noms des adultes
List<String> adultNames = mapUsers(adults, user -> user.name);
System.out.println("Noms des adultes: " + adultNames);
// 4. Créditer un bonus aux adultes
processUsers(adults, user -> System.out.println(user.name + " a reçu un bonus !"));
}
}
Pourquoi est-ce mieux que l’approche « classique » ?
Cette approche apporte de la flexibilité : les mêmes méthodes peuvent être utilisées dans des scénarios très différents simplement en passant d’autres fonctions. Le code ne se décline pas en une infinité de variantes « on filtre comme ceci », « on affiche comme cela » — nous avons des outils généraux qui fonctionnent partout. Il est donc plus compact, plus clair et moins sujet aux erreurs. En outre, ce style s’intègre parfaitement avec le Stream API moderne : en passant aux flux, vous ne devrez pas tout réapprendre — l’approche reste la même.
6. Erreurs typiques lors du passage de fonctions en paramètres
Erreur n° 1 : la signature ne correspond pas à l’interface attendue.
Si une méthode attend un Predicate<User> et que vous passez une lambda qui ne renvoie pas un boolean mais, par exemple, un String, le compilateur vous arrêtera net et n’autorisera pas la compilation du projet. Vérifiez que la valeur de retour et les paramètres correspondent à ce qui est attendu.
Erreur n° 2 : la lambda utilise des variables susceptibles de changer.
En Java, les expressions lambda ne peuvent utiliser que des variables final ou effectively final du contexte extérieur. Si vous essayez de modifier une telle variable à l’intérieur de la lambda, vous obtiendrez une erreur de compilation.
Erreur n° 3 : confusion entre interfaces.
Parfois, on veut passer, par exemple, un Consumer<T>, mais on écrit par erreur une fonction qui renvoie quelque chose (par exemple, une Function<T, R>). Assurez-vous que votre lambda renvoie exactement ce qu’il faut (ou rien — pour un Consumer).
Erreur n° 4 : des lambdas trop complexes directement dans les paramètres.
Si une lambda fait plus d’une ou deux lignes, mieux vaut l’extraire dans une variable ou une méthode distincte. Sinon, le code devient illisible et vous serez le seul à vous y retrouver (et encore, pas toujours).
GO TO FULL VERSION