L'abstraction
Notions théoriques
Pourquoi l'abstraction ?
Dans notre jeu, la classe Personnage représente un concept général. Mais un "personnage" en soi n'existe pas : il n'y a que des Guerriers, des Mages, des Archers. Créer un new Personnage() n'a donc pas de sens dans notre modèle.
L'abstraction permet de définir un modèle commun sans pouvoir l'instancier directement. On force ainsi chaque sous-classe à être concrète et complète.
La classe abstraite
Le mot-clé abstract placé avant class rend une classe abstraite. Une classe abstraite :
- ne peut pas être instanciée (
new Personnage()→ erreur de compilation) - peut contenir des méthodes normales (avec corps)
- peut contenir des méthodes abstraites (sans corps)
public abstract class Personnage {
String nom;
int pointsDeVie;
int force;
// Méthode concrète (avec corps) : héritée telle quelle
void afficherStatistiques() {
System.out.println("=== " + nom + " ===");
System.out.println("PV : " + pointsDeVie);
}
// Méthode abstraite (sans corps) : DOIT être implémentée par les sous-classes
abstract void attaquer(Personnage cible);
}
Dès qu'une classe contient une méthode abstract, la classe elle-même doit être déclarée abstract. Sinon, le compilateur refuse.
La méthode abstraite
Une méthode abstraite se déclare avec le mot-clé abstract et n'a pas de corps (pas d'accolades {}). Elle se termine par un ;.
abstract void attaquer(Personnage cible);
Elle agit comme un contrat : toute classe concrète qui hérite de Personnage doit implémenter attaquer(), sinon elle aussi doit être déclarée abstract.
Implémenter une méthode abstraite
La classe enfant concrète doit implémenter toutes les méthodes abstraites de la classe parente. L'annotation @Override est obligatoire.
public class Guerrier extends Personnage {
String typeArme;
public Guerrier(String nom, int pointsDeVie, int force, String typeArme) {
super(nom, pointsDeVie, force);
this.typeArme = typeArme;
}
@Override
public void attaquer(Personnage cible) {
int degats = force * 2;
System.out.println(nom + " frappe " + cible.nom + " avec son " + typeArme + " ! Dégâts : " + degats);
cible.pointsDeVie -= degats;
}
}
Classe abstraite vs Interface
| Classe abstraite | Interface | |
|---|---|---|
| Instanciable ? | Non | Non |
| Méthodes concrètes | Oui | Oui (avec default) |
| Méthodes abstraites | Oui | Oui (toutes par défaut) |
| Attributs | Oui | Constantes seulement |
| Héritage multiple | Non | Oui (une classe peut implémenter plusieurs interfaces) |
| Mot-clé | extends | implements |
Choisissez une classe abstraite quand les sous-classes partagent du code commun et un état interne. Choisissez une interface quand vous définissez un contrat de comportement que plusieurs classes non liées doivent respecter.
Exemple pratique
// Fichier : Personnage.java
public abstract 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;
}
// Méthode concrète : comportement commun à tous les personnages
void afficherStatistiques() {
System.out.println("=== " + nom + " ===");
System.out.println("PV : " + pointsDeVie);
System.out.println("Force : " + force);
}
boolean estVivant() {
return pointsDeVie > 0;
}
// Méthode abstraite : chaque personnage attaque différemment
public abstract void attaquer(Personnage cible);
}
// Fichier : Guerrier.java
public class Guerrier extends Personnage {
String typeArme;
public Guerrier(String nom, int pointsDeVie, int force, String typeArme) {
super(nom, pointsDeVie, force);
this.typeArme = typeArme;
}
@Override
public void attaquer(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
public void attaquer(Personnage cible) {
if (mana >= 15) {
int degats = force * 3;
System.out.println(nom + " lance un sort sur " + cible.nom + " ! Dégâts magiques : " + degats);
cible.pointsDeVie -= degats;
mana -= 15;
} else {
int degats = force;
System.out.println(nom + " frappe faiblement " + cible.nom + " (plus de mana). Dégâts : " + degats);
cible.pointsDeVie -= degats;
}
}
}
// Fichier : Main.java
public class Main {
public static void main(String[] args) {
// Personnage p = new Personnage("X", 100, 10); // ERREUR : classe abstraite !
Guerrier guerrier = new Guerrier("Thor", 120, 18, "épée runique");
Mage mage = new Mage("Merlin", 80, 12, 60);
guerrier.afficherStatistiques();
mage.afficherStatistiques();
// Les deux utilisent la même méthode attaquer(), mais avec des comportements différents
guerrier.attaquer(mage);
mage.attaquer(guerrier);
}
}
Le commentaire // Personnage p = new Personnage(...); illustre ce qui est impossible avec une classe abstraite. IntelliJ soulignera cette ligne en rouge avec le message "Personnage is abstract; cannot be instantiated".