Découvre les notions avancées suivantes en Java : les types génériques, les classes anonymes, les interfaces fonctionnelles et les lambdas.
- Oracle - Generic Types
- Oracle - Package java.util.function
- Functional Interfaces in Java
- Java Lambda Expressions
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.
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 123Pour é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 castTu trouveras plus d'informations sur le lien suivant :
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.
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éthodevoid accept(T t), qui prend en argument un élément de typeTafin d'effectuer un traitement qui ne retourne aucune valeurSupplier<T>: possède une méthodeT get(), qui retourne un élément de typeTPredicate<T>: possède une méthodeboolean test(T t), qui effectue un test sur une valeur de typeTet retourne un booléenFunction<T,R>: possède une méthodeR apply(T t), qui prend un élément de typeTet retourne un résultat de typeR
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.0Si 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.
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 == 0Reprenons 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.0Les 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 :
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
SimpleTaskavec un attributcontentde typeString. Ajoute un constructeur pour initialisercontentet 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
ComplexTaskavec les attributstitle,descriptionetdone. 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
taskde type génériqueT. - Ajoute une méthode
addpour stocker une tâche. - Ajoute une méthode
getpour 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 statutdone.
Étape 5 : Utiliser Consumer pour afficher le contenu de la tâche.
- Crée une classe anonyme qui implémente l'interface fonctionnelle
Consumerafin 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.
En autonomie, sur une demi-journée.
Il sera vérifié que :
- Le code s'exécute sans erreur.
- Un lien vers un dépôt GitLab.
- 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.