CodeGym /Cours /JAVA 25 SELF /Passer des fonctions en paramètres : exemples

Passer des fonctions en paramètres : exemples

JAVA 25 SELF
Niveau 49 , Leçon 2
Disponible

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).

1
Mission
JAVA 25 SELF, niveau 49, leçon 2
Bloqué
Parade des animaux : Formation par longueur des noms 🐘
Parade des animaux : Formation par longueur des noms 🐘
1
Mission
JAVA 25 SELF, niveau 49, leçon 2
Bloqué
Rapport sur les indicateurs "Surveillance Stellaire" 🔭
Rapport sur les indicateurs "Surveillance Stellaire" 🔭
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION