Version sans JavaScript, en lecture seule : les ateliers, les quiz et l’exécution du code n’y sont pas. Version complète · Catalogue · Se connecter
Support de cours · chapitre 12
Généricité et énumérations
Deux outils de typage qui permettent de détecter certaines erreurs à la compilation plutôt qu'à l'exécution. Chapitre à lire, non certifié.
Les énumérations
Le problème des constantes
public static final int ROUGE = 1;
public static final int BLEU = 2;
public static final int JAUNE = 3;
int couleur = ROUGE;
couleur = 42; // parfaitement légal, et parfaitement absurde
Ces constantes n'offrent aucune sécurité de type et ne sont pas regroupées : rien n'empêche d'utiliser une valeur qui n'existe pas. Le problème est le même avec des chaînes : entre "chapeau" et "chapaeu", une faute de frappe suffit à créer un bug.
public enum Couleur { ROUGE, BLEU, JAUNE }
Couleur c = Couleur.ROUGE; // et rien d'autre n'est acceptable
Une énumération peut porter du comportement
Contrairement à une simple liste de constantes, une énumération peut associer des données et des méthodes à ses valeurs.
Avant de lancer le programme, essayez de prévoir le prix d'un canard légendaire à 100 centimes de base, et ce que fait le switch sur un cas non traité.
Rarete.java
enum Rarete {
COMMUN(1), RARE(5), LEGENDAIRE(50);
private final int multiplicateur;
Rarete(int multiplicateur) { this.multiplicateur = multiplicateur; }
public int appliquer(int prixBase) { return prixBase * multiplicateur; }
}
enum Accessoire { CHAPEAU, CAPE, BOUEE }
class Demo {
static String commenter(Accessoire a) {
switch (a) {
case CHAPEAU: return "stylé";
case CAPE: return "super-héros";
case BOUEE: return "prêt pour la piscine";
}
// Inatteignable tant que l'énumération ne change pas.
throw new IllegalStateException("accessoire non traité : " + a);
}
public static void main(String[] args) {
for (Rarete r : Rarete.values()) {
System.out.println(r + " : " + r.appliquer(100) + " centimes"
+ " (position " + r.ordinal() + ")");
}
System.out.println();
for (Accessoire a : Accessoire.values()) {
System.out.println(a + " : " + commenter(a));
}
System.out.println();
System.out.println("depuis une chaîne : " + Rarete.valueOf("RARE").appliquer(10));
}
}
Ici, un atelier : on y écrit et on y exécute du code. Il demande JavaScript.
values() donne toutes les valeurs, ordinal() la position, valueOf fait l'inverse de toString. Une énumération est une classe à part entière : elle peut avoir des champs, un constructeur et des méthodes.
La généricité
Le problème des types bruts
List bassin = new ArrayList();
bassin.add(new CanardEnPlastique("Jean", 200));
bassin.add("pas un canard..."); // accepté
CanardEnPlastique c = (CanardEnPlastique) bassin.get(1); // ClassCastException
Il faut un cast à chaque lecture, et l'erreur n'apparaît qu'à l'exécution, loin de la ligne qui l'a provoquée.
List<CanardEnPlastique> bassin = new ArrayList<CanardEnPlastique>();
bassin.add(new CanardEnPlastique("Jean", 200));
// bassin.add("PAS UN CANARD"); refusé à la compilation
CanardEnPlastique c = bassin.get(0); // aucun cast
L'invariance
List<String> noms = new ArrayList<String>();
List<Object> objets = noms; // INTERDIT
Pourtant, un String est un Object. Le compilateur refuse cette affectation parce que, si elle était permise, on pourrait écrire :
objets.add(new CanardEnPlastique("Faux", 0));
String s = noms.get(0); // ClassCastException, et personne n'a triché
List<String> n'est donc pas un sous-type de List<Object>. On dit que les génériques sont invariants.
Les jokers
Pour qu'une méthode accepte plusieurs types de listes sans perdre la sécurité de type, on utilise un joker :
static void afficherNoms(List<? extends CanardEnPlastique> bassin) {
for (CanardEnPlastique c : bassin) System.out.println(c.getNom());
}
? extends T accepte une liste de T ou de n'importe quelle sous-classe de T : on peut y lire des T. ? super T accepte une liste de T ou de l'une de ses classes parentes : on peut y écrire des T. Pour s'en souvenir, on retient « producteur extends, consommateur super ».
Ce que les génériques ne savent pas faire
À l'exécution, le paramètre de type n'existe plus : c'est l'effacement de type, un choix fait en 2004 pour rester compatible avec le code existant.
new T() // impossible
new T[10] // impossible
x instanceof List<String> // impossible
Pour la même raison, deux méthodes qui ne diffèrent que par List<String> et List<Integer> ont la même signature après effacement, et le compilateur les refuse.
Questions de quiz
1. Pourquoi une énumération vaut-elle mieux qu'un jeu de constantes entières ?
Parce qu'une énumération est un type : aucune valeur hors de la liste n'est acceptée, les valeurs sont regroupées explicitement, et elle peut avoir des champs et des méthodes.
2.
List<String>est-elle un sous-type deList<Object>?
Non, car les génériques sont invariants. Si c'était le cas, on pourrait ajouter n'importe quel Object à une liste de String, et la lecture d'un élément échouerait.
3. Pourquoi
new T()est-il impossible dans une classe générique ?
Parce que le paramètre de type est effacé à la compilation : à l'exécution, il ne reste aucune information sur T, donc aucune classe à instancier.