Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 25

Classes abstraites : créer des bases non-instanciables

Recommended resources

Bonjour ! Dans la leçon précédente, nous avons vu comment les interfaces définissent des "contrats" de comportement, permettant à des classes très différentes de partager une même capacité, comme la capacité de voler() pour un Avion et un Drone. C'est un outil puissant pour le découplage.

Aujourd'hui, nous allons explorer un concept complémentaire mais distinct : les classes abstraites. L'objectif de cette leçon est d'apprendre à créer des classes de base qui ne peuvent pas être instanciées elles-mêmes, mais qui servent de modèle commun à une famille de sous-classes. C'est un mécanisme à mi-chemin entre une classe concrète et une interface, idéal pour factoriser du code tout en forçant les classes filles à implémenter des comportements spécifiques.

Cette compétence est essentielle pour concevoir des hiérarchies d'objets logiques et robustes, une pratique courante en développement backend.

1. Qu'est-ce qu'une classe abstraite ?

Une classe abstraite est une classe que vous ne pouvez pas utiliser pour créer un objet directement avec new. Son unique but est de servir de classe de base (classe mère) pour d'autres classes.

Elle peut contenir :

  • Des attributs et des constructeurs, comme une classe normale.
  • Des méthodes concrètes (avec une implémentation) qui seront partagées par toutes ses sous-classes.
  • Des méthodes abstraites, qui sont des déclarations de méthodes sans corps, que les sous-classes seront obligées d'implémenter.

La vidéo suivante introduit très clairement ces notions.

Apprendre Java #3 Classe Abstraite

La chaîne JirAWS propose une introduction concise et efficace aux classes et méthodes abstraites.

Regardez ce court segment pour comprendre les définitions fondamentales. Définitions : Portez attention à la syntaxe d'une méthode abstraite (mot-clé abstract, pas de corps {}) et à la règle qui en découle : si une classe contient ne serait-ce qu'une seule méthode abstraite, la classe elle-même doit être déclarée abstraite. Non-instanciation : Retenez bien la contrainte principale : new MaClasseAbstraite() est interdit par le compilateur.


2. Pourquoi créer une classe non-instanciable ?

À première vue, une classe qu'on ne peut pas instancier peut sembler inutile. En réalité, c'est un outil de conception puissant. On l'utilise quand on peut définir un concept général, mais que l'implémentation de certains détails n'a de sens que pour des spécialisations de ce concept.

Prenons un exemple. Imaginez que vous modélisez des figures géométriques. Le concept de "Figure" (Shape) est utile : toutes les figures ont un centre, une couleur, etc. On peut donc créer une classe Shape pour factoriser ce code. Cependant, que signifierait calculer l'aire d'une "Figure" générique ? Cela n'a pas de sens. Le calcul dépend de la nature de la figure : côté * côté pour un carré, π * r² pour un cercle.

Ce diagramme UML illustre bien le concept. La classe `ObjetGraphique` est abstraite (nom en italique) et définit une méthode abstraite `seDessiner()`. Les classes concrètes comme `Cercle` et `Rectangle` héritent de `ObjetGraphique` et fournissent leur propre implémentation de `seDessiner()`.

Une classe abstraite permet de résoudre ce problème :

  1. On déclare la classe Shape comme abstract. Cela empêche quiconque de créer une instance vague de Shape.
  2. On y place une méthode public abstract double calculerAire();. Cela force toute classe concrète qui hérite de Shape (comme Cercle ou Carre) à fournir sa propre formule de calcul d'aire.

La classe abstraite agit comme un "contrat partiel" : elle fournit du code commun et impose une structure aux enfants.

Méthodes abstraites, classes abstraites et interfaces

L'article du site Koor.fr détaille parfaitement ce raisonnement avec l'exemple des figures géométriques.

