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 de List<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.