Menu

L'héritage en Java : extends, super et la redéfinition

Comment une sous-classe Java hérite des champs et méthodes avec extends, appelle la classe parente via super et redéfinit le comportement - ainsi que les pièges courants.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

L'héritage est une réutilisation de type "est-un"

L'héritage permet à une nouvelle classe de s'appuyer sur une classe existante. La nouvelle classe -la sous-classe- récupère automatiquement les champs et méthodes de la superclasse, puis ajoute ou modifie ce dont elle a besoin. On y recourt lorsqu'un type est une variante plus spécifique d'un autre : un Dog est un Animal, un SavingsAccount est un BankAccount.

On l'écrit avec le mot-clé extends. Tout ce qui est public ou protected dans le parent est disponible dans l'enfant sans avoir à le réécrire.

Dog ne déclare jamais name ni eat(), et pourtant il possède les deux. C'est tout l'intérêt : le comportement partagé vit à un seul endroit.

super : atteindre le parent

Le constructeur d'une sous-classe doit s'assurer que le parent est initialisé en premier. Vous le faites avec super(...), qui appelle le constructeur du parent et doit être la première instruction du constructeur de la sous-classe. Si vous l'omettez, Java insère un appel silencieux au constructeur sans argument du parent - et si le parent n'a pas un tel constructeur, le code ne compile pas.

La sortie montre que le constructeur du parent s'exécute avant le corps de l'enfant - la construction se propage du sommet de la hiérarchie vers le bas.

Redéfinir des méthodes

Une sous-classe peut remplacer une méthode héritée en la redéfinissant avec la même signature. C'est la redéfinition, et vous devriez toujours la marquer avec @Override. L'annotation n'est pas obligatoire, mais elle pousse le compilateur à vérifier que vous correspondez réellement à une méthode du parent - elle attrape les fautes de frappe comme tostring() au lieu de toString() qui, sinon, créeraient silencieusement une toute nouvelle méthode.

Même si le tableau est typé Animal, chaque élément exécute sa propre méthode speak(). Java choisit la méthode en fonction de l'objet réel à l'exécution, et non du type déclaré de la variable - c'est le fondement du polymorphisme.

Appeler la version du parent avec super.method()

Redéfinir n'oblige pas à jeter le travail du parent. Utilisez super.method() pour exécuter la version héritée, puis l'enrichir :

Sans super., appeler log à l'intérieur de TimestampLogger.log s'appellerait lui-même et entrerait en récursion infinie. super. signifie explicitement "la version du parent".

Champs hérités et accès

Une sous-classe voit les membres public et protected du parent, mais pas ses membres private. Les champs private existent toujours dans l'objet - les propres méthodes du parent peuvent y accéder - mais la sous-classe ne peut pas les référencer directement. Utilisez protected lorsque vous voulez que les sous-classes y aient accès tout en gardant un membre caché du code sans rapport.

class Base {
    private int secret;      // invisible pour les sous-classes
    protected int shared;    // visible pour les sous-classes
}

class Derived extends Base {
    void demo() {
        shared = 5;          // OK
        // secret = 5;       // erreur de compilation - private à Base
    }
}

C'est aussi pourquoi le constructeur d'une sous-classe doit souvent appeler super(...) : c'est le seul moyen d'initialiser l'état privé du parent.

Empêcher l'héritage avec final

Parfois, une classe ne devrait pas pouvoir être étendue du tout - String est final précisément pour cette raison. Marquer une classe final interdit le sous-classement ; marquer une méthode final interdit de la redéfinir tout en autorisant encore l'extension de la classe.

final class Constants { }            // ne peut pas être sous-classée

class Config {
    final void load() { }            // les sous-classes peuvent étendre Config
                                     // mais ne peuvent pas redéfinir load()
}

Recourez à final lorsque le comportement d'une classe doit être garanti et immuable dans tout le programme - c'est un signal délibéré de "ne pas étendre", pas l'option par défaut.

Un piège courant : préférez la composition quand ce n'est pas "est-un"

L'héritage est tentant parce qu'il réutilise du code, mais il couple fortement l'enfant au parent. Si la relation n'est pas un véritable "est-un" - disons une Car qui a simplement besoin d'un Engine - ne faites pas Car extends Engine. Une voiture a un moteur, elle n'est pas un moteur. Modélisez cela avec un champ (composition) à la place :

class Car {
    private Engine engine = new Engine();   // Car A-UN Engine

    void start() { engine.ignite(); }
}

N'utilisez l'héritage que lorsque la sous-classe est vraiment une forme spécialisée de la superclasse et que vous voulez hériter de son comportement et le substituer.

Suite : Les interfaces

L'héritage avec extends vous donne un parent unique et une implémentation partagée. Mais une classe ne peut étendre qu'une seule classe - alors comment donner une capacité commune à des classes sans rapport entre elles ? C'est à cela que servent les interfaces : un contrat que de nombreuses classes peuvent implémenter, et le sujet de la page suivante.

Questions fréquentes

Qu'est-ce que l'héritage en Java ?

L'héritage permet à une classe (la sous-classe) de réutiliser les champs et méthodes d'une autre classe (la superclasse) grâce au mot-clé extends. La sous-classe récupère automatiquement les membres public et protected du parent et peut en ajouter de nouveaux ou remplacer le comportement hérité par redéfinition. Il modélise une relation "est-un" : un Dog est un Animal.

À quoi sert le mot-clé super en Java ?

super désigne la classe parente. super(...) dans un constructeur appelle le constructeur du parent (et doit être la première instruction), tandis que super.method() appelle la version du parent d'une méthode que vous avez redéfinie. Cela permet à une sous-classe de s'appuyer sur la logique du parent au lieu de la remplacer entièrement.

Quelle est la différence entre la redéfinition et la surcharge en Java ?

La redéfinition (overriding) redéfinit une méthode héritée dans une sous-classe en utilisant la même signature, ce qui change le comportement - marquez-la avec @Override. La surcharge (overloading) définit plusieurs méthodes portant le même nom mais avec des listes de paramètres différentes dans la même classe. La redéfinition concerne l'héritage et la résolution à l'exécution ; la surcharge n'est que deux méthodes qui partagent un nom.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER