Lors de la conception d’un modèle, nous voulons parfois créer une entité qui ne peut pas être instanciée directement. Par exemple, imaginons que nous avons une classe Canin
avec les sous-classes Loup
, Renard
, et Coyote
. Nous voulons que d’autres développeurs puissent créer des instances des sous-classes, mais peut-être que nous ne voulons pas qu’ils puissent créer une instance de Canin
. En d’autres termes, nous voulons forcer tous les objets de Canin
à avoir un type particulier à l’exécution.
Introduction aux Classes Abstraites
Voici les classes abstraites. Une classe abstraite est une classe déclarée avec le modificateur abstract
qui ne peut pas être instanciée directement et peut contenir des méthodes abstraites. Voyons un exemple basé sur le modèle de données Canin
:
public abstract class Canin {}
public class Loup extends Canin {}
public class Renard extends Canin {}
public class Coyote extends Canin {}
Dans cet exemple, d’autres développeurs peuvent créer des instances de Loup
, Renard
, ou Coyote
, mais pas de Canin
. Bien sûr, ils peuvent passer une référence de variable comme un Canin
, mais l’objet sous-jacent doit être une sous-classe de Canin
à l’exécution.
Mais attendez, il y a plus ! Une classe abstraite peut contenir des méthodes abstraites. Une méthode abstraite est une méthode déclarée avec le modificateur abstract
qui ne définit pas de corps. Autrement dit, une méthode abstraite force les sous-classes à surcharger la méthode.
Pourquoi voudrions-nous cela ? Le polymorphisme, bien sûr ! En déclarant une méthode abstraite, nous pouvons garantir qu’une certaine version sera disponible sur une instance sans avoir à spécifier quelle est cette version dans la classe parente abstraite.
public abstract class Canin {
public abstract String getSon();
public void aboyer() { System.out.println(getSon()); }
}
public class Loup extends Canin {
public String getSon() {
return "Wooooooof!";
}
}
public class Renard extends Canin {
public String getSon() {
return "Glapissement!";
}
}
public class Coyote extends Canin {
public String getSon() {
return "Rugissement!";
}
}
Nous pouvons ensuite créer une instance de Renard
et l’assigner au type parent Canin
. La méthode surchargée sera utilisée à l’exécution.
public static void main(String[] p) {
Canin r = new Renard();
r.aboyer(); // Glapissement!
}
Facile jusqu’ici. Mais il y a certaines règles dont vous devez être conscient :
- Seules les méthodes d’instance peuvent être marquées
abstract
dans une classe, pas les variables, constructeurs, ou méthodesstatic
. - Une méthode abstraite ne peut être déclarée que dans une classe abstraite.
- Une classe non abstraite qui étend une classe abstraite doit implémenter toutes les méthodes abstraites héritées.
- La surcharge d’une méthode abstraite suit les règles existantes pour la surcharge des méthodes.
Voyons si vous pouvez repérer pourquoi chacune de ces déclarations de classe ne compile pas :
public class RenardFennec extends Canin {
public int getSon() {
return 10;
}
}
public class RenardArctique extends Canin {}
public class LoupGeant extends Canin {
public abstract repos();
public String getSon() {
return "Ouaf!";
}
}
public class Chacal extends Canin {
public abstract String nom;
public String getSon() {
return "Rire";
}
}
Tout d’abord, la classe RenardFennec
ne compile pas car c’est une surcharge de méthode invalide. En particulier, les types de retour ne sont pas covariants. La classe RenardArctique
ne compile pas car elle ne surcharge pas la méthode abstraite getSon()
. La classe LoupGeant
ne compile pas car elle n’est pas abstraite mais déclare une méthode abstraite repos()
. Enfin, la classe Chacal
ne compile pas car les variables ne peuvent pas être marquées abstraites.
Une classe abstraite est le plus souvent utilisée lorsque vous voulez qu’une autre classe hérite des propriétés d’une classe particulière, mais que vous voulez que la sous-classe remplisse certains détails d’implémentation.
Plus tôt, nous avons dit qu’une classe abstraite est une classe qui ne peut pas être instanciée. Cela signifie que si vous essayez de l’instancier, le compilateur signalera une exception, comme dans cet exemple :
abstract class Alligator {
public static void main(String... nourriture) {
var a = new Alligator(); // NE COMPILE PAS
}
}
Une classe abstraite peut être initialisée, mais seulement dans le cadre de l’instanciation d’une sous-classe non abstraite.
Déclaration de Méthodes Abstraites
Une méthode abstraite est toujours déclarée sans corps. Elle inclut également un point-virgule (;) après la déclaration de la méthode. Comme vous l’avez vu dans l’exemple précédent, une classe abstraite peut inclure des méthodes non abstraites, dans ce cas avec la méthode aboyer()
. En fait, une classe abstraite peut inclure tous les mêmes membres qu’une classe non abstraite, y compris des variables, des méthodes static
et d’instance, des constructeurs, etc.
Cela pourrait vous surprendre de savoir qu’une classe abstraite n’est pas obligée d’inclure des méthodes abstraites. Par exemple, le code suivant compile même s’il ne définit aucune méthode abstraite :
public abstract class Lama {
public void macher() {}
}
Même sans méthodes abstraites, la classe ne peut pas être directement instanciée. Gardez un œil sur les méthodes abstraites déclarées en dehors des classes abstraites, comme celle-ci :
public class Aigrette { // NE COMPILE PAS
public abstract void picorer();
}
Comme le modificateur final
, le modificateur abstract
peut être placé avant ou après le modificateur d’accès dans les déclarations de classe et de méthode, comme le montre cette classe Tigre
:
abstract public class Tigre {
abstract public int griffe();
}
Le modificateur abstract
ne peut pas être placé après le mot-clé class
dans une déclaration de classe, ni après le type de retour dans une déclaration de méthode. Les déclarations suivantes de Ours
et hurler()
ne compilent pas pour ces raisons :
public class abstract Ours { // NE COMPILE PAS
public int abstract hurler(); // NE COMPILE PAS
}
Il n’est pas possible de définir une méthode abstraite qui a un corps ou une implémentation par défaut. Vous pouvez toujours définir une méthode par défaut avec un corps—vous ne pouvez simplement pas la marquer comme abstract
. Tant que vous ne marquez pas la méthode comme final
, la sous-classe a la possibilité de surcharger la méthode héritée.
Création d’une Classe Concrète
Une classe abstraite devient utilisable lorsqu’elle est étendue par une sous-classe concrète. Une classe concrète est une classe non abstraite. La première sous-classe concrète qui étend une classe abstraite est requise pour implémenter toutes les méthodes abstraites héritées. Cela inclut l’implémentation de toutes les méthodes abstraites héritées des interfaces héritées.
Lorsque vous voyez une classe concrète étendant une classe abstraite, vérifiez qu’elle implémente bien toutes les méthodes abstraites requises. Pouvez-vous voir pourquoi la classe Morse
suivante ne compile pas ?
public abstract class Animal {
public abstract String getNom();
}
public class Morse extends Animal {} // NE COMPILE PAS
Dans cet exemple, nous voyons que Animal
est marquée comme abstract
et Morse
ne l’est pas, faisant de Morse
une sous-classe concrète d’Animal
. Puisque Morse
est la première sous-classe concrète, elle doit implémenter toutes les méthodes abstraites héritées—getNom()
dans cet exemple. Comme elle ne le fait pas, le compilateur signale une erreur avec la déclaration de Morse
.
Nous soulignons la première sous-classe concrète pour une raison. Une classe abstraite peut étendre une classe non abstraite et vice versa. Chaque fois qu’une classe concrète étend une classe abstraite, elle doit implémenter toutes les méthodes qui sont héritées comme abstraites. Illustrons cela avec un ensemble de classes héritées :
public abstract class Mammifere {
abstract void montrerCorne();
abstract void mangerFeuille();
}
public abstract class Rhinoceros extends Mammifere {
void montrerCorne() {} // Hérité de Mammifere
}
public class RhinocerosNoir extends Rhinoceros {
void mangerFeuille() {} // Hérité de Mammifere
}
Dans cet exemple, la classe RhinocerosNoir
est la première sous-classe concrète, tandis que les classes Mammifere
et Rhinoceros
sont abstraites. La classe RhinocerosNoir
hérite la méthode mangerFeuille()
comme abstraite et est donc requise pour fournir une implémentation, ce qu’elle fait. Qu’en est-il de la méthode montrerCorne()
? Puisque la classe parente, Rhinoceros
, fournit une implémentation de montrerCorne()
, la méthode est héritée dans la classe RhinocerosNoir
comme une méthode non abstraite. Pour cette raison, la classe RhinocerosNoir
est autorisée mais pas obligée de surcharger la méthode montrerCorne()
. Les trois classes dans cet exemple sont correctement définies et compilent.
Et si nous changions la déclaration de Rhinoceros
pour supprimer le modificateur abstract
?
public class Rhinoceros extends Mammifere { // NE COMPILE PAS
void montrerCorne() {}
}
En changeant Rhinoceros
en une classe concrète, elle devient la première classe non abstraite à étendre la classe abstraite Mammifere
. Par conséquent, elle doit fournir une implémentation des deux méthodes montrerCorne()
et mangerFeuille()
. Puisqu’elle ne fournit qu’une de ces méthodes, la déclaration modifiée de Rhinoceros
ne compile pas.
Essayons un exemple de plus. La classe concrète suivante Lion
hérite de deux méthodes abstraites, getNom()
et rugir()
:
public abstract class Animal {
abstract String getNom();
}
public abstract class GrosChat extends Animal {
protected abstract void rugir();
}
public class Lion extends GrosChat {
public String getNom() {
return "Lion";
}
public void rugir() {
System.out.println("Le Lion pousse un fort RUGISSEMENT!");
}
}
Dans cet exemple de code, GrosChat
étend Animal
mais est marqué comme abstract
; par conséquent, il n’est pas requis de fournir une implémentation pour la méthode getNom()
. La classe Lion
n’est pas marquée comme abstract
, et en tant que première sous-classe concrète, elle doit implémenter toutes les méthodes abstraites héritées non définies dans une classe parente. Les trois classes compilent avec succès.
Création de Constructeurs dans les Classes Abstraites
Même si les classes abstraites ne peuvent pas être instanciées, elles sont toujours initialisées à travers des constructeurs par leurs sous-classes. Par exemple, considérez le programme suivant :
abstract class Mammifere {
abstract CharSequence macher();
public Mammifere() {
System.out.println(macher()); // Cette ligne compile-t-elle ?
}
}
public class Ornithorynque extends Mammifere {
String macher() { return "délicieux!"; }
public static void main(String[] args) {
new Ornithorynque();
}
}
En utilisant les règles de constructeur, le compilateur insère un constructeur par défaut sans argument dans la classe Ornithorynque
, qui appelle d’abord super()
dans la classe Mammifere
. Le constructeur Mammifere
n’est appelé que lorsque la classe abstraite est initialisée à travers une sous-classe ; par conséquent, il y a une implémentation de macher()
au moment où le constructeur est appelé. Ce code compile et imprime délicieux!
à l’exécution.
Rappelez-vous que les classes abstraites sont initialisées avec des constructeurs de la même manière que les classes non abstraites. Par exemple, si une classe abstraite ne fournit pas de constructeur, le compilateur insérera automatiquement un constructeur par défaut sans argument.
La principale différence entre un constructeur dans une classe abstraite et une classe non abstraite est qu’un constructeur dans une classe abstraite ne peut être appelé que lorsqu’il est initialisé par une sous-classe non abstraite. Cela a du sens, car les classes abstraites ne peuvent pas être instanciées.
Repérer les Déclarations Invalides
Nous concluons notre discussion sur les classes abstraites avec une revue des problèmes potentiels que vous êtes plus susceptibles de rencontrer.
Pouvez-vous voir pourquoi chacune des méthodes suivantes ne compile pas ?
public abstract class Tortue {
public abstract long manger() // NE COMPILE PAS
public abstract void nager() {}; // NE COMPILE PAS
public abstract int getAge() { // NE COMPILE PAS
return 10;
}
public abstract void dormir; // NE COMPILE PAS
public void allerDansCoquille(); // NE COMPILE PAS
}
La première méthode, manger()
, ne compile pas car elle est marquée abstract
mais ne se termine pas par un point-virgule (;). Les deux méthodes suivantes, nager()
et getAge()
, ne compilent pas car elles sont marquées abstract
, mais elles fournissent un bloc d’implémentation entre accolades ({}). Une déclaration de méthode abstraite doit se terminer par un point-virgule sans aucune accolade. La méthode suivante, dormir
, ne compile pas car il manque les parenthèses, (), pour les arguments de méthode. La dernière méthode, allerDansCoquille()
, ne compile pas car elle n’est pas marquée abstract
et doit donc fournir un corps entre accolades.
abstract et final Modificateurs
Que se passerait-il si vous marquiez une classe ou une méthode à la fois abstract
et final
? Si vous marquez quelque chose abstract
, vous avez l’intention que quelqu’un d’autre l’étende ou l’implémente. Mais si vous marquez quelque chose final
, vous empêchez quiconque de l’étendre ou de l’implémenter. Ces concepts sont en conflit direct l’un avec l’autre.
En raison de cette incompatibilité, Java ne permet pas à une classe ou une méthode d’être marquée à la fois abstract
et final
. Par exemple, le fragment de code suivant ne compilera pas :
public abstract final class Tortue { // NE COMPILE PAS
public abstract final void marcher(); // NE COMPILE PAS
}
Dans cet exemple, ni la déclaration de classe ni la déclaration de méthode ne compileront car elles sont marquées à la fois abstract
et final
.
abstract et private Modificateurs
Une méthode ne peut pas être marquée à la fois abstract
et private
. Cette règle a du sens si vous y réfléchissez. Comment définiriez-vous une sous-classe qui implémente une méthode requise si la méthode n’est pas héritée par la sous-classe ? La réponse est que vous ne pouvez pas, ce qui est pourquoi le compilateur se plaindra si vous essayez de faire ce qui suit :
public abstract class Baleine {
private abstract void chanter(); // NE COMPILE PAS
}
public class BaleineBosse extends Baleine {
private void chanter() {
System.out.println("La baleine à bosse chante");
}
}
Dans cet exemple, la méthode abstraite chanter()
définie dans la classe parente Baleine
n’est pas visible pour la sous-classe BaleineBosse
. Même si BaleineBosse
fournit une implémentation, elle n’est pas considérée comme une surcharge de la méthode abstraite puisque la méthode abstraite n’est pas héritée. Le compilateur reconnaît cela dans la classe parente et signale une erreur dès que private
et abstract
sont appliqués à la même méthode.
Bien qu’il ne soit pas possible de déclarer une méthode abstract
et private
, il est possible (bien que redondant) de déclarer une méthode final
et private
.
Si nous changions le modificateur d’accès de private
à protected
dans la classe parente Baleine
, le code compilerait-il ?
public abstract class Baleine {
protected abstract void chanter();
}
public class BaleineBosse extends Baleine {
private void chanter() { // NE COMPILE PAS
System.out.println("La baleine à bosse chante");
}
}
Dans cet exemple modifié, le code ne compilera toujours pas, mais pour une raison complètement différente. Si vous vous souvenez des règles pour surcharger une méthode, la sous-classe ne peut pas réduire la visibilité de la méthode parente, chanter()
. Parce que la méthode est déclarée protected
dans la classe parente, elle doit être marquée comme protected
ou public
dans la classe enfant. Même avec les méthodes abstraites, les règles pour surcharger les méthodes doivent être suivies.
abstract et static Modificateurs
Comme nous l’avons discuté plus tôt, une méthode static
ne peut qu’être masquée, pas surchargée. Elle est définie comme appartenant à la classe, pas à une instance de la classe. Si une méthode static
ne peut pas être surchargée, alors il s’ensuit qu’elle ne peut pas non plus être marquée abstract
puisqu’elle ne peut jamais être implémentée. Par exemple, la classe suivante ne compile pas :
abstract class Hippopotame {
abstract static void nager(); // NE COMPILE PAS
}
Assurez-vous de connaître quels modificateurs peuvent et ne peuvent pas être utilisés ensemble, en particulier pour les classes abstraites et les interfaces.