Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 23

Polymorphisme : Redéfinition de méthodes (@Override)

Recommended resources

Bonjour ! Dans la leçon précédente, nous avons vu comment utiliser le mot-clé super pour appeler les constructeurs et les méthodes d'une classe mère. Vous avez ainsi appris à réutiliser et étendre le code hérité, une pratique fondamentale en programmation orientée objet.

Aujourd'hui, nous allons exploiter pleinement la puissance de l'héritage en explorant le polymorphisme. L'objectif est de comprendre comment la redéfinition de méthodes, guidée par l'annotation @Override, nous permet de créer des programmes plus flexibles et plus faciles à maintenir. Le polymorphisme est l'un des piliers de la POO, et sa maîtrise vous ouvrira la porte à des architectures logicielles élégantes et robustes.

1. Qu'est-ce que le polymorphisme ?

Le mot "polymorphisme" vient du grec et signifie "plusieurs formes". En programmation, il désigne la capacité d'un objet à être référencé par plusieurs types (sa propre classe et n'importe laquelle de ses super-classes) et, par conséquent, à se comporter différemment selon son type réel lors de l'exécution.

Le principe fondamental est le suivant :

Une variable de référence de type "super-classe" peut contenir un objet de n'importe laquelle de ses "sous-classes". Lorsque l'on appelle une méthode sur cette variable, c'est la version de la méthode définie dans la classe réelle de l'objet qui sera exécutée, et non celle de la classe de la variable de référence.

Ce mécanisme, appelé liaison tardive ou dynamic dispatch, est au cœur du polymorphisme.

Le polymorphisme — Le langage Java

Cet article introduit très clairement le principe du polymorphisme avec un exemple simple et efficace.

Lisez la section "Principe du polymorphisme". Portez une attention particulière à la fin de l'extrait de code, où une variable de type Animal référence successivement un objet Chat puis un objet Chien. Observez comment l'appel à animal.crier() produit des résultats différents, illustrant parfaitement la liaison tardive.

L'image suivante décompose ce processus d'appel de méthode.

Cette image illustre comment la JVM (Machine Virtuelle Java) résout un appel de méthode à l'exécution. Quand `a.afficher()` est appelé, la JVM regarde le type réel de l'objet référencé par `a` (ici, `C`). Elle cherche d'abord la méthode `afficher()` dans la classe `C`. Si elle la trouve, elle l'exécute. Sinon, elle remonte la hiérarchie d'héritage (`B`, puis `A`) jusqu'à trouver la première implémentation correspondante.

2. La redéfinition de méthode (Method Overriding)

Le mécanisme qui rend le polymorphisme possible est la redéfinition de méthode (ou method overriding en anglais). Comme nous l'avons entrevu dans la leçon sur super, une classe fille peut fournir sa propre implémentation d'une méthode qu'elle hérite de sa classe mère.

Pour qu'une méthode soit considérée comme une redéfinition, elle doit respecter des règles précises :

  • Elle doit avoir le même nom que la méthode de la classe mère.
  • Elle doit avoir la même liste de paramètres (même nombre, type et ordre).
  • Son niveau d'accès doit être au moins aussi visible que celui de la méthode mère (ex: on peut passer de protected à public, mais pas de public à private).
  • Le type de retour doit être le même, ou un sous-type de celui de la méthode mère (on parle de type de retour covariant).

La vidéo suivante montre concrètement comment redéfinir une méthode et fait une distinction importante avec la surcharge de méthode, un concept proche mais différent.

JAVA (Intermédiaire) - 23 - Surcharge et redéfinition de methodes en JAVA

Cette vidéo des "TEACHERS DU NET" explique clairement la redéfinition de méthode et la différencie de la surcharge.

Concentrez-vous sur ces deux parties : Redéfinition de méthode : Observez comment la classe Triangle redéfinit la méthode aire() héritée de Forme pour implémenter un calcul spécifique. C'est l'essence même de l'overriding. Différence avec la surcharge : C'est un point crucial. La redéfinition (overriding) concerne l'héritage et a la même signature de méthode. La surcharge (overloading) se passe au sein de la même classe et implique des méthodes de même nom mais avec des paramètres différents. Comprendre cette distinction vous évitera beaucoup de confusion.


3. Le rôle crucial de l'annotation @Override

Vous avez déjà vu l'annotation @Override dans les exemples. Bien qu'elle ne soit pas techniquement obligatoire pour que le polymorphisme fonctionne, son utilisation est une bonne pratique professionnelle indispensable.

@Override est une instruction pour le compilateur. Elle lui dit : "Mon intention est de redéfinir une méthode de la super-classe. Vérifie pour moi que je le fais correctement."

