L'héritage
Notions théoriques
Qu'est-ce que l'héritage ?
L'héritage est un mécanisme fondamental de la POO qui permet à une classe d'hériter des attributs et des méthodes d'une autre classe. On parle de :
- classe parente (ou superclasse) : la classe dont on hérite
- classe enfant (ou sous-classe) : la classe qui hérite
En Java, le mot-clé extends établit la relation d'héritage.
L'héritage modélise une relation "est-un" : un Guerrier est un Personnage. Un Mage est un Personnage. Cette relation doit être vraie dans les deux sens pour que l'héritage soit justifié.
Syntaxe de l'héritage
// Classe parente
public class Personnage {
String nom;
int pointsDeVie;
int force;
void afficherStatistiques() {
System.out.println("=== " + nom + " ===");
System.out.println("PV : " + pointsDeVie);
System.out.println("Force : " + force);
}
}
// Classe enfant qui hérite de Personnage
public class Guerrier extends Personnage {
String typeArme;
void coupEpee(Personnage cible) {
System.out.println(nom + " frappe " + cible.nom + " avec son " + typeArme + " !");
cible.pointsDeVie -= force * 2;
}
}
Le Guerrier hérite automatiquement de nom, pointsDeVie, force et afficherStatistiques() sans avoir à les redéclarer.
Le constructeur et super()
Quand une classe enfant définit un constructeur, elle doit appeler le constructeur de la classe parente avec super(...). Cet appel doit être la première instruction du constructeur enfant.
public class Personnage {
String nom;
int pointsDeVie;
int force;
public Personnage(String nom, int pointsDeVie, int force) {
this.nom = nom;
this.pointsDeVie = pointsDeVie;
this.force = force;
}
}
public class Guerrier extends Personnage {
String typeArme;
public Guerrier(String nom, int pointsDeVie, int force, String typeArme) {
super(nom, pointsDeVie, force); // DOIT être la 1ère ligne
this.typeArme = typeArme;
}
}
Si vous oubliez l'appel à super() et que la classe parente n'a pas de constructeur sans argument, Java génère une erreur de compilation. L'appel à super() doit toujours être la première ligne du constructeur enfant.
Redéfinir une méthode avec @Override
Une classe enfant peut redéfinir (ou surcharger) une méthode héritée pour lui donner un nouveau comportement. L'annotation @Override est obligatoire dans ce cours : elle signale au compilateur que vous redéfinissez intentionnellement une méthode parente.
public class Mage extends Personnage {
int mana;
public Mage(String nom, int pointsDeVie, int force, int mana) {
super(nom, pointsDeVie, force);
this.mana = mana;
}
// Redéfinition de la méthode parente
@Override
void afficherStatistiques() {
super.afficherStatistiques(); // appelle la version parente
System.out.println("Mana : " + mana);
}
}
super.methode() permet d'appeler la version de la méthode définie dans la classe parente.
Appeler une méthode parente avec super.methode()
super.methode() est utile quand on veut enrichir le comportement de la méthode parente plutôt que le remplacer complètement.
@Override
void afficherStatistiques() {
super.afficherStatistiques(); // affiche nom, PV, force
System.out.println("Mana : " + mana); // ajoute la ligne mana
}
Limites de l'héritage en Java
Java ne supporte que l'héritage simple : une classe enfant ne peut hériter que d'une seule classe parente. Contrairement à C++, on ne peut pas écrire class Guerrier extends Personnage, Combattant. Pour simuler l'héritage multiple, Java propose les interfaces (séance suivante) : une classe peut implémenter autant d'interfaces que nécessaire, et une interface peut elle-même hériter de plusieurs interfaces avec extends.
Interdire l'héritage avec final
Le mot-clé final placé devant une classe empêche toute classe d'en hériter.
public final class Archer extends Personnage {
// Personne ne peut créer une sous-classe d'Archer
}
Exemple pratique
// Fichier : Personnage.java
public class Personnage {
String nom;
int pointsDeVie;
int force;
public Personnage(String nom, int pointsDeVie, int force) {
this.nom = nom;
this.pointsDeVie = pointsDeVie;
this.force = force;
}
void afficherStatistiques() {
System.out.println("=== " + nom + " ===");
System.out.println("PV : " + pointsDeVie);
System.out.println("Force : " + force);
}
}
// Fichier : Guerrier.java
public class Guerrier extends Personnage {
String typeArme;
public Guerrier(String nom, int pointsDeVie, int force, String typeArme) {
super(nom, pointsDeVie, force); // appel obligatoire, 1ère ligne
this.typeArme = typeArme;
}
void coupEpee(Personnage cible) {
int degats = force * 2;
System.out.println(nom + " frappe " + cible.nom + " avec son " + typeArme + " ! Dégâts : " + degats);
cible.pointsDeVie -= degats;
}
}
// Fichier : Mage.java
public class Mage extends Personnage {
int mana;
public Mage(String nom, int pointsDeVie, int force, int mana) {
super(nom, pointsDeVie, force);
this.mana = mana;
}
@Override
void afficherStatistiques() {
super.afficherStatistiques();
System.out.println("Mana : " + mana);
}
void bouleDeFeue(Personnage cible) {
if (mana >= 20) {
int degats = force * 3;
System.out.println(nom + " lance une boule de feu sur " + cible.nom + " ! Dégâts : " + degats);
cible.pointsDeVie -= degats;
mana -= 20;
} else {
System.out.println(nom + " n'a pas assez de mana !");
}
}
}
// Fichier : Main.java
public class Main {
public static void main(String[] args) {
Guerrier guerrier = new Guerrier("Thor", 120, 18, "épée runique");
Mage mage = new Mage("Merlin", 80, 12, 100);
guerrier.afficherStatistiques(); // méthode héritée de Personnage
mage.afficherStatistiques(); // méthode redéfinie dans Mage
mage.bouleDeFeue(guerrier);
guerrier.coupEpee(mage);
}
}
guerrier.afficherStatistiques() appelle la version de Personnage. mage.afficherStatistiques() appelle la version redéfinie dans Mage, qui elle-même appelle super.afficherStatistiques() pour réutiliser le code commun.
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Vous allez créer la hiérarchie de classes du jeu RPG : Guerrier et Mage héritant de Personnage, chacun avec sa propre attaque spécifique.
Étape 1 — Ajouter un constructeur à Personnage
Modifiez la classe Personnage pour lui ajouter un constructeur prenant nom, pointsDeVie et force en paramètres.
Un constructeur avec paramètres garantit qu'un objet est toujours créé dans un état valide. Sans constructeur, new Personnage() crée un personnage avec un nom null et 0 PV, ce qui peut provoquer des erreurs difficiles à détecter.
Étape 2 — Créer la classe Guerrier
Créez Guerrier.java avec un attribut typeArme et un constructeur qui appelle super().
L'appel à super() doit toujours être la première instruction du constructeur enfant. C'est une règle du compilateur Java, pas une convention : si vous placez une autre instruction avant, le code ne compilera pas.
Étape 3 — Créer la classe Mage avec redéfinition
Créez Mage.java avec un attribut mana, redéfinissez afficherStatistiques() avec @Override, et ajoutez la méthode bouleDeFeue().
L'annotation @Override n'est pas obligatoire pour que la redéfinition fonctionne, mais elle protège contre les erreurs de frappe. Si vous écrivez afficherStatistique() (sans 's'), Java créera une nouvelle méthode au lieu de redéfinir l'existante. Avec @Override, le compilateur détecte l'erreur immédiatement.