Programmation fonctionnelle en Java : lambdas, interfaces et bonnes pratiques

Maitrisez la programmation fonctionnelle en Java : fonctions anonymes, expressions lambda et interfaces fonctionnelles. Exemples pratiques et bonnes pratiques pour un code plus propre.

Mahmoud DEVO
Mahmoud DEVO
December 27, 2025 7 min read
Programmation fonctionnelle en Java : lambdas, interfaces et bonnes pratiques

Introduction

Depuis Java 8, la programmation fonctionnelle a revolutionne la maniere dont les developpeurs ecrivent du code Java. Ce paradigme, inspire des langages comme Haskell et Scala, permet de traiter les fonctions comme des citoyens de premiere classe, c’est-a-dire des valeurs qui peuvent etre passees en argument, retournees par d’autres fonctions et stockees dans des variables.

La programmation fonctionnelle en Java repose sur trois piliers fondamentaux : les expressions lambda, les interfaces fonctionnelles et les references de methodes. Ces concepts permettent d’ecrire du code plus concis, plus lisible et plus facile a maintenir, tout en reduisant les erreurs liees aux effets de bord.

Dans cet article complet, nous allons explorer en profondeur ces concepts avec des exemples pratiques que vous pourrez directement appliquer dans vos projets. Nous verrons egalement les bonnes pratiques a adopter et les pieges courants a eviter pour tirer le meilleur parti de la programmation fonctionnelle en Java.


Concepts de base

Avant de plonger dans les exemples avances, il est essentiel de maitriser les concepts fondamentaux de la programmation fonctionnelle en Java.

1. Fonctions anonymes (Classes anonymes)

Avant Java 8, les fonctions anonymes etaient implementees via des classes anonymes. Cette approche, bien que fonctionnelle, necessitait beaucoup de code boilerplate.

// Approche traditionnelle avec classe anonyme
Runnable task = new Runnable() {
    @Override
    public void run() {
        System.out.println("Execution de la tache...");
        // Logique metier complexe
        for (int i = 0; i < 5; i++) {
            System.out.println("Iteration: " + i);
        }
    }
};
task.run();

// Comparateur avec classe anonyme
Comparator<String> comparator = new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
};

2. Expressions lambda

Les expressions lambda simplifient considerablement l’ecriture de fonctions anonymes. La syntaxe est plus concise et met en avant l’intention du code plutot que la mecanique.

// Syntaxe de base : (parametres) -> expression
Runnable task = () -> System.out.println("Tache executee !");

// Lambda avec un seul parametre (parentheses optionnelles)
Consumer<String> printer = message -> System.out.println(message);

// Lambda avec plusieurs parametres
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;

// Lambda avec bloc de code
Comparator<String> comparator = (s1, s2) -> {
    int len1 = s1.length();
    int len2 = s2.length();
    return Integer.compare(len1, len2);
};

// Utilisation pratique avec Collections
List<String> noms = Arrays.asList("Alice", "Bob", "Charlie", "David");
noms.sort((a, b) -> a.compareTo(b));
noms.forEach(nom -> System.out.println("Nom: " + nom));

3. Interfaces fonctionnelles

Une interface fonctionnelle est une interface qui ne contient qu’une seule methode abstraite. L’annotation @FunctionalInterface est optionnelle mais recommandee car elle garantit que l’interface reste fonctionnelle.

@FunctionalInterface
interface Calculateur {
    double calculer(double a, double b);

    // Methodes par defaut autorisees
    default void afficherResultat(double a, double b) {
        System.out.println("Resultat: " + calculer(a, b));
    }

    // Methodes statiques autorisees
    static Calculateur addition() {
        return (a, b) -> a + b;
    }
}

// Utilisation
Calculateur addition = (a, b) -> a + b;
Calculateur multiplication = (a, b) -> a * b;
Calculateur puissance = (a, b) -> Math.pow(a, b);

System.out.println(addition.calculer(10, 5));        // 15.0
System.out.println(multiplication.calculer(10, 5));   // 50.0
System.out.println(puissance.calculer(2, 10));        // 1024.0

4. Interfaces fonctionnelles standard du JDK

Java fournit un ensemble d’interfaces fonctionnelles dans le package java.util.function :

import java.util.function.*;

// Predicate<T> : T -> boolean
Predicate<String> estVide = String::isEmpty;
Predicate<Integer> estPositif = n -> n > 0;
Predicate<String> estLong = s -> s.length() > 10;

// Function<T, R> : T -> R
Function<String, Integer> longueur = String::length;
Function<Integer, String> convertir = n -> "Nombre: " + n;

