1. Polymorphisme : qu’est-ce que c’est et à quoi ça sert
Si vous pensez que le polymorphisme vient de l’univers des mutants Marvel, désolé de vous décevoir : en programmation, tout est bien plus calme, mais pas moins magique. Le polymorphisme, c’est la capacité d’objets avec des implémentations différentes à réagir différemment aux mêmes appels de méthodes.
Exemple de la vie courante :
Vous avez une classe Book et une classe Magazine, toutes deux héritent de la classe abstraite LibraryItem. Vous souhaitez pouvoir appeler la méthode printInfo() pour n’importe quel élément de la bibliothèque, et qu’elle affiche les informations pertinentes — pour un livre, l’auteur et le titre ; pour un magazine, le numéro de parution et la date.
Exemple de code :
abstract class LibraryItem {
String title;
LibraryItem(String title) {
this.title = title;
}
abstract void printInfo();
}
class Book extends LibraryItem {
String author;
Book(String title, String author) {
super(title);
this.author = author;
}
@Override
void printInfo() {
System.out.println("Livre : " + title + ", auteur : " + author);
}
}
class Magazine extends LibraryItem {
int issueNumber;
Magazine(String title, int issueNumber) {
super(title);
this.issueNumber = issueNumber;
}
@Override
void printInfo() {
System.out.println("Magazine : " + title + ", numéro : " + issueNumber);
}
}
On peut maintenant créer un tableau d’éléments différents et appeler printInfo() pour chacun :
LibraryItem[] items = {
new Book("Sa Majesté des mouches", "William Golding"),
new Magazine("Science et Vie", 5)
};
for (LibraryItem item : items) {
item.printInfo();
}
// Affichera :
// Livre : Sa Majesté des mouches, auteur : William Golding
// Magazine : Science et Vie, numéro : 5
Voilà comment fonctionne le polymorphisme !
2. Erreurs typiques avec le polymorphisme
Tentative d’appeler des méthodes absentes du type de base
L’une des erreurs les plus fréquentes est d’essayer d’appeler une méthode qui n’est déclarée que dans la classe fille via une référence du type de base.
LibraryItem item = new Book("Harry Potter", "J. K. Rowling");
// item.getAuthor(); // Erreur de compilation ! Il n’y a pas de méthode getAuthor() dans LibraryItem
Java compile le code en se basant sur ce qu’elle voit dans le type de la variable (LibraryItem) et non dans l’objet réel (Book). Par conséquent, si vous devez appeler une méthode spécifique au livre, vous devez effectuer un cast :
if (item instanceof Book) {
Book book = (Book) item;
// On peut maintenant appeler book.getAuthor()
}
Conversion de type sans vérification
Si vous êtes persuadé que l’objet est un Book alors que ce n’est pas le cas, vous obtiendrez une ClassCastException à l’exécution. Par exemple :
LibraryItem item = new Magazine("Forbes", 12);
Book book = (Book) item; // BOUM ! ClassCastException
La bonne pratique — toujours vérifier le type :
if (item instanceof Book) {
Book book = (Book) item;
// OK
} else {
System.out.println("Ce n’est pas un livre !");
}
Ne pas tirer parti du polymorphisme
Parfois, les développeurs écrivent du code trop lié à des types concrets, alors qu’ils pourraient utiliser des abstractions. Par exemple, si vous écrivez :
Book[] books = ...;
for (Book book : books) {
book.printInfo();
}
Cela ne fonctionne que pour les livres. Et si demain vous avez des magazines, des journaux, des bandes dessinées ? Il vaut mieux utiliser un tableau de LibraryItem[] et travailler avec les méthodes de la classe de base ou de l’interface.
3. Abstractions : pourquoi elles sont utiles et comment ne pas les gâcher
Classes abstraites et interfaces
L’abstraction, c’est l’art de mettre en avant l’essentiel et de cacher les détails. En Java, on dispose pour cela des classes abstraites et des interfaces.
- Classe abstraite — une classe qui ne peut pas être instanciée directement, seulement héritée.
- Interface — un contrat : ce que la classe doit savoir faire, mais pas comment elle le fait.
Erreur 1 : créer une classe abstraite sans méthodes abstraites
Si votre classe abstraite ne contient aucune méthode abstraite, demandez-vous — doit-elle vraiment être abstraite ? Peut-être vaut-il mieux en faire une classe normale ?
abstract class UselessAbstract {
void sayHello() {
System.out.println("Hello!");
}
}
// Mieux vaut faire une classe normale s’il n’y a pas de méthodes abstraites
Erreur 2 : absence d’implémentation des méthodes obligatoires dans les sous-classes
Si une classe hérite d’une classe abstraite ou implémente une interface, elle doit implémenter toutes les méthodes abstraites. Si vous oubliez — le compilateur vous le rappellera, mais il arrive que les méthodes soient implémentées « pour la forme » et ne fassent rien. C’est mauvais pour la maintenance du code.
class Magazine extends LibraryItem {
Magazine(String title, int issueNumber) {
super(title);
// ...
}
@Override
void printInfo() {
// Vide ! Mauvais !
}
}
Erreur 3 : hiérarchie d’abstractions trop profonde ou confuse
Quand les classes héritent les unes des autres sur cinq à dix niveaux, il devient très difficile de s’y retrouver. Il vaut mieux créer des hiérarchies « plates », où tout est clair.
Mauvais exemple :
LibraryItem
|
BookItem
|
PrintedBook
|
IllustratedBook
|
ChildrenIllustratedBook
Compliqué, n’est-ce pas ? Mieux vaut se limiter à deux ou trois niveaux.
4. Pratique : application du polymorphisme et des abstractions dans une application pédagogique
Améliorons votre application pédagogique de bibliothèque. Auparavant, vous n’aviez que des livres. Ajoutons maintenant des magazines et mettons en place une interface commune pour les publications imprimées.
Déclarons une classe abstraite :
abstract class LibraryItem {
protected String title;
public LibraryItem(String title) {
this.title = title;
}
public abstract void printInfo();
}
Ajoutons des classes dérivées :
class Book extends LibraryItem {
private String author;
public Book(String title, String author) {
super(title);
this.author = author;
}
@Override
public void printInfo() {
System.out.println("Livre : " + title + ", auteur : " + author);
}
}
class Magazine extends LibraryItem {
private int issueNumber;
public Magazine(String title, int issueNumber) {
super(title);
this.issueNumber = issueNumber;
}
@Override
public void printInfo() {
System.out.println("Magazine : " + title + ", numéro : " + issueNumber);
}
}
Utilisons le polymorphisme :
LibraryItem[] items = {
new Book("Clean Code", "Robert Martin"),
new Magazine("Java World", 3)
};
for (LibraryItem item : items) {
item.printInfo();
}
Ajoutons une interface pour les publications électroniques
Supposons que certaines publications puissent être lues en ligne. Introduisons une interface :
interface ReadableOnline {
void openOnline();
}
class EBook extends Book implements ReadableOnline {
private String url;
public EBook(String title, String author, String url) {
super(title, author);
this.url = url;
}
@Override
public void openOnline() {
System.out.println("Ouverture du livre électronique à l’adresse : " + url);
}
}
On peut maintenant manipuler les livres électroniques via l’interface :
ReadableOnline ebook = new EBook("Java pour les nuls", "Barry Burd", "https://example.com/java");
ebook.openOnline();
5. Comment éviter les problèmes de polymorphisme et d’abstractions : bonnes pratiques
- Utilisez des interfaces et des classes abstraites pour décrire des comportements, pas des états.
Par exemple, l’interface Printable décrit bien la capacité d’« impression », tandis que stocker dans une interface un champ String title est déjà une mauvaise idée. - Vérifiez le type de l’objet avec instanceof avant de caster.
Surtout si l’objet peut être de types différents. Cela vous mettra à l’abri des ClassCastException. - Visez des hiérarchies « plates » et compréhensibles.
Plus l’arbre d’héritage est simple, plus il est facile de maintenir et de faire évoluer le code. - Évitez les abstractions « vides ».
Si une classe ne contient pas de méthodes abstraites et n’est pas destinée à être héritée — ne la déclarez pas abstraite. - Utilisez l’annotation @Override dès que vous redéfinissez une méthode.
Cela aide le compilateur à détecter les erreurs de signature.
6. Erreurs typiques lors de l’utilisation du polymorphisme et des abstractions
Erreur n° 1 : cast sans vérification
Parfois, on veut aller plus vite et caster sans vérifier. Cela peut marcher, mais peut aussi provoquer une chute soudaine du programme. Utilisez toujours instanceof :
if (item instanceof Book) {
Book book = (Book) item;
// ...
}
Erreur n° 2 : tentative d’appeler une méthode de la sous-classe via une référence de type de base
LibraryItem item = new Book("Java", "Auteur");
item.getAuthor(); // Erreur de compilation : LibraryItem ne possède pas une telle méthode !
Solution — soit effectuer un cast, soit ajouter la méthode nécessaire dans la classe de base (si cela a du sens).
Erreur n° 3 : implémentation incomplète d’une interface ou d’une classe abstraite
Si vous oubliez d’implémenter toutes les méthodes d’une interface — le compilateur n’autorisera pas la construction du projet. Mais si vous implémentez des « bouchons » qui ne font rien, cela mènera à des comportements inattendus.
Erreur n° 4 : hiérarchie d’héritage trop profonde
Si vous avez plus de trois niveaux d’héritage — demandez-vous s’il n’est pas possible de simplifier l’architecture.
Erreur n° 5 : violation du principe de responsabilité unique
Si une abstraction décrit trop de responsabilités, elle devient difficile à maintenir. Il vaut mieux la découper en plusieurs interfaces ou classes.
GO TO FULL VERSION