CodeGym /Cours /JAVA 25 SELF /Redéfinition de méthodes (overriding) : différence avec l...

Redéfinition de méthodes (overriding) : différence avec la surcharge

JAVA 25 SELF
Niveau 18 , Leçon 2
Disponible

1. Redéfinition de méthodes

La redéfinition (overriding) — c’est la possibilité, dans une sous-classe, d’écrire sa propre version d’une méthode déjà présente dans la classe parente. Grâce à cela, le véritable polymorphisme fonctionne à l’exécution du programme (run-time).

En termes simples :
Si vous avez une classe de base avec une méthode et que vous voulez que la sous-classe exécute cette méthode à sa manière, il suffit de déclarer une méthode avec la même signature dans la sous-classe. Lors de l’appel de la méthode via une référence de type de base, c’est la version de la méthode du type réel de l’objet qui sera appelée.

Exemple de la vie courante.
Imaginez que vous avez une équipe d’animaux et que vous demandez à chacun « émettre un son ». Pour tous, la commande est makeSound(), mais le chien aboie, le chat miaule et la vache meugle. Dans le code, cela ressemble à l’appel de la même méthode, mais le résultat diffère — voilà la magie de la redéfinition !

Syntaxe de la redéfinition

Exemple de base

class Animal {
    void makeSound() {
        System.out.println("L’animal émet un son");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Ouaf !");
    }
}

class Cat extends Animal {
    @Override
    void makeSound() {
        System.out.println("Miaou !");
    }
}

Ici, la classe Animal définit la méthode makeSound(). Les sous-classes Dog et Cat redéfinissent cette méthode en fournissant leur propre implémentation.

Annotation @Override

En Java, il est recommandé (et c’est une bonne habitude) de marquer les méthodes redéfinies avec l’annotation @Override:

@Override
void makeSound() { ... }

Ce n’est pas obligatoire pour que le code fonctionne, mais :

  • Le compilateur vérifiera que vous redéfinissez bien une méthode (et non que vous vous êtes trompé de nom ou de paramètres).
  • Améliore la lisibilité du code — les autres développeurs voient immédiatement que cette méthode redéfinit celle du parent.

Appel d’une méthode redéfinie

Animal myDog = new Dog();
myDog.makeSound(); // Affichera : Ouaf !

Même si la variable est de type Animal, elle pointe en réalité vers un objet Dog, et c’est la méthode de la classe Dog qui est appelée. C’est la liaison dynamique (tardive).

2. Différence entre la redéfinition (overriding) et la surcharge (overloading)

Surcharge (overloading)

  • Dans une même classe (ou dans une hiérarchie, mais toujours « proches »).
  • Les méthodes ont le même nom mais des paramètres différents (type, nombre, ordre).
  • Le choix de la méthode se fait à la compilation.
void print(int x) { ... }
void print(String s) { ... }

Redéfinition (overriding)

  • Dans des classes différentes : la méthode est déclarée dans la superclasse et redéfinie dans la sous-classe.
  • Les méthodes ont le même nom et la même signature (paramètres et type de retour).
  • Le choix de la méthode se fait à l’exécution (run-time), en fonction du type réel de l’objet.

Tableau de comparaison

Surcharge (overloading) Redéfinition (overriding)
Dans une même classe Dans la superclasse et la sous-classe
Nom de la méthode Identique Identique
Paramètres Différents Identiques
Type de retour Peut différer Doit correspondre ou être un sous-type
Quand Compilation Exécution (run-time)
Annotation Non requis @Override (recommandé)

3. Règles de la redéfinition des méthodes

La redéfinition est puissante, mais elle s’accompagne de règles strictes. Passons-les en revue.

La signature de la méthode doit correspondre

  • Le nom de la méthode, les types et l’ordre des paramètres doivent être identiques à ceux de la méthode dans la superclasse.
  • Le type de retour doit correspondre ou être covariant (c’est-à-dire un sous-type du type de retour de la méthode parente).

Exemple avec un type de retour covariant :

class Animal {
    Animal reproduce() { return new Animal(); }
}
class Cat extends Animal {
    @Override
    Cat reproduce() { return new Cat(); } // OK ! Cat est un sous-type d’Animal
}

Modificateur d’accès

Le modificateur d’accès d’une méthode redéfinie ne peut pas être plus restrictif que celui de la méthode dans la superclasse. Si la méthode du parent est public, alors elle doit rester public dans la sous-classe. Il est impossible de la rendre moins accessible (protected ou private).

Exemple :

class Parent {
    public void greet() { }
}
class Child extends Parent {
    // void greet() { } // Erreur ! Le modificateur par défaut — package-private, moins accessible que public
    @Override
    public void greet() { } // OK
}

Exceptions

  • La méthode redéfinie ne peut pas lever de nouvelles exceptions vérifiées (checked) qui ne sont pas déclarées dans la méthode de base.
  • Elle peut lever moins d’exceptions ou les mêmes.

Exemple :

class Parent {
    void doWork() throws IOException { }
}
class Child extends Parent {
    @Override
    void doWork() throws FileNotFoundException { } // OK, FileNotFoundException est un sous-type d’IOException
    // void doWork() throws SQLException { } // Erreur ! SQLException n’est pas déclaré dans le parent
}

Les méthodes statiques ne sont pas redéfinies

Les méthodes statiques peuvent être masquées (hidden), mais pas redéfinies. Si vous déclarez une méthode statique avec la même signature dans une sous-classe, ce n’est pas une redéfinition ! Ce sera simplement du masquage, pas du polymorphisme.

