1. Classes et méthodes abstraites
Parfois, dans la vie (et en programmation), on a envie de dire : « Je ne sais pas exactement comment c’est fait, mais je sais que ça doit exister ! ». Par exemple, tous les animaux doivent savoir émettre un son, mais lequel — cela dépend de l’animal concret. Pour ce genre de cas, Java propose des classes abstraites et des méthodes abstraites.
Une classe abstraite est une classe qui ne peut pas être instanciée directement (on ne peut pas écrire new Animal() si Animal est abstraite), mais dont on peut hériter. Une telle classe peut contenir à la fois des méthodes ordinaires (implémentées) et des méthodes abstraites — c’est‑à‑dire déclarées mais non implémentées.
Une méthode abstraite est une méthode sans corps. Elle est déclarée à l’aide du mot-clé abstract et doit obligatoirement être implémentée dans les sous-classes (sauf si la sous-classe elle‑même est abstraite).
Exemple concret
Supposons que nous ayons une application pour un zoo. Nous voulons que tous les animaux aient une méthode makeSound(), mais nous ne savons pas quel son exact ils émettent. Nous créons alors une classe abstraite :
public abstract class Animal {
public abstract void makeSound(); // Méthode abstraite
}
Et les animaux concrets implémentent cette méthode chacun à leur manière :
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Ouaf-ouaf!");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Miaou!");
}
}
Maintenant, si quelqu’un essaie de créer new Animal(), le compilateur dira aussitôt : « Désolé, mais les animaux abstraits n’existent pas dans la nature ! ». C’est utile : vous garantissez que seuls des animaux concrets avec un comportement concret existeront dans le programme.
2. Polymorphisme via l’abstraction
Abstraction signifie mettre en avant une interface commune pour un groupe d’objets. La classe abstraite définit justement cette interface commune : elle indique quelles méthodes doivent être obligatoirement implémentées par toutes les sous-classes.
Le polymorphisme et l’abstraction fonctionnent de pair : la classe abstraite garantit que tous les héritiers possèdent les méthodes nécessaires, et le polymorphisme permet d’appeler ces méthodes via une référence de type de base.
Exemple : construisons un zoo
Assemblons un petit zoo. Nous avons une classe abstraite Animal et plusieurs de ses héritiers :
public abstract class Animal {
public abstract void makeSound();
}
public class Cow extends Animal {
@Override
public void makeSound() {
System.out.println("Meuh!");
}
}
public class Duck extends Animal {
@Override
public void makeSound() {
System.out.println("Coin-coin!");
}
}
Nous pouvons maintenant créer un tableau d’animaux :
Animal[] zoo = {
new Dog(),
new Cat(),
new Cow(),
new Duck()
};
for (Animal animal : zoo) {
animal.makeSound(); // Pour chaque animal, la bonne méthode sera appelée
}
Chaque objet du tableau est un animal concret, mais pour le code c’est simplement un Animal. Grâce au polymorphisme et à l’abstraction, nous pouvons être sûrs que chaque objet possède la méthode makeSound() et qu’elle fonctionnera correctement.
3. Utiliser les classes abstraites pour le polymorphisme
Voyons un exemple plus pratique. Imaginons que nous développions une application de gestion des employés d’une entreprise. Nous avons différents types d’employés : managers, développeurs, testeurs. Tous ont une méthode commune work(), mais ils l’exécutent chacun à leur façon.
Classe abstraite Employee
public abstract class Employee {
protected String name;
public Employee(String name) {
this.name = name;
}
public abstract void work();
}
Sous-classes concrètes
public class Manager extends Employee {
public Manager(String name) {
super(name);
}
@Override
public void work() {
System.out.println(name + " dirige l'équipe.");
}
}
public class Developer extends Employee {
public Developer(String name) {
super(name);
}
@Override
public void work() {
System.out.println(name + " écrit du code.");
}
}
public class Tester extends Employee {
public Tester(String name) {
super(name);
}
@Override
public void work() {
System.out.println(name + " teste l'application.");
}
}
Utilisation du polymorphisme
Nous pouvons maintenant créer un tableau d’employés et appeler pour chacun la méthode work() :
Employee[] employees = {
new Manager("Anna"),
new Developer("Ivan"),
new Tester("Maria")
};
for (Employee e : employees) {
e.work();
}
Résultat :
Anna dirige l'équipe.
Ivan écrit du code.
Maria teste l'application.
Remarquez que nous ne savons pas (et ne voulons pas savoir) dans la boucle de quel type précis est chaque employé. Nous appelons simplement work(), et chaque objet fait son travail.
4. Nuances utiles
Garantie d’implémentation des méthodes
Une classe abstraite force tous ses héritiers à implémenter les méthodes requises. Si vous oubliez d’implémenter une méthode abstraite dans une sous-classe, le compilateur vous rappellera à l’ordre : « Tu dois le faire ! »
Interface universelle
Un code qui travaille avec un tableau ou une liste d’un type abstrait (Employee[], List<Animal>) peut être totalement générique. Vous pouvez ajouter de nouvelles sous-classes — et le code principal n’aura pas à changer.
Protection contre les objets « douteux »
Puisqu’une classe abstraite ne peut pas être instanciée directement, personne ne pourra créer par inadvertance un objet d’un type « incompris » qui n’implémente pas les méthodes requises.
Théorie et syntaxe : comment déclarer une classe et une méthode abstraites
- Une classe abstraite se déclare avec le mot-clé abstract avant class.
- Une méthode abstraite se déclare avec le mot-clé abstract et n’a pas de corps (uniquement un point-virgule).
- Une classe qui contient au moins une méthode abstraite doit elle‑même être abstraite.
- Toute classe qui hérite d’une classe abstraite doit implémenter toutes ses méthodes abstraites, ou bien être abstraite elle‑même.
Schéma
public abstract class Animal {
public abstract void makeSound();
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Miaou!");
}
}
5. Erreurs typiques lors de l’utilisation des classes abstraites
Erreur n° 1 : tentative de créer un objet d’une classe abstraite.
Du code comme new Animal() ne se compilera pas. Les classes abstraites — c’est comme un manuel de montage de meuble sans les pièces : tant qu’une sous-classe concrète n’existe pas, on ne peut pas assembler d’objet.
Erreur n° 2 : oubli d’implémenter une méthode abstraite dans une sous-classe.
Si vous avez déclaré une méthode abstraite mais ne l’avez pas implémentée dans l’héritier (et que la classe n’est pas abstraite non plus), le compilateur s’en offensera et signalera une erreur.
Erreur n° 3 : oubli des modificateurs d’accès.
Une méthode redéfinie ne peut pas avoir un modificateur d’accès plus strict que celui de la classe de base. Par exemple, si la méthode abstraite était public, alors l’implémentation doit aussi être public (et non protected ou private).
Erreur n° 4 : tentative d’utiliser une méthode abstraite avec un corps.
Une méthode abstract ne peut pas avoir de corps, sinon le compilateur lèvera les yeux au ciel et dira : « Décide-toi — abstraction ou implémentation ! »
Erreur n° 5 : le polymorphisme ne fonctionne pas pour les méthodes statiques.
Le polymorphisme ne fonctionne que pour les méthodes non statiques. Les méthodes statiques ne sont pas redéfinies — elles sont masquées ; par conséquent, le comportement à l’appel dépend du type de la variable, et non de l’objet effectif.
GO TO FULL VERSION