POO en Java · L2 · Section 3/7
Interfaces, génériques, records
Progression
#Interfaces, génériques et records modernes
Les génériques ne sont pas une syntaxe rebutante : ils documentent les contrats, éliminent les conversions hasardeuses et déplacent des erreurs de l'exécution vers la compilation. Ce chapitre part d'un dépôt e-commerce : comment typer un Repository pour qu'il accepte des agrégats spécialisés tout en gardant une API homogène ? La réponse passe par les bornes, la variance et les records.
Prérequis : chapitres Modélisation objet et Héritage/composition ; interfaces et classes abstraites.
Objectifs : lire et écrire des signatures génériques avec variance (règle PECS) ; combiner records et interfaces scellées pour des hiérarchies fermées ; exploiter le pattern matching et les
switchexhaustifs sur records.
#1. Un contrat générique minimal
Un dépôt expose quatre gestes : chercher par identifiant, sauvegarder, tout récupérer, supprimer. La généricité rend ce contrat réutilisable pour chaque agrégat :
1import java.util.Collection;2import java.util.List;3import java.util.Optional;4 5public interface Repository<T, ID> {6 Optional<T> findById(ID id);7 void save(T aggregate);8 <S extends T> void saveAll(Collection<S> aggregates);9 List<T> findAll();10 void deleteById(ID id);11}Deux paramètres de type (T l'agrégat, ID l'identifiant) suffisent. La méthode saveAll mérite un regard : <S extends T> accepte une collection de sous-types de T (par exemple List<PremiumClient> là où T = Client) alors qu'une signature naïve en Collection<T> l'aurait refusée.
#2. Variance et règle PECS
En Java, les génériques sont invariants : List<PremiumClient> n'est pas un List<Client>. Sans cette règle, on pourrait insérer un Client ordinaire dans une liste promise comme liste de clients premium. Les jokers bornés réintroduisent de la souplesse là où elle est sûre :
? extends T: producteur, on ne fait que lire desT.? super T: consommateur, on ne fait qu'écrire desT.
Moyen mnémotechnique PECS : Producer Extends, Consumer Super. Concrètement :
1import java.util.List;2import java.util.function.Consumer;3 4/** Lit des prix (producteur) : borne extends. */5static double total(List<? extends Tarif> tarifs) {6 double somme = 0;7 for (Tarif t : tarifs) { // lecture : toujours un Tarif8 somme += t.montant();9 }10 return somme;11}12 13/** Reçoit des clients (consommateur) : borne super. */14static void notifier(List<? super Client> dest, Client c) {Un List<? extends Tarif> interdit add : impossible de garantir que l'élément inséré respecte la borne réelle. Cette frustration est la preuve que le typage fonctionne.
#3. Records et interfaces scellées : hiérarchies fermées
Depuis Java 16 (records) et 17 (scellés), on assemble des hiérarchies de données fermées : l'ensemble des cas est connu du compilateur. Modélisons les commandes applicatives d'un moteur de commandes e-commerce :
1import java.util.List;2import java.util.UUID;3 4public sealed interface Commande permits CreerCommande, ExpedierCommande, AnnulerCommande {}5 6public record CreerCommande(UUID idCommande, List<Ligne> lignes) implements Commande {7 public CreerCommande { // constructeur compact : défense8 if (lignes == null || lignes.isEmpty()) {9 throw new IllegalArgumentException("Une commande contient au moins une ligne");10 }11 lignes = List.copyOf(lignes); // copie immuable : pas de mutation externe12 }13}14 Chaque record encode exactement les données nécessaires à son cas ; les listes sont copiées en versions immuables pour que personne ne modifie la commande après construction.
#4. Pattern matching et switch moderne
Java 21 (JEP 441) permet des switch qui déstructurent directement les records, avec gardes when pour les contraintes métier. Le compilateur vérifie l'exhaustivité sur les types scellés :
1static String decrire(Commande commande) {2 return switch (commande) {3 case CreerCommande(var id, var lignes) when lignes.size() > 10 ->4 "Création (grosse commande) : " + id;5 case CreerCommande(var id, var lignes) ->6 "Création : " + id + " (" + lignes.size() + " lignes)";7 case ExpedierCommande(var id, var transporteur) ->8 "Expédition : " + id + " via " + transporteur;9 case AnnulerCommande(var id, var motif) ->10 "Annulation : " + id + " (" + motif + ")";11 };12}Supprimez le cas AnnulerCommande : la compilation échoue avec « the switch expression does not cover all possible input values ». Cette erreur à la compilation remplace des heures de tests. Deux règles d'écriture : les gardes spécifiques avant les cas généraux (l'ordre compte), et un switch exhaustif n'a pas besoin de default.
#5. Atelier : un mini DSL de remboursement
Modélisez un petit langage de politiques de remboursement avec interfaces scellées et records :
- Trois expressions :
RemboursementTotal,Pourcentage(BigDecimal),Plafond(Politique, BigDecimal)(applique une politique mais borne le résultat). - Une fonction
evaluer(Politique, BigDecimal montant)qui renvoie le remboursement enBigDecimal, écrite comme unswitchsur motifs sansdefault. - Vérification observable :
evaluer(new Plafond(new Pourcentage(new BigDecimal("0.10")), new BigDecimal("20")), new BigDecimal("300"))renvoie20(10 % de 300 serait 30, plafonné à 20).
Éléments de correction :
1import java.math.BigDecimal;2 3public sealed interface Politique permits RemboursementTotal, Pourcentage, Plafond {}4public record RemboursementTotal() implements Politique {}5public record Pourcentage(BigDecimal taux) implements Politique {}6public record Plafond(Politique base, BigDecimal maximum) implements Politique {}7 8static BigDecimal evaluer(Politique p, BigDecimal montant) {9 return switch (p) {10 case RemboursementTotal() -> montant;11 case Pourcentage(var taux) -> montant.multiply(taux);12 case Plafond(var base, var maximum) -> {13 var brut = evaluer(base, montant);14 yield brut.min(maximum);Le switch imbriqué se lit comme la définition mathématique de la politique ; ajouter un quatrième cas force le compilateur à vous prévenir partout.