Le polymorphisme en Java
Écrivez du code Java flexible grâce au polymorphisme à la compilation (surcharge) et à l'exécution (redéfinition).
Le polymorphisme, c'est « une interface, plusieurs implémentations ». En Java, il se présente sous deux formes :
- Polymorphisme à la compilation (surcharge) : le compilateur choisit entre des méthodes qui partagent un nom en fonction des types d'arguments passés.
- Polymorphisme à l'exécution (redéfinition) : la JVM choisit entre les implémentations de méthodes en fonction de l'objet réel sur lequel l'appel est effectué.
Le polymorphisme à l'exécution est ce que l'on entend généralement par « polymorphisme » dans un contexte POO, et c'est lui qui rend l'héritage vraiment utile. Sans lui, une référence de type Cat serait le seul moyen d'appeler Cat.speak() — il serait impossible d'écrire du code qui parcourt une liste mixte d'animaux et demande à chacun de parler.
Polymorphisme à la compilation — la surcharge
Deux méthodes dans la même classe peuvent partager un nom à condition que leurs listes de paramètres diffèrent. Le compilateur détermine laquelle appeler en fonction des types d'arguments au site d'appel :
public class Printer {
void print(int n) { System.out.println("int: " + n); }
void print(double d) { System.out.println("double: " + d); }
void print(String s) { System.out.println("string: " + s); }
}
Printer p = new Printer();
p.print(5); // int
p.print(5.0); // double
p.print("hi"); // stringCela est entièrement décidé à la compilation. La méthode choisie est inscrite dans le bytecode ; rien ne change à l'exécution. La surcharge de méthodes a été abordée en détail dans la surcharge de méthodes dans la partie 5.
Polymorphisme à l'exécution — redéfinition et dispatch dynamique
La forme la plus intéressante. Lorsqu'une sous-classe redéfinit une méthode, les appels effectués via une référence de type parent sont tout de même dispatchés vers la version de la sous-classe :
class Animal {
String speak() { return "(noise)"; }
}
class Cat extends Animal {
@Override String speak() { return "meow"; }
}
class Dog extends Animal {
@Override String speak() { return "woof"; }
}
Animal[] zoo = { new Cat(), new Dog(), new Animal() };
for (Animal a : zoo) {
System.out.println(a.speak());
}
// meow
// woof
// (noise)À chaque itération, a est de type Animal, mais l'objet réel est un Cat, un Dog ou un Animal. L'appel a.speak() ne choisit pas la méthode à la compilation — à ce stade, le compilateur sait seulement que a est un Animal. À l'exécution, la JVM examine l'objet réel et dispatche vers le speak de la classe de cet objet.
C'est ce qu'on appelle le dispatch dynamique (parfois appelé dispatch virtuel). C'est ce qui rend la boucle ci-dessus intéressante : elle est écrite de manière générique vis-à-vis de Animal, et elle fonctionne pour n'importe quelle sous-classe — y compris celles qui n'existaient pas lorsque la boucle a été écrite.
Pourquoi c'est important
Le polymorphisme est la fonctionnalité POO qui permet d'écrire du code ouvert à l'extension sans modification. Une fonction qui prend une Shape et appelle area() dessus fonctionne pour toutes les formes qui existent aujourd'hui et pour toutes celles que quelqu'un ajoutera demain. La fonction n'a pas besoin d'une chaîne if (shape instanceof Circle).
double totalArea(List<Shape> shapes) {
double sum = 0;
for (Shape s : shapes) sum += s.area(); // dispatches to each subclass
return sum;
}Ajoutez Triangle extends Shape, et totalArea fonctionne avec des listes de triangles sans aucune modification. C'est l'essence du Principe Ouvert/Fermé — ouvert à l'extension, fermé à la modification.
Upcasting et downcasting
Passer d'un type de sous-classe à un type parent est un upcast. Il est implicite et toujours sûr :
Cat c = new Cat();
Animal a = c; // upcast — implicitL'opération inverse — assigner une référence de type parent à un type de sous-classe — est un downcast. Elle nécessite une expression de cast, et la JVM vérifie à l'exécution que l'objet est bien de ce sous-type :
Animal a = new Cat();
Cat c = (Cat) a; // downcast — runtime check
Animal a2 = new Dog();
Cat c2 = (Cat) a2; // ClassCastException at runtimeL'alternative sûre à la compilation est la vérification instanceof, souvent combinée avec le filtrage de motifs dans les versions modernes de Java :
if (a instanceof Cat c) {
c.purr();
}Les champs ne sont pas polymorphiques
Le dispatch dynamique s'applique uniquement aux méthodes d'instance. Les champs, les méthodes static et les méthodes private sont liés à la compilation en fonction du type déclaré de la référence :
class A {
String label = "A";
static String klass() { return "A"; }
}
class B extends A {
String label = "B";
static String klass() { return "B"; }
}
A a = new B();
System.out.println(a.label); // "A" — field, not polymorphic
System.out.println(a.klass()); // "A" — static, not polymorphicC'est l'une des raisons pour lesquelles on garde les champs privés et on y accède via des méthodes — les méthodes participent au polymorphisme ; les champs, non.
@Override et les bugs silencieux
Annotez toujours les redéfinitions avec @Override. Cette annotation indique au compilateur « ceci est censé redéfinir une méthode parente — échoue si ce n'est pas le cas ». Sans elle, une petite faute de frappe crée une nouvelle méthode qui ressemble à une redéfinition mais n'en est pas une :
class Animal {
String speak() { return "(noise)"; }
}
class Cat extends Animal {
String Speak() { return "meow"; } // capital S — typo, new method
}
Animal a = new Cat();
System.out.println(a.speak()); // "(noise)" — Cat.Speak was never calledL'ajout de @Override permet au compilateur de détecter cela immédiatement.
Polymorphisme avec les interfaces
L'héritage n'est pas le seul moyen. Une interface est aussi un type parent — différentes classes concrètes l'implémentent, et le code qui accepte le type interface fonctionne avec toutes :
interface Greeter {
String greet();
}
class English implements Greeter {
public String greet() { return "Hello"; }
}
class French implements Greeter {
public String greet() { return "Bonjour"; }
}
Greeter g = new French();
System.out.println(g.greet()); // "Bonjour" — dispatched to French.greetMême idée — écrivez du code contre l'abstraction, laissez l'exécution choisir l'implémentation. Le chapitre sur les interfaces aborde les mécanismes en détail.
Un exemple complet
La suite
Le polymorphisme repose sur un mécanisme : une sous-classe remplace une méthode héritée. Ce mécanisme — ce qui est autorisé, ce qui ne l'est pas, et l'annotation @Override qui vous aide à rester honnête — est le sujet du prochain chapitre. Continuez vers la redéfinition de méthodes.