HOWTO · Java
Étendre deux classes en Java
Découvrez pourquoi une classe Java ne peut étendre qu'une seule classe et comment les interfaces et la composition combinent le comportement de plusieurs types.
Sur cette page
Une classe Java ne peut pas étendre deux classes. Chaque classe possède une seule superclasse directe ; une déclaration telle que class Child extends First, Second n’est donc pas valide en Java. Pour combiner le comportement de plusieurs types, étendez une classe et implémentez autant d’interfaces que nécessaire, ou détenez des références vers d’autres objets et déléguez-leur des tâches par composition.
Pourquoi une classe Java ne peut pas étendre deux classes
Java utilise l’héritage simple pour les classes : chacune peut désigner au plus une superclasse avec extends. Un objet possède ainsi une seule chaîne d’héritage de classes, ce qui évite toute ambiguïté sur l’implémentation ou l’état à utiliser lorsque deux parents définissent le même membre. Le compilateur rejette une déclaration de classe qui énumère deux superclasses ; il s’agit d’une restriction à la compilation, et non d’une erreur qui survient pendant l’exécution du programme.
La syntaxe suivante est volontairement invalide en Java et ne compile pas : class Child extends First, Second {}. Utilisez une seule superclasse lorsque la nouvelle classe est réellement une version spécialisée de celle-ci. La section suivante montre comment les contrats d’interface peuvent décrire d’autres capacités.
Utiliser une superclasse et implémenter plusieurs interfaces
Une classe peut étendre une classe et implémenter plusieurs interfaces. La superclasse peut fournir un état ou une implémentation communs, tandis que chaque interface déclare une capacité que la classe s’engage à fournir. Les mots-clés Java sont importants : une classe utilise extends pour sa superclasse et implements pour les interfaces. Une interface peut elle-même étendre plusieurs interfaces, mais cela ne donne pas plusieurs superclasses de classe à une classe.
Cet exemple complet définit deux contrats d’interface et les implémente en plus d’une superclasse :
public class CatalogItem extends Record implements Printable, Identifiable {
CatalogItem(String name) {
super(name);
}
@Override
public void printLabel() {
System.out.println(name());
}
@Override
public String id() {
return "item-" + name().toLowerCase();
}
public static void main(String[] args) {
CatalogItem item = new CatalogItem("Book");
item.printLabel();
System.out.println(item.id());
}
}
interface Printable {
void printLabel();
}
interface Identifiable {
String id();
}
class Record {
private final String name;
Record(String name) {
this.name = name;
}
String name() {
return name;
}
}
Book
item-book
CatalogItem hérite de l’implémentation de name() fournie par Record et fournit les méthodes requises par les deux interfaces. Les interfaces sont utiles lorsque des classes sans lien entre elles doivent partager un type ou un contrat de capacité. À elles seules, elles ne fournissent pas l’état ni l’implémentation de classe qu’apporterait une deuxième superclasse.
Combiner le comportement de classes par composition
La composition combine des objets selon une relation de type « possède un » : une classe stocke des références vers des collaborateurs et appelle leurs méthodes. Cette approche est utile lorsque les comportements proviennent de classes qui ont chacune leur utilité, ou lorsque le collaborateur doit pouvoir être remplacé. La classe englobante contrôle les opérations qu’elle expose et ne délègue que le travail nécessaire à ses appelants.
Ici, un SmartDevice contient un PowerController et un MessageSender, puis leur délègue des opérations :
public class SmartDevice {
private final PowerController power = new PowerController();
private final MessageSender sender = new MessageSender();
void start() {
power.powerOn();
}
void sendMessage(String message) {
sender.send(message);
}
public static void main(String[] args) {
SmartDevice device = new SmartDevice();
device.start();
device.sendMessage("Ready");
}
}
class PowerController {
void powerOn() {
System.out.println("Power on");
}
}
class MessageSender {
void send(String message) {
System.out.println("Sending: " + message);
}
}
Power on
Sending: Ready
SmartDevice ne devient ni un PowerController ni un MessageSender : il utilise les deux. Dans un programme plus vaste, les collaborateurs peuvent être fournis par un constructeur ou représentés par des interfaces, ce qui facilite le remplacement de leurs implémentations et les tests indépendants de la classe englobante.
Choisir le bon modèle de réutilisation
Choisissez l’héritage de classe lorsque le nouveau type est réellement une forme spécialisée d’une superclasse et qu’il doit hériter de son implémentation. Ajoutez des interfaces lorsqu’un type doit s’engager à fournir des capacités partagées avec d’autres classes. Utilisez la composition lorsque le comportement doit être assemblé à partir de composants, délégué ou modifié indépendamment.
Veillez à ce que chaque classe ait une responsabilité claire et n’expose que les opérations dont les appelants ont besoin. Préférez de petits contrats d’interface aux contrats trop larges, et évitez de dupliquer un comportement qui relève d’un collaborateur partagé. Ces choix rendent les relations plus faciles à comprendre et à tester, et facilitent leur adaptation lorsque les besoins évoluent. En résumé, Java ne permet pas à une classe d’étendre deux classes ; une superclasse, plusieurs contrats d’interface et des collaborateurs intégrés par composition couvrent les besoins de réutilisation courants.