Lisez les deux premières sections : Faire usage de l'abstraction : Concentrez-vous sur l'explication de pourquoi la méthode area() (calculer l'aire) ne peut pas être implémentée dans la classe Shape et doit donc être abstraite. Observez l'utilisation du mot-clé abstract sur la classe et la méthode. Une classe abstraite ne peut pas être instanciée ! : Cette section confirme la règle fondamentale et explique sa logique : on ne peut pas créer un objet incomplet. Elle introduit aussi une conséquence importante : une classe fille qui n'implémente pas toutes les méthodes abstraites héritées est elle-même considérée comme abstraite.


3. Implémentation en Java

Voyons maintenant comment cela se traduit en code. Nous allons construire la hiérarchie Shape -> Circle / Square.

Étape 1 : La classe de base abstraite
On définit la classe Shape avec le mot-clé abstract et on y déclare la méthode area() comme abstraite.

public abstract class Shape {
    // Attributs et constructeurs...

    // Méthode concrète, partagée par toutes les sous-classes
    public String getInfos() {
        return "Ceci est une figure géométrique.";
    }

    // Méthode abstraite, SANS corps, que les sous-classes DOIVENT implémenter
    public abstract double area();
}

Étape 2 : Les classes filles concrètes
Chaque classe fille doit extends Shape et fournir une implémentation concrète pour area(). Si elle ne le fait pas, le compilateur générera une erreur.

Votre IDE vous signalera immédiatement une erreur si votre classe concrète n'implémente pas toutes les méthodes abstraites héritées, vous proposant de les ajouter automatiquement.

Voici l'implémentation pour la classe Circle :

public class Circle extends Shape {
    private double radius;

    // Constructeur...
    
    // Implémentation OBLIGATOIRE de la méthode abstraite
    @Override
    public double area() {
        return Math.PI * this.radius * this.radius;
    }
}

Ce passage de l'article de Koor.fr montre le code complet et comment utiliser cette hiérarchie.

Méthodes abstraites, classes abstraites et interfaces

Cet article continue de guider la mise en place de notre exemple.

Lisez la section Implémentation de méthode abstraite. Vous y verrez le code pour les classes Circle et Square, chacune avec sa propre version de la méthode area(). Notez l'utilisation de l'annotation @Override, qui est une bonne pratique. Lisez ensuite la section Utilisation de notre hiérarchie de classes. Elle démontre comment le polymorphisme fonctionne parfaitement avec les classes abstraites. On peut créer une ArrayList<Shape> et y stocker des Circle et des Square, puis appeler shape.area() sur chaque élément, et Java exécutera la bonne méthode en fonction du type réel de l'objet.


4. Le rôle de la relation "est un" (is-a)

Une question clé en conception est : quand utiliser une classe abstraite ? La réponse est liée à la relation "est un".

Utilisez une classe abstraite lorsque vos classes partagent une identité commune forte. Un Cercle est une Figure. Un Chien est un Animal. Cette relation justifie l'héritage. L'abstraction intervient lorsque cette identité commune comporte des aspects qui ne peuvent être définis qu'au niveau des spécialisations.

JAVA (Intermédiaire) - 34 - Differences entre classes abstraites et Interfaces

La chaîne "LES TEACHERS DU NET" explique très bien ce point dans le contexte de l'héritage.

Regardez ce court extrait sur la relation forte. Le parallèle avec un héritage dans la vie réelle est une bonne image pour se souvenir que l'héritage de classe (et donc les classes abstraites) est réservé aux relations étroites entre les concepts.

Une classe peut aussi être déclarée abstract même si elle ne contient aucune méthode abstraite. Cela peut être une décision de conception pour empêcher son instanciation directe, forçant les développeurs à n'utiliser que ses sous-classes plus spécifiques.


5. Exercice pratique : Gestion des employés

Mettez en pratique ces concepts en modélisant une hiérarchie simple d'employés.

Objectif : Créer un système pour calculer les salaires de différents types d'employés.

  1. Créez une classe abstraite Employe avec :

    • Un attribut protected String nom.
    • Un constructeur qui initialise le nom.
    • Une méthode concrète public String getNom() qui retourne le nom.
    • Une méthode abstraite public abstract double calculerSalaire().
  2. Créez une classe EmployeFixe qui hérite de Employe :

    • Elle aura un attribut private double salaireMensuel.
    • Son constructeur prendra le nom et le salaire mensuel.
    • Implémentez la méthode calculerSalaire() pour qu'elle retourne simplement salaireMensuel.
  3. Créez une classe EmployeHoraire qui hérite de Employe :

    • Elle aura deux attributs privés : double tauxHoraire et int heuresTravaillees.
    • Son constructeur prendra le nom, le taux horaire et les heures travaillées.
    • Implémentez calculerSalaire() pour qu'elle retourne tauxHoraire * heuresTravaillees.
  4. Dans votre méthode main :

    • Créez une ArrayList de type Employe : List<Employe> employes = new ArrayList<>();.
    • Ajoutez à la liste une instance de EmployeFixe et une instance de EmployeHoraire.
    • Parcourez la liste avec une boucle et, pour chaque employé, affichez son nom et son salaire calculé en utilisant les méthodes getNom() et calculerSalaire().

Code de départ pour main :

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Employe> employes = new ArrayList<>();
        
        employes.add(new EmployeFixe("Alice", 3000.0));
        employes.add(new EmployeHoraire("Bob", 20.0, 150));
        
        System.out.println("--- Salaires du mois ---");
        for (Employe emp : employes) {
            System.out.println("Employé: " + emp.getNom() + ", Salaire: " + emp.calculerSalaire() + " €");
        }
    }
}

Résultat attendu :

--- Salaires du mois ---
Employé: Alice, Salaire: 3000.0 €
Employé: Bob, Salaire: 3000.0 €

Conclusion

Aujourd'hui, vous avez ajouté un outil fondamental de la POO à votre arsenal. Les classes abstraites sont la solution parfaite lorsque vous voulez créer une famille de classes partageant du code et une structure, tout en laissant à chaque membre de la famille la liberté (et l'obligation) d'implémenter ses propres spécificités.

Les points clés à retenir :

  • Une classe déclarée avec abstract ne peut pas être instanciée.
  • Elle sert de modèle pour ses sous-classes.
  • Elle peut mélanger des méthodes concrètes (code partagé) et des méthodes abstraites (contrat à implémenter).
  • Toute sous-classe non-abstraite doit implémenter toutes les méthodes abstraites héritées.
  • Les classes abstraites sont idéales pour modéliser des hiérarchies avec une forte relation "est un".

Dans la prochaine leçon, nous ferons le bilan final en comparant en détail les classes abstraites et les interfaces. Vous apprendrez à prendre la décision architecturale cruciale : dans quelle situation utiliser l'une ou l'autre ?