1. Introduction
Le polymorphisme est l’un des trois piliers de la programmation orientée objet (avec l’héritage et l’encapsulation). Littéralement, le mot vient du grec « poly » (beaucoup) et « morph » (forme). En programmation, cela signifie : une interface — de multiples implémentations.
Définition
Le polymorphisme est la capacité d’objets de classes différentes à réagir différemment aux mêmes messages (appels de méthodes).
Autrement dit, si vous avez une méthode makeSound(), vous pouvez l’appeler sur n’importe quel animal, mais le chat fera « miaou », le chien « ouaf » et la vache « meuh ». Pour le développeur, ce n’est qu’un appel à animal.makeSound(), mais ce qui se passera réellement dépend du type effectif de l’objet référencé par cette variable.
Analogie de la vie courante
Imaginez que vous avez à la maison une télécommande de télévision, et que cette même télécommande permet aussi de piloter des enceintes, un projecteur et même une machine à café. Vous appuyez sur le bouton « Allumer » — turnOn(), et chaque appareil réagit à sa manière. L’important est que tous ont un « bouton » d’allumage, mais l’implémentation diffère.
Exemple en Java
class Animal {
void makeSound() {
System.out.println("Un son quelconque...");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Ouaf !");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Miaou !");
}
}
class Cow extends Animal {
@Override
void makeSound() {
System.out.println("Meuh !");
}
}
Nous pouvons maintenant faire ceci :
Animal animal1 = new Dog();
Animal animal2 = new Cat();
Animal animal3 = new Cow();
animal1.makeSound(); // Ouaf !
animal2.makeSound(); // Miaou !
animal3.makeSound(); // Meuh !
Remarquez que toutes les variables sont de type Animal, mais le résultat de l’appel dépend du type effectif de l’objet.
2. Types de polymorphisme
En Java (et dans la plupart des langages orientés objet), on distingue deux types principaux de polymorphisme :
Polymorphisme de compilation (statique) — surcharge de méthodes (overloading)
Il s’agit du cas où une classe contient plusieurs méthodes portant le même nom mais ayant des paramètres différents. Le compilateur décide lui-même quelle méthode appeler en fonction des arguments fournis.
Exemple (un petit avant-goût — plus de détails dans la prochaine leçon) :
class Printer {
void print(int x) {
System.out.println("Nombre : " + x);
}
void print(String s) {
System.out.println("Chaîne : " + s);
}
}
Polymorphisme d’exécution (dynamique) — redéfinition de méthodes (overriding)
Un comportement défini dans la classe de base est redéfini dans les sous-classes. La méthode effectivement appelée est choisie à l’exécution (runtime), selon le type effectif de l’objet.
Exemple — voir ci-dessus avec les animaux.
3. À quoi sert le polymorphisme ?
Le polymorphisme n’est pas qu’un joli mot pour un entretien. C’est un outil qui rend votre code flexible, extensible et facile à maintenir.
Universalité du code
Vous pouvez écrire du code qui travaille avec des objets du type de base sans vous soucier des détails de leur implémentation. Par exemple, si vous avez une liste d’animaux, vous pouvez l’itérer et appeler makeSound() sur chacun, sans vous demander s’il s’agit d’un chat ou d’un chien.
Animal[] animals = { new Dog(), new Cat(), new Cow() };
for (Animal animal : animals) {
animal.makeSound(); // À chaque fois, c’est la « bonne » méthode qui sera appelée
}
Facilité d’extension
Si demain votre chef vous dit : « Et si on ajoutait un perroquet ! », vous écrivez simplement une nouvelle classe Parrot extends Animal et vous l’ajoutez au tableau. Tout le reste du code reste inchangé. C’est le principe ouvert à l’extension et fermé à la modification (OCP de SOLID).
Simplification de l’architecture
Vous pouvez construire des systèmes complexes où les différentes parties interagissent via des abstractions (classes de base ou interfaces) sans se préoccuper des implémentations concrètes. Cela économise du temps, des nerfs et du café.
4. Notions clés : type de référence et type effectif
Type de référence de la variable
Lorsque vous écrivez Animal animal = new Dog();, la variable animal a pour type de référence Animal, c’est-à-dire que le compilateur « pense » que c’est un animal et n’autorise que les méthodes déclarées dans la classe Animal.
Type effectif (réel) de l’objet
Mais en mémoire, vous avez réellement un objet de type Dog. C’est lui qui détermine quelle méthode sera appelée lors de l’accès à makeSound().
Illustration
Animal animal = new Dog();
animal.makeSound(); // Appellera Dog.makeSound(), et non pas Animal.makeSound()
Important ! Via une référence du type de base (Animal), vous ne pouvez pas appeler des méthodes qui existent uniquement sur Dog, si elles ne sont pas déclarées dans la classe de base.
Liaison tardive (dynamique)
C’est la « magie » qui se produit à l’exécution : quand vous appelez une méthode via une référence du type de base, la JVM regarde le type effectif de l’objet et invoque la « bonne » implémentation. C’est le polymorphisme en action.
5. Exemple pratique : polymorphisme dans une application
Poursuivons le développement de notre application d’apprentissage. Supposons que nous écrivions une simple simulation de zoo. Nous avons une classe de base Animal et plusieurs de ses descendants. Nous voulons que tous les animaux puissent « émettre un son », sans réécrire du code pour chaque type d’animal.
Étape 1 : classe de base et sous-classes
class Animal {
void makeSound() {
System.out.println("Un son quelconque...");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Ouaf !");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Miaou !");
}
}
Étape 2 : tableau d’animaux
Animal[] zoo = { new Dog(), new Cat(), new Animal() };
Étape 3 : itération et appel de méthode
for (Animal animal : zoo) {
animal.makeSound();
}
Résultat de l’exécution :
Ouaf !
Miaou !
Un son quelconque...
Remarquez : nous ne savons pas à l’avance qui se trouve dans le tableau — le programme s’en charge et appelle la méthode appropriée.
6. Schéma du polymorphisme
. Animal (makeSound)
/ \
Dog Cat
(makeSound) (makeSound)
Animal animal = new Dog();
animal.makeSound(); // --> Dog.makeSound()
Animal animal = new Cat();
animal.makeSound(); // --> Cat.makeSound()
7. Un autre exemple : polymorphisme dans un cas réel
Supposons que vous écriviez un programme de gestion des employés d’une entreprise. Vous avez une classe de base Employee et deux sous-classes : Manager et Developer. Tous les employés peuvent travailler — work() — mais ils le font différemment.
class Employee {
void work() {
System.out.println("L’employé travaille.");
}
}
class Manager extends Employee {
@Override
void work() {
System.out.println("Le manager tient une réunion.");
}
}
class Developer extends Employee {
@Override
void work() {
System.out.println("Le développeur écrit du code.");
}
}
Vous pouvez maintenant faire ceci :
Employee[] staff = { new Manager(), new Developer(), new Employee() };
for (Employee emp : staff) {
emp.work();
}
Résultat :
Le manager tient une réunion.
Le développeur écrit du code.
L’employé travaille.
8. Quand le polymorphisme ne fonctionne PAS
Le polymorphisme ne fonctionne que pour les méthodes déclarées dans la classe de base. Si une sous-classe possède une méthode qui lui est propre, vous ne la verrez pas via une référence de type de base.
class Dog extends Animal {
void fetchStick() {
System.out.println("Le chien rapporte un bâton !");
}
}
Animal animal = new Dog();
// animal.fetchStick(); // Erreur de compilation ! Cette méthode n’est pas visible via Animal
Pour appeler une méthode spécifique, il faut effectuer un cast vers le bon type :
if (animal instanceof Dog) {
((Dog) animal).fetchStick();
}
Mais c’est une autre histoire — l’essentiel : via le polymorphisme, seules les méthodes déclarées dans la classe de base sont accessibles.
9. Erreurs courantes avec le polymorphisme
Erreur n° 1 : S’attendre à ce que toutes les méthodes de la sous-classe soient accessibles via une référence de type de base. En réalité, seules celles déclarées dans la classe de base le sont.
Erreur n° 2 : Ne pas utiliser l’annotation @Override lors de la redéfinition d’une méthode. Sans elle, on peut écrire par inadvertance une méthode avec une signature incorrecte, et le polymorphisme ne fonctionnera pas (la méthode de la classe de base ne sera pas redéfinie).
Erreur n° 3 : Essayer d’appeler une méthode spécifique à la sous-classe sans cast. Le compilateur ne le permettra pas, car il ne sait pas quel objet se cache derrière la référence de type de base.
Erreur n° 4 : Confusion entre surcharge (overloading) et redéfinition (overriding). La surcharge, ce sont plusieurs méthodes portant le même nom mais avec des paramètres différents dans une même classe. La redéfinition, c’est modifier le comportement de la méthode dans une sous-classe.
GO TO FULL VERSION