class Animal {
    static void info() { System.out.println("Animal"); }
}
class Dog extends Animal {
    static void info() { System.out.println("Dog"); }
}

L’appel Dog.info() affichera "Dog", mais si vous l’appelez via une variable de type Animal, c’est la méthode Animal.info() qui sera invoquée. Ce n’est pas du polymorphisme !

On ne peut pas redéfinir les méthodes final

Si une méthode dans la superclasse est déclarée final, toute tentative de la redéfinir conduira à une erreur de compilation.

class Animal {
    final void sleep() { }
}
class Dog extends Animal {
    // @Override
    // void sleep() { } // Erreur ! Impossible de redéfinir une méthode final
}

4. Exemples pratiques

Voyons en pratique comment fonctionne la redéfinition et en quoi elle diffère de la surcharge.

Exemple 1 : la classe Shape et ses descendants

class Shape {
    void draw() {
        System.out.println("On dessine une figure");
    }
}

class Circle extends Shape {
    @Override
    void draw() {
        System.out.println("On dessine un cercle");
    }
}

class Rectangle extends Shape {
    @Override
    void draw() {
        System.out.println("On dessine un rectangle");
    }
}

Utilisons le polymorphisme :

public class Main {
    public static void main(String[] args) {
        Shape s1 = new Circle();
        Shape s2 = new Rectangle();
        s1.draw(); // On dessine un cercle
        s2.draw(); // On dessine un rectangle
    }
}

Bien que les variables soient déclarées comme Shape, c’est la méthode de la classe à laquelle l’objet appartient réellement qui est appelée.

Exemple 2 : différence avec la surcharge

class Printer {
    void print(String s) {
        System.out.println("Chaîne : " + s);
    }

    void print(int n) {
        System.out.println("Nombre : " + n);
    }
}

Ici, les deux méthodes s’appellent print, mais leurs paramètres diffèrent — c’est une surcharge, pas une redéfinition.

5. Redéfinition et appel de la méthode parente (super)

Parfois, dans une méthode redéfinie, on souhaite d’abord exécuter la logique du parent puis ajouter la sienne. Pour cela, on utilise le mot-clé super.

class Animal {
    void makeSound() {
        System.out.println("L’animal émet un son");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        super.makeSound(); // Appel de la méthode parente
        System.out.println("Ouaf !");
    }
}

L’appel new Dog().makeSound() affichera :

L’animal émet un son
Ouaf !

Comment fonctionne la liaison dynamique (late binding)

Lorsque vous appelez une méthode via une référence de type de base, Java, à l’exécution, regarde à quel objet réel correspond cette référence et appelle précisément la version de la méthode qui est définie dans la classe de cet objet.

Animal a = new Cat();
a.makeSound(); // Appellera Cat.makeSound(), et non Animal.makeSound()

C’est la base du polymorphisme en Java.

6. Lien avec votre application

Dans notre application pédagogique (par exemple, un système de gestion des employés), vous pouvez créer une classe de base Employee avec une méthode work(), et les sous-classes Manager et Developer peuvent implémenter cette méthode à leur manière :

class Employee {
    void work() {
        System.out.println("L’employé travaille");
    }
}

class Manager extends Employee {
    @Override
    void work() {
        System.out.println("Le manager dirige");
    }
}

class Developer extends Employee {
    @Override
    void work() {
        System.out.println("Le développeur écrit du code");
    }
}

Vous pouvez désormais stocker tous les employés dans un même tableau ou une même liste :

Employee[] employees = {new Manager(), new Developer(), new Developer()};
for (Employee e : employees) {
    e.work(); // Chacun a sa propre sortie !
}

7. Erreurs courantes lors de la redéfinition de méthodes

Erreur n° 1: faute de frappe dans le nom de la méthode ou ses paramètres. Si vous vous trompez par inadvertance dans le nom de la méthode ou ses paramètres, vous ne redéfinissez pas la méthode, vous en créez une nouvelle. En conséquence, le polymorphisme ne fonctionne pas. C’est précisément pourquoi il faut toujours utiliser l’annotation @Override — le compilateur vous corrigera immédiatement.

Erreur n° 2: modificateur d’accès plus strict. Si, chez le parent, la méthode est public, et que vous la déclarez protected ou sans modificateur dans la classe fille, vous obtiendrez une erreur de compilation.

Erreur n° 3: tentative de redéfinir une méthode static ou final. Les méthodes statiques ne sont pas redéfinies, et les méthodes final ne peuvent pas être redéfinies du tout. Si vous essayez — le compilateur vous arrêtera.

Erreur n° 4: modification du type de retour vers un type incompatible. Si le type de retour de la méthode dans la sous-classe ne correspond pas à celui de la superclasse (et n’en est pas un sous-type), le compilateur n’autorisera pas la redéfinition.

Erreur n° 5: ajout de nouvelles exceptions vérifiées. Une méthode redéfinie ne peut pas lever de nouvelles exceptions vérifiées (checked) qui ne figurent pas dans la déclaration de la méthode de base. Si vous le faites — le compilateur signalera une erreur.

Erreur n° 6: oublier super. Si, dans une méthode redéfinie, vous souhaitez conserver une partie du comportement du parent, n’oubliez pas d’appeler explicitement super.methodName(). Java ne le fera pas pour vous.

Vous savez maintenant comment fonctionne la redéfinition de méthodes, en quoi elle se distingue de la surcharge, et comment elle permet de réaliser le polymorphisme en Java. Dans la prochaine leçon, nous verrons comment appliquer le polymorphisme en pratique — avec des collections, des tableaux et des cas concrets !

Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION