Polymorphisme

Le langage Java supporte l'utilisation d'un objet d'une classe dérivée là où un objet de la classe de base est nécessaire : c'est le principe de substitution de Liskov.

JFrame fenetre = new JFrame();
JLabel etiquette = new JLabel("Bonjour !");
fenetre.add(etiquette, BorderLayout.CENTER);

La classe JFrame hérite de la classe Container la méthode add employée dans cet exemple. Sa signature est la suivante :

public void add(Component composant, Object contraintes)

L'objet auquel fait référence la variable etiquette appartient à la classe JLabel, qui hérite (en plusieurs étapes) de la classe Component : il peut donc se substituer à l'objet que réclame la méthode add en premier argument.

Sur le même principe, la constante CENTER appartient à la classe String, qui hérite de la classe Object. Elle peut donc servir à initialiser le paramètre contraintes.

Le principe de substitution est utilisé pour faire de la programmation par contrat : une classe hérite d'une autre classe non plus pour récupérer des fonctionnalités déjà codées, mais surtout pour avoir le droit de se substituer à la classe d'origine afin de procurer un service spécifique.

Ainsi, une fenêtre a besoin de composants pour contruire son apparence. La classe Component est une façon de préciser le contrat de base que tout composant doit satisfaire pour avoir le droit d'être intégré dans une fenêtre. Plusieurs classes héritent de la classe Component : JLabel, JTextField, JPanel, etc. Chacune obéit au même contrat de base, mais le remplit de façon différente.

Dans l'organisation vue plus haut, on voit que la classe de base (ici Component) ne sera jamais instanciée. On doit y définir les méthodes nécessaires pour collaborer avec la fenêtre, mais il n'est pas obligatoire (et souvent pas souhaitable) de coder ces méthodes : elles seront de toute façon redéfinies dans les classes dérivées.

Une telle méthode peut être déclarée abstract dans la classe de base pour éviter d'avoir à la coder :

public abstract int intValue();

Cet exemple (tiré de la classe Number) montre la définition d'une méthode abstraite. Le corps est remplacé par un point-virgule, et on ajoute le qualificatif abstract.

Une classe qui possède une ou plusieurs méthodes déclarées abstraites est elle-même une classe abstraite et doit être déclarée avec le même qualificatif :

public abstract class Number {
  ...
}

Les méthodes d'une telle classe sont «en chantier», donc elle n'est pas destinée à être instanciée : il est interdit de créer un objet d'une classe abstraite (vous avez sans doute rencontré ce problème avec la classe Graphics).

Remarque Les classes dérivées d'une classe abstraite héritent de toutes ses méthodes abstraites. Elles ont donc pour obligation de redéfinir ces méthodes, sans quoi elles seront également abstraites.

Quand on pousse le concept de classe abstraite jusqu'à sa conclusion logique, on obtient une interface : une classe qui n'apporte rien d'autre que des obligations. C'est la forme la plus pure de programmation par contrat.

Une interface ne peut contenir que des méthodes abstraites (et rarement des membres de classe). Les attributs d'une interface sont obligatoirement constants. Enfin, tous les membres d'une interface (attributs et méthodes) sont automatiquement publics.

public interface Runnable {
  void run();
}

Dans cet exemple, on voit qu'une définition d'interface utilise le mot réservé interface à la place du mot class, et que les membres n'ont pas de qualificatif de visibilité ni d'abstraction.

Les interfaces sont plus simples que des classes, ce qui permet d'autoriser une nouveauté : une classe peut hériter de plusieurs interfaces à la fois. Pour bien différencier les deux mécanismes, on dit qu'on hérite d'une classe, mais qu'on réalise une interface. Le mot-clé extends est donc remplacé par implements.

public class Fille extends Mere implements PremiereInterface, SecondeInterface {
  ...
}

