W3docs

L'héritage en Java

Réutilisez et étendez le comportement des classes Java avec le mot-clé extends et les règles de l'héritage simple.

L'héritage permet à une classe de s'appuyer sur une autre au lieu de repartir de zéro. La nouvelle classe — la sous-classe — hérite de tous les champs et méthodes du parent, et ajoute (ou remplace) uniquement ce qui est différent. C'est ainsi que Java exprime les relations "est-un" : un Cat est un Animal, un AdminUser est un User.

Le mot-clé est extends. Les mécanismes sont simples. La partie difficile — et c'est vraiment le sujet de ce chapitre — consiste à reconnaître quand l'héritage est le bon outil et quand il ne l'est pas.

Un premier exemple

public class Animal {
  String name;
  void breathe() { System.out.println(name + " breathes"); }
}

public class Cat extends Animal {
  void purr() { System.out.println(name + " purrs"); }
}

Cat déclare uniquement purr(). Elle possède tout de même name et breathe(), car ils ont été hérités :

Cat c = new Cat();
c.name = "Mittens";
c.breathe();     // Mittens breathes — inherited from Animal
c.purr();        // Mittens purrs   — declared on Cat

Animal est la superclasse (ou parent), Cat est la sous-classe (ou enfant). D'autres langages les appellent classe de base/classe dérivée.

Ce qui est hérité

Une sous-classe hérite :

  • De tous les champs et méthodes public et protected de chaque ancêtre.
  • Des champs et méthodes à accès paquetage, si la sous-classe se trouve dans le même paquetage.
  • Tous les membres hérités conservent leurs modificateurs d'origine.

Une sous-classe n'hérite pas :

  • Des constructeurs. Ils ne sont pas des membres au même sens — la sous-classe a besoin des siens.
  • Des champs et méthodes private. Ils existent dans le parent (la disposition mémoire d'un Cat inclut encore les champs private d'Animal), mais la sous-classe ne peut pas les référencer par leur nom. La seule façon de les lire est d'utiliser les méthodes d'accès public/protected héritées.

Héritage simple — un seul parent

Chaque classe étend exactement une autre classe. Il n'y a pas d'héritage multiple pour les classes en Java :

public class Hybrid extends Animal, Vehicle { }    // ERROR — no multiple inheritance

Vous pouvez implémenter plusieurs interfaces (abordées dans interfaces) — cela vous offre la flexibilité multi-source que la plupart des langages obtiennent grâce à l'héritage multiple, sans les problèmes d'ambiguïté.

Si vous n'écrivez pas extends du tout, la classe étend implicitement Object :

public class Foo { }
// is equivalent to
public class Foo extends Object { }

C'est pourquoi chaque objet Java possède toString(), equals(), hashCode() et getClass() — ils sont tous définis sur Object. Le chapitre sur la classe Object les passe tous en revue.

Les classes qui ne peuvent pas être étendues

Une classe marquée final ne peut pas être un parent du tout — essayer de l'étendre provoque une erreur de compilation :

public final class Money { }

public class Coupon extends Money { }   // ERROR — cannot inherit from final Money

C'est délibéré, non une omission : de nombreux types du cœur du langage sont final précisément pour qu'aucune sous-classe ne puisse modifier leur comportement. String, Integer et les autres classes d'enveloppe sont toutes final, ce qui fait partie de ce qui les rend sûres à partager et à mettre en cache. Quand vous souhaitez un type qui ne peut pas être sous-classé — généralement pour des garanties de sécurité ou d'immuabilité — marquez-le final.

Constructeurs et super

Chaque constructeur de sous-classe doit, comme première action, appeler un constructeur du parent. Si vous n'écrivez pas cet appel, Java insère super() pour vous :

public class Animal {
  String name;
  public Animal(String name) { this.name = name; }
}

public class Cat extends Animal {
  public Cat(String name) {
    super(name);             // call Animal(String)
  }
}

