Skip to content

Instantly share code, notes, and snippets.

@bastienapp
Created July 28, 2026 11:54
Show Gist options
  • Select an option

  • Save bastienapp/810d5b6f3c0ee5a57d4761bf763a8e67 to your computer and use it in GitHub Desktop.

Select an option

Save bastienapp/810d5b6f3c0ee5a57d4761bf763a8e67 to your computer and use it in GitHub Desktop.
Java - Lambdas et autres notions avancées

Java - Lambdas et autres notions avancées

Découvre les notions avancées suivantes en Java : les types génériques, les classes anonymes, les interfaces fonctionnelles et les lambdas.

Ressources

Contexte du projet

Types génériques

En lisant la documentation de certaines classes, tu as peut-être déjà été amené à voir des éléments inhabituels comme <E>, <T> ou encore <K, V>.

Il s'agit de types génériques : ces lettres représentent un type qui sera défini lors de l'instanciation de la classe.

Un exemple courant est la déclaration d'une liste : new ArrayList<String>(). Ici, la classe est ArrayList<E> et, lors de la création de l'instance, on précise que E sera de type String.

Tu te demandes peut-être pourquoi utiliser un type générique plutôt que la classe Object. En Java, toutes les classes héritent de Object : tous les objets sont donc des instances de Object. L'exemple suivant va te montrer la différence entre ces deux notions.

1. Utilisation de Object

Imaginons l'exemple suivant : une classe Box qui peut stocker n'importe quel type d'objet.

public class Box {
    private Object object;

    public void set(Object object) {
        this.object = object;
    }

    public Object get() {
        return object;
    }
}

Cette classe fonctionne, mais elle n'offre aucune garantie concernant le type. Tu peux stocker n'importe quel type d'objet, mais tu dois effectuer un cast, c'est-à-dire forcer le type, lors de la récupération. Cela peut provoquer des erreurs à l'exécution.

Box integerBox = new Box();
integerBox.set(123);

// Il faut faire un cast car get() retourne un Object
Integer number = (Integer) integerBox.get();
System.out.println(number);  // Affiche 123

2. Utilisation des types génériques

Pour éviter ces problèmes, tu peux introduire un type générique dans ta classe Box :

public class Box<T> {
    private T object;

    public void set(T object) {
        this.object = object;
    }

    public T get() {
        return object;
    }
}

Avec cette version générique, tu peux créer des Box pour n'importe quel type tout en garantissant la sécurité des types :

Box<Integer> integerBox = new Box<>();
integerBox.set(123);
Integer number = integerBox.get();  // Pas besoin de cast

Tu trouveras plus d'informations sur le lien suivant :

Classes anonymes

Une classe anonyme est une classe sans nom, définie et instanciée dans une seule expression. Elle est souvent utilisée pour implémenter une interface ou étendre une classe abstraite.

Supposons que tu aies une interface Printer avec une méthode print :

interface Printer {
    void print(String message);
}

Habituellement, tu créerais une classe qui implémente cette interface :

public class ConsolePrinter implements Printer {
    @Override
    public void print(String message) {
        System.out.println(message);
    }
}

Au lieu de créer une classe distincte, tu peux utiliser une classe anonyme :

Printer printer = new Printer() {
    @Override
    public void print(String message) {
        System.out.println(message);
    }
};
printer.print("Hello, World!");

Les classes anonymes sont utiles pour créer des implémentations rapides et spécifiques, lorsqu'une classe complète serait superflue.

Interfaces fonctionnelles

Une interface fonctionnelle est une interface qui ne contient qu'une seule méthode abstraite. Tu ne seras probablement pas amené à écrire tes propres interfaces fonctionnelles. Cependant, Java en possède plusieurs que tu devras utiliser, par exemple :

  • Consumer<T> : possède une méthode void accept(T t), qui prend en argument un élément de type T afin d'effectuer un traitement qui ne retourne aucune valeur
  • Supplier<T> : possède une méthode T get(), qui retourne un élément de type T
  • Predicate<T> : possède une méthode boolean test(T t), qui effectue un test sur une valeur de type T et retourne un booléen
  • Function<T,R> : possède une méthode R apply(T t), qui prend un élément de type T et retourne un résultat de type R

Tu trouveras l'ensemble des interfaces fonctionnelles de Java dans la documentation :

Voici un exemple d'utilisation de Supplier<T> :

Supplier<String> stringSupplier = new Supplier<String>() {
    @Override
    public String get() {
        return "Hello, World!";
    }
};
System.out.println(stringSupplier.get());  // Affiche : Hello, World!