Remarque Une interface peut aussi hériter, mais seulement d'une autre interface. Dans ce cas on utilise encore extends.

  1. Véhicules. Faites marcher ce programme incomplet :

    import javax.swing.JOptionPane;
     
    public class Test {
      public static void main(String[] args) {
        Vehicule v;
        Object[] choix = {"Voiture", "Moto"};
     
        int reponse = JOptionPane.showOptionDialog(null,
          "Quel v\u00E9hicule choisissez-vous ?",
          "Question",
          JOptionPane.DEFAULT_OPTION,
          JOptionPane.QUESTION_MESSAGE,
          null,
          choix,
          null);
        if (reponse == 0)
          v = new Voiture();
        else
          v = new Moto();
        System.out.println("Une "+v.sorte()+" poss\u00E8de "+v.nbRoues()+" roues.");
      }
    }

    Il n'y a pas besoin de modifier ce code ; il suffit de le placer dans un fichier nommé Test.java. Par contre, il ne pourra compiler que quand vous aurez écrit des définitions pour les classes Voiture et Moto ainsi que pour l'interface Vehicule.

  2. Moyenne. On désire concevoir une classe qui puisse calculer la moyenne d'une collection hétéroclite de valeurs. On imagine une méthode add pour ajouter une valeur, et une méthode getAverage pour obtenir la moyenne des valeurs données jusque là. Le problème est que les valeurs peuvent appartenir à n'importe quel type primitif parmi byte, short, int, long, float et double.

    1. La solution la plus simple consiste à exploiter la surcharge. Définissez la classe demandée en fournissant une version différente de la méthode add pour chaque type possible.
    2. La première solution est longue à écrire, et nécessite une évolution si on ajoute un nouveau type à la liste. Nous allons profiter du fait que tous les valeurs d'un type primitif peuvent être transformées en objets, et que les classes de ces objets héritent toutes de la classe abstraite Number. Modifiez la définition de la classe précédente pour qu'il n'y ait plus qu'une seule version de la méthode add.

  3. Polyligne. Écrivez un programme qui ouvre une fenêtre affichant une polyligne : une ligne brisée reliant une suite de points.

    Ce programme est un consommateur : il ne peut pas fonctionner sans qu'on lui donne les coordonnées des points à relier. Nous allons donc devoir lui fournir un service qui remplit ce besoin. Les termes de ce service sont explicités sous la forme d'une interface :

    import java.awt.Point;
     
    public interface ProducteurDePoints {
      Point suivant();
    }

    Chaque appel à la méthode suivant fournira le point suivant à relier, jusqu'à ce qu'il n'y ait plus de point, auquel cas la méthode renvoie null. Après cela, l'objet est réinitialisé et les prochains appels à suivant donneront à nouveau la suite des points depuis le début.

    Pour vous aider à écrire le programme, voici un fournisseur possible pour ce service :

    import java.awt.Point;
     
    public class Etoile implements ProducteurDePoints {
      private static final int xCentre = 100;
      private static final int yCentre = 100;
      private static final double rayon = 90.0;
      private static final double angleDepart = Math.PI/4.0;
      private static final double angleIncrement = (4.0*Math.PI)/5.0;
      private double numero;
      public Etoile() {
        this.numero = 0.0;
      }
      public Point suivant() {
        Point p = null;
        if (this.numero < 6.0) {
          double angle = Etoile.angleDepart+this.numero*Etoile.angleIncrement;
          p = new Point((int) (Etoile.rayon*Math.cos(angle)),
                        (int) (Etoile.rayon*Math.sin(angle)));
          p.translate(Etoile.xCentre, Etoile.yCentre);
          this.numero++;
        } else {
          this.numero = 0.0;
        }
        return p;
      }
    }

    Une fois que votre programme sera opérationnel, remplacez le fournisseur par votre propre version qui produira des points décrivant une spirale.

    Remarque Cette substitution ne devrait nécessiter que de changer un seul mot dans la méthode principale du programme.

retour à la page d'accueil

retour au sommet