Aller au contenu principal

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);
}
attention

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 abstraiteInterface
Instanciable ?NonNon
Méthodes concrètesOuiOui (avec default)
Méthodes abstraitesOuiOui (toutes par défaut)
AttributsOuiConstantes seulement
Héritage multipleNonOui (une classe peut implémenter plusieurs interfaces)
Mot-cléextendsimplements
info

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);
}
}
info

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".

Test de mémorisation/compréhension


Quel mot-clé rend une classe non-instanciable en Java ?


Qu'est-ce qu'une méthode abstraite ?


Peut-on instancier une classe abstraite directement avec 'new' ?


Une classe abstraite peut-elle contenir des méthodes avec un corps (des méthodes concrètes) ?


Que se passe-t-il si une classe hérite d'une classe abstraite sans implémenter toutes ses méthodes abstraites ?


Quelle annotation doit accompagner l'implémentation d'une méthode abstraite dans une sous-classe ?


TP pour réfléchir et résoudre des problèmes

Vous allez rendre la classe Personnage abstraite et forcer chaque sous-classe à implémenter sa propre version de attaquer().

Étape 1 — Rendre Personnage abstraite

Modifiez la déclaration de Personnage pour la rendre abstraite et ajoutez la méthode abstraite attaquer().


Bonne pratique - Déclarer abstract dès que possible

Rendre une classe abstraite dès qu'elle ne doit pas être instanciée directement est une bonne pratique. Cela rend l'intention du code explicite : les autres développeurs comprennent immédiatement que cette classe est un modèle, pas un objet concret.

Étape 2 — Implémenter attaquer() dans Guerrier

Ajoutez l'implémentation de attaquer() dans Guerrier avec l'annotation @Override.


Bonne pratique - @Override sur chaque implémentation de méthode abstraite

Quand vous implémentez une méthode abstraite, l'annotation @Override est votre filet de sécurité. Si la signature de la méthode parente change et que vous oubliez de mettre à jour la sous-classe, le compilateur vous avertira immédiatement grâce à @Override.

Étape 3 — Implémenter attaquer() dans Mage

Ajoutez attaquer() dans Mage : si le mana est suffisant, l'attaque coûte 15 de mana et inflige force * 3 dégâts ; sinon, attaque basique de force dégâts.


Bonne pratique - Comportements spécifiques dans les sous-classes

La méthode abstraite attaquer() force chaque sous-classe à définir son propre comportement. C'est le principe même de l'abstraction : définir le "quoi" (tous les personnages attaquent) sans imposer le "comment" (chacun attaque à sa façon).

📌 Une solution