// Consumer<T> : T -> void
Consumer<String> afficher = System.out::println;
Consumer<List<String>> vider = List::clear;

// Supplier<T> : () -> T
Supplier<LocalDateTime> maintenant = LocalDateTime::now;
Supplier<List<String>> nouvelleListe = ArrayList::new;

// BiFunction<T, U, R> : (T, U) -> R
BiFunction<String, String, String> concatener = String::concat;

// UnaryOperator<T> : T -> T
UnaryOperator<String> majuscules = String::toUpperCase;

// BinaryOperator<T> : (T, T) -> T
BinaryOperator<Integer> max = Integer::max;

5. References de methodes

Les references de methodes offrent une syntaxe encore plus concise pour les lambdas qui appellent simplement une methode existante.

List<String> noms = Arrays.asList("Alice", "Bob", "Charlie");

// Reference a une methode statique : Classe::methodeStatique
Function<String, Integer> parser = Integer::parseInt;

// Reference a une methode d'instance : objet::methode
String prefixe = "Utilisateur: ";
Function<String, String> avecPrefixe = prefixe::concat;

// Reference a une methode d'instance d'un type : Type::methode
Function<String, String> enMajuscules = String::toUpperCase;

// Reference a un constructeur : Classe::new
Supplier<ArrayList<String>> createList = ArrayList::new;
Function<Integer, int[]> createArray = int[]::new;

// Exemples pratiques
noms.stream()
    .map(String::toUpperCase)           // Reference de methode
    .filter(s -> s.length() > 3)        // Lambda
    .forEach(System.out::println);      // Reference de methode

Exemples pratiques avances

Traitement de collections avec Stream API

List<Employee> employees = Arrays.asList(
    new Employee("Alice", "IT", 75000),
    new Employee("Bob", "RH", 55000),
    new Employee("Charlie", "IT", 82000),
    new Employee("Diana", "Finance", 68000),
    new Employee("Eve", "IT", 71000)
);

// Filtrer, transformer et collecter
List<String> nomsIT = employees.stream()
    .filter(e -> "IT".equals(e.getDepartement()))
    .map(Employee::getNom)
    .sorted()
    .collect(Collectors.toList());

// Calculer des statistiques
DoubleSummaryStatistics stats = employees.stream()
    .mapToDouble(Employee::getSalaire)
    .summaryStatistics();

System.out.println("Salaire moyen: " + stats.getAverage());
System.out.println("Salaire max: " + stats.getMax());

// Grouper par departement
Map<String, List<Employee>> parDepartement = employees.stream()
    .collect(Collectors.groupingBy(Employee::getDepartement));

// Salaire total par departement
Map<String, Double> salaireParDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartement,
        Collectors.summingDouble(Employee::getSalaire)
    ));

Composition de fonctions

Function<Integer, Integer> doubler = x -> x * 2;
Function<Integer, Integer> ajouterDix = x -> x + 10;

// Composer des fonctions
Function<Integer, Integer> doublerPuisAjouter = doubler.andThen(ajouterDix);
Function<Integer, Integer> ajouterPuisDoubler = doubler.compose(ajouterDix);

System.out.println(doublerPuisAjouter.apply(5));  // (5*2)+10 = 20
System.out.println(ajouterPuisDoubler.apply(5));  // (5+10)*2 = 30

// Composition de predicats
Predicate<String> nonVide = s -> !s.isEmpty();
Predicate<String> commenceParA = s -> s.startsWith("A");
Predicate<String> validateur = nonVide.and(commenceParA);

System.out.println(validateur.test("Alice")); // true
System.out.println(validateur.test("Bob"));   // false

Bonnes pratiques

Adopter la programmation fonctionnelle en Java necessite de suivre certaines bonnes pratiques pour en tirer tous les benefices.

1. Privilegier l’immutabilite

// MAUVAIS : modification de l'etat externe
List<String> resultats = new ArrayList<>();
noms.forEach(nom -> resultats.add(nom.toUpperCase())); // Effet de bord !

// BON : approche fonctionnelle pure
List<String> resultats = noms.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());

2. Garder les lambdas courtes et lisibles

// MAUVAIS : lambda trop complexe
employees.stream()
    .filter(e -> {
        boolean estActif = e.isActif();
        boolean bonSalaire = e.getSalaire() > 50000;
        boolean bonDept = Arrays.asList("IT", "RH").contains(e.getDept());
        return estActif && bonSalaire && bonDept;
    })
    .collect(Collectors.toList());

