Adloun

Héritage et polymorphisme

Cours · programmation Java (école d'ingénieur), chapitre 9 · école d'ingénieur, informatique · le début du chapitre, la suite sur Adloun

Un logiciel de dessin manipule des cercles, des rectangles, des triangles. Tous ont un nom, tous savent calculer leur aire et se décrire ; mais chacun calcule son aire à sa façon. Écrire trois classes indépendantes, c'est recopier trois fois le même code pour le nom et la description, et ne pas pouvoir ranger ces formes dans un même tableau pour, par exemple, calculer l'aire totale d'un dessin.

L'héritage répond à ces deux besoins. Il permet de définir une classe Cercle à partir d'une classe plus générale Forme : le cercle reçoit tout ce que sait faire une forme, et n'écrit que ce qui lui est propre. Le polymorphisme, ensuite, permet de traiter un cercle ou un rectangle simplement comme « une forme » : quand on demande son aire à une forme, c'est la méthode du cercle ou celle du rectangle qui s'exécute, selon l'objet réellement rangé dans la variable.

Objectifs du chapitre

  • Définir une sous-classe avec extends, appeler le constructeur de la super-classe avec super(...).
  • Redéfinir une méthode (@Override) et ne pas la confondre avec une surcharge.
  • Distinguer type statique et type dynamique ; comprendre la liaison dynamique, qui fait le polymorphisme.
  • Redéfinir correctement toString, equals et hashCode, hérités de Object.
  • Tester le type d'un objet avec instanceof et savoir quand ne pas hériter.

9.1 Définir une sous-classe

9.1.1 Le mot-clé extends

Définition 9.1Héritage

Une classe B hérite d'une classe A quand elle est déclarée par class B extends A. On dit que B est une sous-classe (ou classe dérivée, ou classe fille) de A, et que A est la super-classe (ou classe mère) de B. Un objet de type B possède tous les attributs et toutes les méthodes de A, plus ceux que B ajoute.

En Java, une classe n'hérite que d'une seule classe : c'est l'héritage simple. En revanche, une classe peut avoir autant de sous-classes que l'on veut, et une sous-classe peut à son tour avoir des sous-classes : on obtient une hiérarchie de classes.

L'héritage doit traduire la relation « est un » : un cercle est une forme, un cadre est un employé, une voiture est un véhicule. Si la phrase sonne faux, l'héritage est sans doute une erreur (section 9.7).

Voici la hiérarchie de notre logiciel de dessin. La classe Forme contient ce qui est commun ; les sous-classes Cercle et Rectangle ajoutent leurs dimensions et leur propre calcul d'aire.


class Forme {
    protected String nom;

    Forme(String nom) {
        this.nom = nom;
    }

    double aire() {
        return 0.0;                        // une forme « quelconque » n'a pas d'aire connue
    }

    String decrire() {
        return nom + " : aire " + String.format("%.3f", aire());
    }
}

class Cercle extends Forme {
    private double rayon;

    Cercle(String nom, double rayon) {
        super(nom);                        // appelle le constructeur de Forme
        this.rayon = rayon;
    }

    @Override
    double aire() {
        return Math.PI * rayon * rayon;
    }

    @Override
    String decrire() {
        return "Cercle " + super.decrire() + ", rayon " + rayon;
    }
}

class Rectangle extends Forme {
    private double largeur, hauteur;

    Rectangle(String nom, double largeur, double hauteur) {
        super(nom);
        this.largeur = largeur;
        this.hauteur = hauteur;
    }

    @Override
    double aire() {
        return largeur * hauteur;
    }
}

On représente une telle hiérarchie par un diagramme de classes : une boîte par classe (son nom, ses attributs, ses méthodes), et une flèche à pointe creuse de la sous-classe vers la super-classe.

Le signe # note un membre protected, le signe - un membre private. Une sous-classe ne fait figurer que ce qu'elle ajoute ou redéfinit : le Cercle a bien un nom, mais il l'hérite.

9.1.2 Les constructeurs et super(...)

Les constructeurs ne s'héritent pas. Un objet Cercle contient pourtant un attribut nom, déclaré dans Forme : c'est au constructeur de Forme de l'initialiser. Le constructeur de Cercle l'appelle par l'instruction super(nom).

Proposition 9.2Appel du constructeur de la super-classe

La première instruction d'un constructeur de sous-classe est l'appel super(...) à un constructeur de la super-classe. Si on ne l'écrit pas, le compilateur insère super();, c'est-à-dire un appel au constructeur sans paramètre de la super-classe.

La conséquence pratique est importante : si la super-classe n'a pas de constructeur sans paramètre, l'appel explicite à super(...) est obligatoire. Oublions-le dans une classe Carre :


class Carre extends Forme {
    private double cote;
    Carre(double cote) {
        this.cote = cote;
    }
}

Erreur1.java:12: error: constructor Forme in class Forme cannot be applied to given types;
    Carre(double cote) {
                       ^
  required: String
  found:    no arguments
  reason: actual and formal argument lists differ in length
1 error

Le message se lit ainsi : le compilateur a voulu appeler le constructeur de Forme « sans arguments » (found: no arguments), à cause du super(); implicite, mais ce constructeur attend une String (required: String). Il faut écrire super("carré"); en première ligne.

Exemple 9.3l'ordre d'exécution des constructeurs

Trois classes en chaîne, C extends B et B extends A, dont les constructeurs affichent un message :


class A {
    A() { System.out.println("constructeur de A"); }
}

class B extends A {
    B() {
        // super(); est inséré ici par le compilateur
        System.out.println("constructeur de B");
    }
}

class C extends B {
    C() { System.out.println("constructeur de C"); }
}

L'instruction new C(); affiche :


constructeur de A
constructeur de B
constructeur de C

Le constructeur de C commence par appeler celui de B, qui commence par appeler celui de A. Un objet se construit donc de la racine vers les feuilles : la partie « A » de l'objet est prête avant la partie « B », qui est prête avant la partie « C ».

9.1.3 Ce qu'une sous-classe voit : protected

Un attribut private de la super-classe existe bien dans l'objet de la sous-classe, mais le code de la sous-classe n'y a pas accès :


class Compte {
    private double solde;
}

class CompteEpargne extends Compte {
    void ajouterInterets(double taux) {
        solde = solde * (1 + taux);
    }
}

Erreur3.java:11: error: solde has private access in Compte
        solde = solde * (1 + taux);
        ^

Deux solutions. La première, souvent la meilleure, consiste à passer par les méthodes publiques de la super-classe (getSolde(), crediter(...)) : la super-classe garde le contrôle de son invariant. La seconde consiste à déclarer l'attribut protected.

Définition 9.4Visibilité protected

Un membre protected est accessible dans sa classe, dans les sous-classes (même situées dans un autre paquet) et dans les classes de son paquet. Il est donc moins protégé qu'un membre private, plus qu'un membre public.

modificateurla classele paquetles sous-classespartout
privateouinonnonnon
(aucun)ouiouinonnon
protectedouiouiouinon
publicouiouiouioui

sauf, bien sûr, les sous-classes du même paquet.

La suite du cours est sur Adloun

Le chapitre en entier, formules et figures comprises, avec le tuteur, les fiches, les QCM et les exercices.

Lire la suite du cours

En accès libre, sans inscription.

Exercices corrigés de ce chapitre

Lire la suite du cours →