Si Animal n'avait pas de constructeur sans argument et que Cat n'écrivait pas super(...), le compilateur se plaindrait — il n'y a pas d'Animal() à insérer implicitement. Le chapitre sur le mot-clé super couvre en détail le chaînage des constructeurs avec super(...) et les appels super.method().

Redéfinition

Une sous-classe peut remplacer une méthode héritée en en déclarant une avec la même signature :

public class Animal {
  String speak() { return "(some noise)"; }
}

public class Cat extends Animal {
  @Override
  String speak() { return "meow"; }
}

Cat c = new Cat();
System.out.println(c.speak());   // meow

@Override est une annotation qui indique au compilateur « j'ai l'intention que cette méthode remplace une méthode héritée — veuillez signaler une erreur si ce n'est pas le cas. » Utilisez-la toujours. Elle détecte les fautes de frappe et les incompatibilités de signature qui, sinon, créeraient silencieusement une nouvelle méthode au lieu de redéfinir l'ancienne. Le chapitre sur la redéfinition de méthodes couvre toutes les règles.

Transtypage vers le haut et polymorphisme

Une instance de sous-classe peut être affectée à une variable du type parent :

Animal a = new Cat();    // upcast — implicit
a.speak();               // calls Cat's speak() — picked at runtime

C'est le fondement du polymorphisme, le chapitre suivant. La variable a est de type Animal, mais l'objet réel est un Cat, donc la version Cat de speak s'exécute.

Quand l'héritage n'est pas le bon outil

L'héritage est le mécanisme le plus surutilisé en POO. Voici quelques signes d'alerte indiquant que vous devriez utiliser la composition (un champ d'un autre type) à la place :

  • La sous-classe ne passe pas vraiment le test « est-un ». Un Stack n'est pas vraiment un Vector — pourtant java.util.Stack étend Vector et est largement considéré comme une erreur de conception.
  • Les éléments internes mutables du parent fuient dans la sous-classe. Les modifications de l'implémentation du parent cassent la sous-classe.
  • Vous héritez pour réutiliser quelques méthodes, pas parce que les types sont véritablement substituables.

La règle de base tirée du livre Effective Java de Joshua Bloch : préférez la composition à l'héritage. Si B a besoin du comportement de A mais n'est pas vraiment un A, donnez à B un champ privé de type A et transmettez ce dont il a besoin.

// Inheritance — fragile
public class MyList extends ArrayList<String> { ... }

// Composition — robust
public class MyList {
  private final List<String> inner = new ArrayList<>();
  public void add(String s) { inner.add(s); }
}

La version avec composition est à l'abri des surprises quand ArrayList ajoute de nouvelles méthodes ou modifie le fonctionnement de ses champs privés.

Héritage et accès

Les membres hérités conservent le modificateur qu'ils avaient dans le parent. Une sous-classe ne peut pas restreindre la visibilité d'une méthode redéfinie — rendre une méthode public private dans la sous-classe violerait le principe de substitution de Liskov, et le compilateur le refuse :

public class A {
  public void hello() { }
}
public class B extends A {
  private void hello() { }    // ERROR — cannot reduce visibility
}

Vous pouvez élargir la visibilité (redéfinir une méthode protected en public), mais c'est rarement nécessaire.

Un exemple complet

java— editable, runs on the server

La suite

super est apparu plusieurs fois ici — dans le chaînage des constructeurs et comme moyen d'accéder à une méthode parent redéfinie. Le prochain chapitre sur le mot-clé super passe en revue toutes les situations où il apparaît. Quand un parent doit définir ce que ses enfants font mais pas comment, on fait appel aux classes abstraites, qui s'appuient directement sur les règles d'héritage couvertes ici.

Pratique

Pratique
Laquelle de ces affirmations sur l'héritage Java est vraie ?
Laquelle de ces affirmations sur l'héritage Java est vraie ?
Was this page helpful?