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 6

Le type déclaré et le type réel

Le compilateur se fonde sur le type déclaré, l'exécution sur l'objet réel. Ce chapitre distingue les deux, source d'erreurs fréquentes à l'audit de rentrée.

Ce chapitre est une révision de la COOA : sa notion (N1) est vérifiée par le diagnostic du 21/10, et n'est pas à l'épreuve.

Java est à la fois statiquement typé et dynamiquement lié. Ces deux propriétés ne se contredisent pas, car elles interviennent à deux moments différents.

Ce que vérifie le compilateur

CanardEnPlastique a = new CanardEnPlastique("Gérard", 499);
CanardSwag        b = new CanardSwag("Jacqueline", 999, "cape");

a.cancaner();          // coin coin
b.cancaner();          // coin (mais stylé)
b.getAccessoire();     // légal : b est déclaré CanardSwag

En revanche :

CanardEnPlastique a = new CanardEnPlastique("Jean", 300);
a.getAccessoire();     // refusé : CanardEnPlastique n'a pas cette méthode

L'appel serait refusé même si a contenait un CanardSwag à l'exécution. Le compilateur ne tient pas compte de ce que la variable contiendra, seulement de son type déclaré.

Ce qui se passe à l'exécution

CanardEnPlastique c = new CanardSwag("Ouarzazate", 799, "chapeau");
c.cancaner();          // coin (mais stylé)

Le type déclaré est CanardEnPlastique, le type réel est CanardSwag. C'est la redéfinition du type réel qui s'exécute : c'est ce qu'on appelle le polymorphisme.

Exemple complet

Avant de lancer le programme, essayez de prévoir ce qu'affiche chacune des trois lignes.

Typage.java

class CanardEnPlastique {
    public String crier() { return "coin"; }
}

class CanardSwag extends CanardEnPlastique {
    @Override public String crier() { return "yo"; }
    public String accessoire()      { return "cape"; }
}

class Demo {
    public static void main(String[] args) {

        CanardEnPlastique c = new CanardSwag();

        System.out.println("1. c.crier()       : " + c.crier());
        System.out.println("2. c.getClass()    : " + c.getClass().getSimpleName());

        // 3. Décommentez : le compilateur refuse, alors que l'objet SAIT le faire.
        // System.out.println("3. c.accessoire()  : " + c.accessoire());

        // Le downcast rend la méthode visible, à vos risques.
        CanardSwag swag = (CanardSwag) c;
        System.out.println("4. après downcast  : " + swag.accessoire());

        // Et voici ce qui arrive quand l'objet n'est pas ce qu'on prétend.
        CanardEnPlastique simple = new CanardEnPlastique();
        try {
            CanardSwag rate = (CanardSwag) simple;
            System.out.println(rate.accessoire());
        } catch (ClassCastException e) {
            System.out.println("5. downcast raté   : " + e.getClass().getSimpleName());
        }
    }
}

Ici, un atelier : on y écrit et on y exécute du code. Il demande JavaScript.

La ligne 2 montre que l'objet est bien un CanardSwag. La ligne 3, si vous la décommentez, montre que le compilateur n'en tient pas compte et refuse l'appel.

Les casts

Vers le haut (upcast). Il est implicite et toujours sûr :

CanardSwag swag = new CanardSwag("Jacqueline", 999, "cape");
CanardEnPlastique canard = swag;      // pas de parenthèses nécessaires

En effet, un CanardSwag est forcément un CanardEnPlastique.

Vers le bas (downcast). Il doit être écrit explicitement, et le compilateur l'accepte sans vérifier :

CanardEnPlastique canard = new CanardEnPlastique("Jean", 399);
CanardSwag swag = (CanardSwag) canard;    // compile
swag.getAccessoire();                     // ClassCastException à l'exécution

Les limites d'instanceof

if (c instanceof CanardSwag) {
    CanardSwag s = (CanardSwag) c;
    System.out.println("Accessoire : " + s.getAccessoire());
}

Ce code fonctionne, mais il se maintient mal :

  • il est fragile : à chaque nouvelle sous-classe, il faut retrouver tous les instanceof ;
  • il est fermé à l'extension : chaque nouveau type oblige à modifier du code existant ;
  • il est verbeux : trois lignes là où une redéfinition n'en demande aucune à l'appel.

Dans la version polymorphe, on appelle directement une méthode de l'objet au lieu de tester son type :

class CanardEnPlastique {
    public String description() { return "Canard basique"; }
}

class CanardSwag extends CanardEnPlastique {
    @Override public String description() { return "Swag avec " + accessoire; }
}

void afficher(CanardEnPlastique c) {
    System.out.println(c.description());     // plus aucun test de type
}
Question d'épreuve

CanardEnPlastique c = new CanardSwag();, quelle méthode crier() s'exécute, et pourquoi le compilateur refuse-t-il c.accessoire() ?

C'est celle de CanardSwag, car la liaison est dynamique : c'est le type réel de l'objet qui compte. En revanche, c.accessoire() est refusé parce que le compilateur ne considère que le type déclaré, CanardEnPlastique, qui n'a pas cette méthode, quel que soit l'objet contenu dans la variable à l'exécution.