Supplier<Double> randomSupplier = new Supplier<Double>() {
    @Override
    public Double get() {
        return Math.random();
    }
};
System.out.println(randomSupplier.get());  // Affiche un nombre aléatoire entre 0.0 et 1.0

Si tu souhaites créer tes propres interfaces fonctionnelles, je te conseille la lecture suivante :

Même si, pour le moment, créer une interface avec une seule méthode peut te paraître étrange, le code Java est grandement simplifié lorsqu'on combine une interface fonctionnelle avec des lambdas.

Lambdas

Les lambdas, ou fonctions fléchées, sont des expressions qui simplifient l'instanciation d'interfaces fonctionnelles.

La syntaxe d'une lambda est la suivante :

(parameter1, parameter2) -> { code block }

Par exemple :

(int a, int b) -> {
    int result = a + b;
    System.out.println(result);
}

Lorsqu'il n'y a qu'un seul paramètre, les parenthèses sont facultatives. Dans le cas d'une expression composée d'une seule ligne et nécessitant un retour, les accolades et le mot-clé return doivent être supprimés.

parameter -> expression

Par exemple :

a -> a % 2 == 0

1. Avec les interfaces fonctionnelles

Reprenons le code de notre précédent exemple utilisant Supplier afin de le simplifier avec des lambdas :

Supplier<String> stringSupplier = () -> "Hello, World!";
System.out.println(stringSupplier.get());  // Affiche : Hello, World!

Supplier<Double> randomSupplier = () -> Math.random();
System.out.println(randomSupplier.get());  // Affiche un nombre aléatoire entre 0.0 et 1.0

2. Avec les méthodes standards

Les lambdas sont particulièrement puissantes lorsqu'elles sont combinées avec des méthodes standard de Java comme forEach, filter ou map. Par exemple :

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.forEach(n -> System.out.println(n * 2));

Tu trouveras plus d'informations sur l'utilisation des lambdas dans ces documentations :

Mise en pratique

Tu travailles sur un système de gestion de tâches. Tu vas implémenter plusieurs fonctionnalités en utilisant les notions de types génériques, de classes anonymes, d'interfaces fonctionnelles et de lambdas.

Étape 1 : Créer une classe SimpleTask, qui possède un attribut content (String).

  • Instruction : Crée une classe SimpleTask avec un attribut content de type String. Ajoute un constructeur pour initialiser content et une méthode getter pour accéder à son contenu.

Étape 2 : Créer une classe ComplexTask, qui possède les attributs title (String), description (String) et done (boolean).

  • Instruction : Crée une classe ComplexTask avec les attributs title, description et done. Ajoute un constructeur pour initialiser ces attributs, ainsi que des méthodes getter pour accéder à leurs valeurs.

Étape 3 : Créer une classe générique TaskContainer, qui peut contenir un objet de n'importe quel type T.

  • La classe doit posséder un constructeur vide.
  • Ajoute un attribut task de type générique T.
  • Ajoute une méthode add pour stocker une tâche.
  • Ajoute une méthode get pour récupérer la tâche.

Étape 4 : Utiliser la classe TaskContainer pour stocker une tâche.

  • Crée une instance de TaskContainer<SimpleTask>, puis stocke une tâche simple contenant du texte.
  • Crée une instance de TaskContainer<ComplexTask>, puis stocke une tâche complexe avec un titre, une description et un statut done.

Étape 5 : Utiliser Consumer pour afficher le contenu de la tâche.

  • Crée une classe anonyme qui implémente l'interface fonctionnelle Consumer afin d'afficher le contenu de la tâche simple créée à l'étape 4.

Le début du code sera le suivant :

Consumer<SimpleTask> simpleTaskPrinter = // ici créer la classe anonyme
  • Fais de même pour afficher le contenu d'une tâche complexe.

Étape 6 : Simplifier avec les Lambdas.

  • Remplace les classes anonymes créées à l'étape 5 par des lambdas afin de rendre le code plus concis.

Modalités pédagogiques

En autonomie, sur une demi-journée.

Modalités d'évaluation

Il sera vérifié que :

  • Le code s'exécute sans erreur.

Livrables

  • Un lien vers un dépôt GitLab.

Critères de performance

  • Le code source est documenté.
  • Utiliser les normes de codage du langage.
  • La documentation technique de l’environnement de travail est comprise.
  • Utiliser un outil de gestion de versions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment