Maintenant que nous avons créé une classe, que pouvons-nous en faire ? L’une des plus grandes forces de Java est l’utilisation de son modèle d’héritage pour simplifier le code. Par exemple, supposons que vous ayez cinq classes, chacune étendant la classe Animal
. De plus, chaque classe définit une méthode manger()
avec une implémentation identique. Dans ce scénario, il est bien préférable de définir manger()
une seule fois dans la classe Animal
plutôt que de devoir maintenir la même méthode dans cinq classes distinctes.
L’héritage d’une classe ne donne pas seulement accès aux méthodes héritées de la classe parente, mais prépare également le terrain pour les collisions entre les méthodes définies à la fois dans la classe parente et la sous-classe. Dans cette section, nous passons en revue les règles de l’héritage des méthodes et comment Java gère ces scénarios.
Nous appelons la capacité d’un objet à prendre de nombreuses formes différentes le polymorphisme. Nous aborderons ce sujet plus en détail dans le prochain chapitre, mais pour l’instant, vous devez simplement savoir qu’un objet peut être utilisé de différentes manières, en partie en fonction de la variable de référence utilisée pour appeler l’objet.
Redéfinir une Méthode
Que se passe-t-il si une méthode avec la même signature est définie à la fois dans les classes parente et enfant ? Par exemple, vous pourriez vouloir définir une nouvelle version de la méthode et lui faire adopter un comportement différent pour cette sous-classe. La solution consiste à redéfinir la méthode dans la classe enfant. En Java, la redéfinition d’une méthode se produit lorsqu’une sous-classe déclare une nouvelle implémentation pour une méthode héritée avec la même signature et un type de retour compatible.
N’oubliez pas qu’une signature de méthode est composée du nom de la méthode et des paramètres de la méthode. Elle n’inclut pas le type de retour, les modificateurs d’accès, les spécificateurs optionnels ou les exceptions déclarées.
Lorsque vous redéfinissez une méthode, vous pouvez toujours faire référence à la version parente de la méthode en utilisant le mot-clé super
. De cette manière, les mots-clés this
et super
vous permettent de choisir entre les versions actuelle et parente d’une méthode, respectivement. Nous illustrons cela avec l’exemple suivant :
public class Marsupial {
public double getPoidsMoyen() {
return 50;
}
}
public class Kangourou extends Marsupial {
public double getPoidsMoyen() {
return super.getPoidsMoyen()+20;
}
}
public static void main(String[] args) {
System.out.println(new Marsupial().getPoidsMoyen()); // 50.0
System.out.println(new Kangourou().getPoidsMoyen()); // 70.0
}
Dans cet exemple, la classe Kangourou
redéfinit la méthode getPoidsMoyen()
mais dans le processus appelle la version parente en utilisant la référence super
.
Appels Infinis lors de la Redéfinition de Méthode
Vous pourriez vous demander si l’utilisation de super
dans l’exemple précédent était nécessaire. Par exemple, que produirait le code suivant si nous supprimions le mot-clé super
?
public double getPoidsMoyen() {
return getPoidsMoyen()+20; // StackOverflowError
}
Dans cet exemple, le compilateur n’appellerait pas la méthode Marsupial
parente ; il appellerait la méthode Kangourou
actuelle. L’application tentera de s’appeler elle-même à l’infini et produira une StackOverflowError
à l’exécution.
Pour redéfinir une méthode, vous devez suivre un certain nombre de règles. Le compilateur effectue les vérifications suivantes lorsque vous redéfinissez une méthode :
- La méthode dans la classe enfant doit avoir la même signature que la méthode dans la classe parente.
- La méthode dans la classe enfant doit être au moins aussi accessible que la méthode dans la classe parente.
- La méthode dans la classe enfant ne peut pas déclarer une exception vérifiée qui est nouvelle ou plus large que la classe de toute exception déclarée dans la méthode de la classe parente.
- Si la méthode renvoie une valeur, elle doit être du même type ou d’un sous-type de la méthode dans la classe parente, ce qu’on appelle les types de retour covariants.
Bien que ces règles puissent sembler déroutantes ou arbitraires au premier abord, elles sont nécessaires pour assurer la cohérence. Sans ces règles en place, il serait possible de créer des contradictions au sein du langage Java.
Règle n°1 : Signatures de Méthode
La première règle pour redéfinir une méthode est assez évidente. Si deux méthodes ont le même nom mais des signatures différentes, les méthodes sont surchargées, pas redéfinies. Les méthodes surchargées sont considérées comme indépendantes et ne partagent pas les mêmes propriétés polymorphiques que les méthodes redéfinies.
Nous avons abordé la surcharge d’une méthode précédemment, et c’est similaire à la redéfinition d’une méthode, car les deux impliquent la définition d’une méthode utilisant le même nom. La surcharge diffère de la redéfinition en ce que les méthodes surchargées utilisent une liste de paramètres différente. Il est important que vous compreniez cette distinction et que les méthodes redéfinies ont la même signature et beaucoup plus de règles que les méthodes surchargées.
Règle n°2 : Modificateurs d’Accès
Quel est l’objectif de la deuxième règle concernant les modificateurs d’accès ? Essayons un exemple illustratif :
public class Chameau {
public int getNombreBosses() {
return 1;
}
}
public class ChameauBactrien extends Chameau {
private int getNombreBosses() { // NE COMPILE PAS
return 2;
}
}
Dans cet exemple, ChameauBactrien
tente de redéfinir la méthode getNombreBosses()
définie dans la classe parente, mais échoue car le modificateur d’accès private
est plus restrictif que celui défini dans la version parente de la méthode. Supposons que ChameauBactrien
soit autorisé à compiler. Cette classe compilerait-elle ?
public class Cavalier {
public static void main(String[] args) {
Chameau c = new ChameauBactrien();
System.out.print(c.getNombreBosses()); // ???
}
}
La réponse est que nous ne savons pas. Le type de référence pour l’objet est Chameau
, où la méthode est déclarée public
, mais l’objet est en fait une instance de type ChameauBactrien
, où la méthode est déclarée private
. Java évite ces types de problèmes d’ambiguïté en limitant la redéfinition d’une méthode à des modificateurs d’accès qui sont aussi accessibles ou plus accessibles que la version dans la méthode héritée.
Règle n°3 : Exceptions Vérifiées
La troisième règle stipule que la redéfinition d’une méthode ne peut pas déclarer de nouvelles exceptions vérifiées ou des exceptions vérifiées plus larges que la méthode héritée. Cela est fait pour des raisons polymorphiques similaires à la limitation des modificateurs d’accès. En d’autres termes, vous pourriez vous retrouver avec un objet qui est plus restrictif que le type de référence auquel il est assigné, ce qui entraînerait une exception vérifiée qui n’est pas gérée ou déclarée. Une implication de cette règle est que les méthodes redéfinies sont libres de déclarer n’importe quel nombre de nouvelles exceptions non vérifiées.
Si vous ne savez pas ce qu’est une exception vérifiée ou non vérifiée, ne vous inquiétez pas. Pour l’instant, vous devez juste savoir que la règle s’applique uniquement aux exceptions vérifiées. Il est également utile de savoir que IOException
et FileNotFoundException
sont des exceptions vérifiées et que FileNotFoundException
est une sous-classe de IOException
.
Essayons un exemple :
public class Reptile {
protected void dormir() throws IOException {}
protected void cacher() {}
protected void sortirCoquille() throws FileNotFoundException {}
}
public class TortueGalapagos extends Reptile {
public void dormir() throws FileNotFoundException {}
public void cacher() throws FileNotFoundException {} // NE COMPILE PAS
public void sortirCoquille() throws IOException {} // NE COMPILE PAS
}
Dans cet exemple, nous avons trois méthodes redéfinies. Ces méthodes redéfinies utilisent le modificateur plus accessible public
, ce qui est autorisé selon notre deuxième règle pour les méthodes redéfinies. La première méthode redéfinie dormir()
dans TortueGalapagos
compile sans problème car l’exception déclarée est plus étroite que l’exception déclarée dans la classe parente.
La méthode redéfinie cacher()
ne compile pas car elle déclare une nouvelle exception vérifiée qui n’est pas présente dans la déclaration parente. La méthode redéfinie sortirCoquille()
ne compile pas non plus, car IOException
est une exception vérifiée plus large que FileNotFoundException
.
Règle n°4 : Types de Retour Covariants
La quatrième et dernière règle concernant la redéfinition d’une méthode est probablement la plus compliquée, car elle nécessite de connaître les relations entre les types de retour. La méthode de redéfinition doit utiliser un type de retour qui est covariant avec le type de retour de la méthode héritée.
Essayons un exemple à des fins d’illustration :
public class Rhinoceros {
protected CharSequence getNom() {
return "rhinoceros";
}
protected String getCouleur() {
return "gris, noir, ou blanc";
}
}
public class RhinocerosJava extends Rhinoceros {
public String getNom() {
return "rhinoceros de java";
}
public CharSequence getCouleur() { // NE COMPILE PAS
return "gris";
}
}
La sous-classe RhinocerosJava
tente de redéfinir deux méthodes de Rhinoceros
: getNom()
et getCouleur()
. Les deux méthodes redéfinies ont le même nom et la même signature que les méthodes héritées. Les méthodes redéfinies ont également un modificateur d’accès plus large, public
, que les méthodes héritées. N’oubliez pas qu’un modificateur d’accès plus large est acceptable dans une méthode redéfinie.
Nous avons appris que String
implémente l’interface CharSequence
, ce qui fait de String
un sous-type de CharSequence
. Par conséquent, le type de retour de getNom()
dans RhinocerosJava
est covariant avec le type de retour de getNom()
dans Rhinoceros
.
En revanche, la méthode redéfinie getCouleur()
ne compile pas car CharSequence
n’est pas un sous-type de String
. Pour le dire autrement, toutes les valeurs String
sont des valeurs CharSequence
, mais toutes les valeurs CharSequence
ne sont pas des valeurs String
. Par exemple, un StringBuilder
est un CharSequence
mais pas un String
. Vous devez savoir si le type de retour de la méthode de redéfinition est le même ou un sous-type du type de retour de la méthode héritée.
Un test simple pour la covariance est le suivant : étant donné un type de retour hérité A et un type de retour redéfini B, pouvez-vous assigner une instance de B à une variable de référence pour A sans cast ? Si oui, alors ils sont covariants. Cette règle s’applique aussi bien aux types primitifs qu’aux types d’objets. Si l’un des types de retour est void
, alors ils doivent tous les deux être void
, car rien n’est covariant avec void
sauf lui-même.
Scénario du Monde Réel
Marquer des Méthodes avec l’Annotation @Override
Une annotation est une balise de métadonnées qui fournit des informations supplémentaires sur votre code. Vous pouvez utiliser l’annotation @Override
pour indiquer au compilateur que vous essayez de redéfinir une méthode.
public class Poisson {
public void nager() {};
}
public class Requin extends Poisson {
@Override
public void nager() {};
}
Lorsqu’elle est utilisée correctement, l’annotation n’a pas d’impact sur le code. En revanche, lorsqu’elle est utilisée incorrectement, cette annotation peut vous empêcher de faire une erreur. Ce qui suit ne compile pas en raison de la présence de l’annotation @Override
:
public class Poisson {
public void nager() {};
}
public class Requin extends Poisson {
@Override
public void nager(int vitesse) {}; // NE COMPILE PAS
}
Le compilateur voit que vous essayez de redéfinir une méthode et recherche une version héritée de nager()
qui prend une valeur int
. Comme le compilateur n’en trouve pas, il signale une erreur. Bien que la connaissance des sujets avancés (comme la création d’annotations) ne soit pas nécessaire, savoir comment les utiliser correctement est important.
Redéclarer des Méthodes Privées
Que se passe-t-il si vous essayez de redéfinir une méthode private
? En Java, vous ne pouvez pas redéfinir les méthodes private
car elles ne sont pas héritées. Le fait qu’une classe enfant n’ait pas accès à la méthode parente ne signifie pas que la classe enfant ne peut pas définir sa propre version de la méthode. Cela signifie simplement, strictement parlant, que la nouvelle méthode n’est pas une version redéfinie de la méthode de la classe parente.
Java vous permet de redéclarer une nouvelle méthode dans la classe enfant avec la même signature ou une signature modifiée que la méthode dans la classe parente. Cette méthode dans la classe enfant est une méthode séparée et indépendante, sans rapport avec la méthode de la version parente, donc aucune des règles pour la redéfinition des méthodes n’est invoquée. Par exemple, ces deux déclarations compilent :
public class Scarabee {
private String getTaille() {
return "Indéfini";
}
}
public class ScarabeeRhinoceros extends Scarabee {
private int getTaille() {
return 5;
}
}
Remarquez que le type de retour diffère dans la méthode enfant, passant de String
à int
. Dans cet exemple, la méthode getTaille()
dans la classe parente est redéclarée, donc la méthode dans la classe enfant est une nouvelle méthode et non une redéfinition de la méthode dans la classe parente.
Et si la méthode getTaille()
était déclarée public
dans Scarabee
? Dans ce cas, la méthode dans ScarabeeRhinoceros
serait une redéfinition invalide. Le modificateur d’accès dans ScarabeeRhinoceros
est plus restrictif, et les types de retour ne sont pas covariants.
Masquer des Méthodes Statiques
Une méthode static
ne peut pas être redéfinie car les objets de classe n’héritent pas les uns des autres de la même manière que les objets d’instance. En revanche, ils peuvent être masqués. Une méthode masquée se produit lorsqu’une classe enfant définit une méthode static
avec le même nom et la même signature qu’une méthode static
héritée définie dans une classe parente. Le masquage de méthode est similaire mais pas exactement identique à la redéfinition de méthode. Les quatre règles précédentes pour la redéfinition d’une méthode doivent être suivies lorsqu’une méthode est masquée. De plus, une cinquième règle est ajoutée pour masquer une méthode :
La méthode définie dans la classe enfant doit être marquée comme static
si elle est marquée comme static
dans une classe parente.
Dit simplement, il s’agit de masquage de méthode si les deux méthodes sont marquées static
et de redéfinition de méthode si elles ne sont pas marquées static
. Si l’une est marquée static
et l’autre ne l’est pas, la classe ne compilera pas.
Examinons quelques exemples de la nouvelle règle :
public class Ours {
public static void manger() {
System.out.println("L'ours mange");
}
}
public class Panda extends Ours {
public static void manger() {
System.out.println("Le panda mâche");
}
public static void main(String[] args) {
manger();
}
}
Dans cet exemple, le code compile et s’exécute. La méthode manger()
dans la classe Panda
masque la méthode manger()
dans la classe Ours
, imprimant “Le panda mâche” à l’exécution. Comme elles sont toutes deux marquées comme static
, ce n’est pas considéré comme une méthode redéfinie. Cela dit, il y a toujours une certaine forme d’héritage. Si vous supprimez la déclaration manger()
dans la classe Panda
, alors le programme imprimera “L’ours mange” à la place.
Voyez si vous pouvez comprendre pourquoi chacune des déclarations de méthode dans la classe OursSoleil
ne compile pas :
public class Ours {
public static void eternuer() {
System.out.println("L'ours éternue");
}
public void hiberner() {
System.out.println("L'ours hiberne");
}
public static void rire() {
System.out.println("L'ours rit");
}
}
public class OursSoleil extends Ours {
public void eternuer() { // NE COMPILE PAS
System.out.println("L'ours soleil éternue doucement");
}
public static void hiberner() { // NE COMPILE PAS
System.out.println("L'ours soleil va dormir");
}
protected static void rire() { // NE COMPILE PAS
System.out.println("L'ours soleil rit");
}
}
Dans cet exemple, eternuer()
est marqué static
dans la classe parente mais pas dans la classe enfant. Le compilateur détecte que vous essayez de redéfinir en utilisant une méthode d’instance. Cependant, eternuer()
est une méthode static
qui devrait être masquée, ce qui amène le compilateur à générer une erreur. La deuxième méthode, hiberner()
, ne compile pas pour la raison opposée. La méthode est marquée static
dans la classe enfant mais pas dans la classe parente.
Enfin, la méthode rire()
ne compile pas. Même si les deux versions de la méthode sont marquées static
, la version dans OursSoleil
a un modificateur d’accès plus restrictif que celle qu’elle hérite, et elle enfreint la deuxième règle pour la redéfinition des méthodes. N’oubliez pas que les quatre règles pour la redéfinition des méthodes doivent être suivies lors du masquage des méthodes static
.
Masquer des Variables
Comme vous l’avez vu avec la redéfinition de méthode, il y a beaucoup de règles lorsque deux méthodes ont la même signature et sont définies à la fois dans les classes parente et enfant. Heureusement, les règles pour les variables portant le même nom dans les classes parente et enfant sont beaucoup plus simples. En fait, Java ne permet pas aux variables d’être redéfinies. Les variables peuvent cependant être masquées.
Une variable masquée se produit lorsqu’une classe enfant définit une variable avec le même nom qu’une variable héritée définie dans la classe parente. Cela crée deux copies distinctes de la variable au sein d’une instance de la classe enfant : une instance définie dans la classe parente et une définie dans la classe enfant.
Comme pour le masquage d’une méthode static
, vous ne pouvez pas redéfinir une variable ; vous ne pouvez que la masquer. Examinons une variable masquée. Que pensez-vous que l’application suivante imprime ?
class Carnivore {
protected boolean aPelage = false;
}
public class Suricate extends Carnivore {
protected boolean aPelage = true;
public static void main(String[] args) {
Suricate s = new Suricate();
Carnivore c = s;
System.out.println(s.aPelage); // true
System.out.println(c.aPelage); // false
}
}
Confus par la sortie ? Ces deux classes définissent une variable aPelage
, mais avec des valeurs différentes. Même si un seul objet est créé par la méthode main()
, les deux variables existent indépendamment l’une de l’autre. La sortie change en fonction de la variable de référence utilisée.
Si vous n’avez pas compris le dernier exemple, ne vous inquiétez pas. Nous abordons le polymorphisme plus en détail dans le prochain chapitre. Pour l’instant, vous devez juste savoir que la redéfinition d’une méthode remplace la méthode parente sur toutes les variables de référence (autres que super
), tandis que le masquage d’une méthode ou d’une variable ne remplace le membre que si un type de référence enfant est utilisé.
Écrire des Méthodes final
Nous concluons notre discussion sur l’héritage des méthodes avec une règle assez explicite : les méthodes final
ne peuvent pas être redéfinies. En marquant une méthode comme final
, vous interdisez à une classe enfant de remplacer cette méthode. Cette règle est en place à la fois lorsque vous redéfinissez une méthode et lorsque vous masquez une méthode. En d’autres termes, vous ne pouvez pas masquer une méthode static
dans une classe enfant si elle est marquée final
dans la classe parente.
Examinons un exemple :
public class Oiseau {
public final boolean aPlumes() {
return true;
}
public final static void volerAuLoin() {}
}
public class Manchot extends Oiseau {
public final boolean aPlumes() { // NE COMPILE PAS
return false;
}
public final static void volerAuLoin() {} // NE COMPILE PAS
}
Dans cet exemple, la méthode d’instance aPlumes()
est marquée comme final
dans la classe parente Oiseau
, donc la classe enfant Manchot
ne peut pas redéfinir la méthode parente, ce qui entraîne une erreur de compilation. La méthode static
volerAuLoin()
est également marquée final
, donc elle ne peut pas être masquée dans la sous-classe. Dans cet exemple, que la méthode enfant utilise ou non le mot-clé final
est sans importance : le code ne compilera pas de toute façon.
Cette règle s’applique uniquement aux méthodes héritées. Par exemple, si les deux méthodes étaient marquées private
dans la classe parente Oiseau
, alors la classe Manchot
, telle que définie, compilerait. Dans ce cas, les méthodes private
seraient redéclarées, et non redéfinies ou masquées.