Quel est l'intérêt ? Elle vous protège contre les erreurs de frappe ou d'inattention.

  • Sans @Override : Si vous faites une faute de frappe dans le nom de la méthode (ex: tostring() au lieu de toString()), le compilateur pensera que vous créez une toute nouvelle méthode. Votre code compilera, mais ne se comportera pas comme prévu au moment de l'exécution, car la redéfinition n'aura pas eu lieu. C'est un bug subtil et difficile à trouver.
  • Avec @Override : Si vous faites la même erreur, le compilateur verra que la méthode que vous essayez de "redéfinir" n'existe pas dans la classe mère. Il générera une erreur de compilation, vous forçant à corriger le problème immédiatement.

La courte vidéo ci-dessous démontre parfaitement cette situation.

15 - L'annotation @Override en polymorphisme Java

Cette vidéo de "Très Facile !" illustre de manière très efficace l'utilité de l'annotation @Override.

Regardez ces passages : Exécution sans problème : L'auteur montre que le code polymorphique fonctionne, même après avoir retiré l'annotation @Override. Cela peut laisser penser qu'elle est inutile. Le piège du typo : C'est la partie la plus importante. L'auteur introduit une faute de frappe dans le nom de la méthode. Observez comment le compilateur réagit différemment avec et sans l'annotation @Override. C'est la démonstration parfaite de sa valeur.


4. Le polymorphisme en action

Voyons un exemple complet qui rassemble tous ces concepts. Nous allons créer une liste de différentes formes géométriques et les afficher, en laissant le polymorphisme faire le travail.

Classes de base :

// Classe Mère
public class Forme {
    public void dessiner() {
        System.out.println("Dessin d'une forme générique.");
    }
}

// Classe Fille 1
public class Cercle extends Forme {
    @Override
    public void dessiner() {
        System.out.println("Dessin d'un cercle : O");
    }
}

// Classe Fille 2
public class Carre extends Forme {
    @Override
    public void dessiner() {
        System.out.println("Dessin d'un carré : []");
    }
}

Utilisation polymorphique :

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        // Une liste de Forme peut contenir des Cercle, des Carre, etc.
        List<Forme> formes = new ArrayList<>();
        formes.add(new Cercle());
        formes.add(new Carre());
        formes.add(new Forme());
        formes.add(new Cercle());

        // On parcourt la liste et on appelle la méthode dessiner() pour chaque élément
        for (Forme forme : formes) {
            // La JVM appelle la bonne version de dessiner() en fonction du type réel de l'objet
            forme.dessiner(); 
        }
    }
}

Résultat de l'exécution :

Dessin d'un cercle : O
Dessin d'un carré : []
Dessin d'une forme générique.
Dessin d'un cercle : O

Cet exemple montre la puissance du polymorphisme : la boucle for n'a pas besoin de savoir si l'objet est un Cercle ou un Carre. Elle le traite simplement comme une Forme et appelle dessiner(). La JVM se charge de choisir la bonne implémentation à chaque itération.


5. Travaux pratiques

Il est temps de mettre en pratique ce que vous avez appris. L'exercice suivant vous demande de modéliser une hiérarchie de classes, de redéfinir la méthode toString() et de vérifier le comportement polymorphique.

Mise en oeuvre du concept d'héritage - KooR.fr

Cet article propose un excellent exercice de synthèse sur l'héritage et le polymorphisme.

Lisez la section "Travaux pratiques" et réalisez l'exercice demandé. Analysez le diagramme UML pour comprendre les classes Shape, Circle, Square et leurs relations. Notez que vous devrez utiliser la classe java.awt.Point, qui fait partie du JDK. Implémentez les trois classes en Java. Assurez-vous de : Mettre en place la relation d'héritage avec extends. Créer les constructeurs et utiliser super() pour appeler le constructeur parent. Redéfinir la méthode toString() dans chaque classe en utilisant l'annotation @Override. Créez la classe Start pour tester votre code. Créez une collection (List<Shape>) et peuplez-la avec des instances de Circle et de Square. Parcourez la collection et affichez chaque objet. Grâce à la redéfinition de toString(), System.out.println() appellera automatiquement la bonne version de la méthode pour chaque objet. Ne regardez la correction qu'après avoir sérieusement tenté de résoudre l'exercice par vous-même. C'est en codant que les concepts s'ancrent le mieux.


Conclusion

Aujourd'hui, vous avez découvert le polymorphisme, un concept qui rend le code orienté objet flexible, extensible et facile à maintenir.

Points clés à retenir :

  • Le polymorphisme permet de manipuler des objets de différentes classes filles via une référence de type super-classe, tout en invoquant le comportement spécifique de chaque objet.
  • La redéfinition de méthode (method overriding) est le mécanisme qui permet à une sous-classe de fournir une implémentation spécifique pour une méthode héritée.
  • L'annotation @Override est un filet de sécurité indispensable qui garantit que vous redéfinissez bien une méthode existante, vous protégeant ainsi des bugs subtils dus à des erreurs de frappe.

Dans les prochaines leçons, nous allons pousser ce concept encore plus loin avec les interfaces et les classes abstraites. Vous verrez comment définir des "contrats" de comportement sans même fournir d'implémentation, ce qui est la clé pour construire des systèmes logiciels découplés et hautement modulaires, une pratique courante dans le développement backend professionnel.