// BON : extraire la logique dans une methode
private boolean estEmployeEligible(Employee e) {
    return e.isActif()
        && e.getSalaire() > 50000
        && List.of("IT", "RH").contains(e.getDept());
}

employees.stream()
    .filter(this::estEmployeEligible)
    .collect(Collectors.toList());

3. Utiliser les interfaces fonctionnelles standard

// MAUVAIS : creer une interface personnalisee inutile
@FunctionalInterface
interface StringProcessor {
    String process(String input);
}

// BON : utiliser Function<String, String>
Function<String, String> processor = String::toUpperCase;

4. Preferer les references de methodes quand c’est possible

// Acceptable
list.forEach(item -> System.out.println(item));

// Meilleur
list.forEach(System.out::println);

Pieges courants

1. Capture de variables mutables

// DANGEREUX : la variable doit etre effectivement finale
int compteur = 0;
list.forEach(item -> compteur++); // Erreur de compilation !

// SOLUTION : utiliser AtomicInteger ou reduce
AtomicInteger compteur = new AtomicInteger(0);
list.forEach(item -> compteur.incrementAndGet());

// OU mieux : utiliser count()
long count = list.stream().count();

2. Effets de bord dans les streams

// MAUVAIS : effets de bord imprevisibles
List<String> resultats = new ArrayList<>();
stream.parallel()
    .forEach(item -> resultats.add(item)); // Race condition !

// BON : utiliser collect
List<String> resultats = stream.parallel()
    .collect(Collectors.toList());

3. Reutilisation de streams

// ERREUR : un stream ne peut etre utilise qu'une fois
Stream<String> stream = list.stream();
stream.forEach(System.out::println);
stream.count(); // IllegalStateException !

// SOLUTION : creer un nouveau stream
list.stream().forEach(System.out::println);
long count = list.stream().count();

4. Performance avec les streams paralleles

// ATTENTION : parallel() n'est pas toujours benefique
// Pour les petites collections, le surcoût depasse le gain
List<String> petiteListe = Arrays.asList("a", "b", "c");
petiteListe.parallelStream() // Surcoût inutile !
    .map(String::toUpperCase)
    .collect(Collectors.toList());

// Utiliser parallel() uniquement pour les grandes collections
// et les operations couteuses
grandeCollection.parallelStream()
    .filter(this::operationCouteuse)
    .collect(Collectors.toList());

5. Optional et null

// MAUVAIS : melanger Optional et null
Optional<String> opt = null; // Ne jamais faire ca !

// BON
Optional<String> opt = Optional.empty();
Optional<String> opt = Optional.ofNullable(valeurPotentiellementNull);

// MAUVAIS : utiliser get() sans verification
String value = optional.get(); // NoSuchElementException possible !

// BON
String value = optional.orElse("valeur par defaut");
String value = optional.orElseThrow(() -> new CustomException("Valeur requise"));
optional.ifPresent(System.out::println);

Conclusion

La programmation fonctionnelle en Java represente une evolution majeure du langage qui permet d’ecrire du code plus expressif, plus maintenable et souvent plus performant. Les expressions lambda, les interfaces fonctionnelles et la Stream API forment un ecosysteme coherent qui transforme la maniere dont nous abordons les problemes de programmation.

Les points cles a retenir sont :

  • Les lambdas simplifient l’ecriture de fonctions anonymes
  • Les interfaces fonctionnelles definissent des contrats clairs pour les comportements
  • Les references de methodes offrent une syntaxe concise et lisible
  • L’immutabilite et l’absence d’effets de bord sont fondamentales
  • La Stream API permet des transformations de donnees elegantes

En adoptant progressivement ces concepts et en evitant les pieges courants, vous ameliorerez significativement la qualite de votre code Java. N’hesitez pas a experimenter avec ces techniques dans vos projets pour developper votre maitrise de la programmation fonctionnelle.


Pour aller plus loin

Pour approfondir vos connaissances en programmation fonctionnelle Java, explorez ces sujets avances :

  • Les Collectors personnalises pour des agregations complexes
  • Les spliterators pour le parallelisme avance
  • L’integration avec CompletableFuture pour la programmation asynchrone
  • Les bibliotheques comme Vavr pour une programmation fonctionnelle plus poussee
Advertisement

In-Article Ad

Dev Mode

Share this article

Mahmoud DEVO

Mahmoud DEVO

Senior Full-Stack Developer

I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.

Enjoyed this article?

Subscribe to get more tech content delivered to your inbox.

Related Articles