L'Héritage en Java
Introduction à l'Héritage
L'héritage est un concept fondamental de la programmation orientée objet (POO) qui permet à
une classe d'hériter des caractéristiques (attributs et méthodes) d'une autre classe. Cela favorise
la réutilisation du code, réduit le temps de développement et facilite la maintenance des
applications.
Le Problème de la Duplication de Code
Sans héritage, la création de classes similaires peut entraîner une duplication de code. Par
exemple, si l'on a les classes Personne, Etudiant et Employe, toutes partageant des attributs
comme nom, cin et anneeNaiss, ainsi qu'une méthode age(), la modification de ces éléments
communs nécessiterait des changements dans chaque classe individuellement, augmentant le
risque d'erreurs.
La Solution : Super-classe et Sous-classes
L'héritage résout ce problème en introduisant le concept de super-classe (ou classe mère) et de
sous-classes (ou classes filles).
Super-classe (Classe Mère) : Contient les attributs et méthodes communs à plusieurs
classes. Dans l'exemple, Personne serait la super-classe.
Sous-classes (Classes Filles) : Héritent des caractéristiques de la super-classe et ajoutent
leurs propres attributs et méthodes spécifiques. Etudiant et Employe seraient des sous-
classes de Personne.
Avantages de l'Héritage
Réutilisation du Code : Évite de réécrire le code commun.
Maintenance Simplifiée : Une modification d'un attribut ou d'une méthode commune n'a
besoin d'être effectuée qu'une seule fois dans la super-classe.
Notion d'Héritage
L'héritage permet à une classe (classe fille) de se comporter globalement comme une autre
classe (classe mère), tout en introduisant des différences spécifiques sans modifier le code
source de la classe mère.
Classe Mère (Parente, Super-classe) : La classe dont on hérite.
Classe Fille (Sous-classe) : La classe qui hérite.
Le code source de la classe fille ne contient que les éléments qui diffèrent ou sont ajoutés par
rapport à la classe mère.
Exemple d'Héritage
Considérons les classes Personne, Etudiant et Employe :
Personne
:
Attributs : nom, cin, anneeNaiss
Méthode : age()
Etudiant
(hérite de
Personne
):
Attributs spécifiques : note1, note2
Méthode spécifique : moyenne()
Employe
(hérite de
Personne
):
Attributs spécifiques : prixHeure, nbreHeure
Méthode spécifique : salaire()
Modélisation UML
Dans un diagramme de classes UML, l'héritage est représenté par une flèche pointant de la sous-
classe vers la super-classe.
classDiagram
Personne <|-- Etudiant
Personne <|-- Employe
Personne : +String nom
Personne : +int cin
Personne : +int anneeNaiss
Personne : +int age()
Etudiant : +float note1
Etudiant : +float note2
Etudiant : +float moyenne()
Employe : +float prixHeure
Employe : +int nbreHeure
Employe : +float salaire()
L'Héritage en Java
Définition
En Java, l'héritage est le mécanisme par lequel une classe peut acquérir les propriétés (attributs
et méthodes) d'une autre classe. Le mot-clé extends est utilisé pour déclarer l'héritage.
class Etudiant extends Personne {
// Attributs et méthodes spécifiques à Etudiant
}
Une Seule Classe Mère
Java ne supporte pas l'héritage multiple. Une classe ne peut hériter que d'une seule classe mère.
Cependant, une super-classe peut avoir plusieurs sous-classes.
class Personne { /* ... */ }
class Etudiant extends Personne { /* ... */ }
class Employe extends Personne { /* ... */ }
class Chercheur extends Personne { /* ... */ }
Héritage en Cascade
L'héritage peut s'étendre sur plusieurs niveaux. Une classe fille peut devenir une classe mère
pour une autre classe.
Exemple : Personne -> Etudiant -> EleveIngenieur
EleveIngenieur hérite de Etudiant, qui hérite de Personne.
Un EleveIngenieur a donc accès aux méthodes et attributs de Etudiant et Personne.
Le Principe "Est un"
Si une classe B hérite d'une classe A (B extends A), alors tout objet de type B est aussi un objet
de type A. On dit que B est un sous-type de A.
Exemples :
Un Etudiant est une Personne.
Un Chat est un Animal.
Une Voiture est un Vehicule.
Cela signifie qu'une variable de type A peut référencer un objet de type B.
Personne p = new Etudiant(); // Autorisé
La Classe Object
Toutes les classes en Java héritent implicitement de la classe Object, qui est la super-classe
racine de la hiérarchie des classes. La classe Object fournit des méthodes fondamentales
comme toString() et equals().
Le Mot-clé super
Le mot-clé super est utilisé dans une sous-classe pour accéder aux membres (attributs et
méthodes) de sa classe mère.
Accès aux attributs : [Link]
Appel aux méthodes : [Link]()
Appel aux constructeurs : super(paramètres)
Constructeurs et super
L'appel au constructeur de la super-classe doit être la première instruction dans le constructeur
de la sous-classe.
Si aucun appel explicite à super() n'est fait, le constructeur par défaut de la classe mère est
implicitement appelé.
Si la classe mère n'a pas de constructeur par défaut (parce qu'elle a des constructeurs
surchargés), il est obligatoire d'appeler explicitement un constructeur surchargé de la classe
mère en utilisant super(...).
public class Animal {
public Animal(int nbPattes) { /* ... */ }
}
public class Chat extends Animal {
// Si Animal n'a pas de constructeur par défaut, on doit appeler le constructeur de Animal
public Chat() {
super(4); // Appel explicite au constructeur de Animal
}
}
Si un constructeur surchargé est créé pour la classe mère, et que le constructeur par défaut
n'existe pas, une erreur de compilation se produit si l'on tente d'appeler super() sans arguments.
Solutions pour les Constructeurs sans Constructeur par Défaut :
1. Déclarer explicitement un constructeur par défaut dans la classe mère.
2. Faire un appel explicite au constructeur surchargé de la classe mère dans le constructeur de
la sous-classe.
Enchaînement des Constructeurs
Les appels à super() enchaînent l'exécution des constructeurs depuis la classe la plus générale
(racine de la hiérarchie) jusqu'à la classe la plus spécifique.
public class Humain {
public Humain() { [Link]("Humain"); }
}
public class Client extends Humain {
public Client() {
super(); // Appel implicite si Humain a un constructeur par défaut
[Link]("Client");
}
}
public class ClientFidele extends Client {
public ClientFidele() {
super(); // Appel implicite
[Link]("ClientFidele");
}
}
// L'instanciation de ClientFidele affichera : Humain, Client, ClientFidele
Redéfinition et Surcharge de Méthodes
Redéfinition (Override) : Une méthode dans la sous-classe a la même signature (nom et
types de paramètres) qu'une méthode héritée de la classe mère. Le corps de la méthode est
modifié pour adapter son comportement. On utilise souvent [Link]() pour réutiliser le
code de la méthode parente.
Surcharge (Overload) : Une méthode dans la même classe (ou une classe héritée) a le même
nom mais une signature différente (nombre, type ou ordre des paramètres).
Signature d'une méthode : Nom de la méthode + ensemble des types de ses paramètres.
class Personne {
public void lire() {
[Link]("Je sais lire.");
}
}
class Etudiant extends Personne {
// Redéfinition de la méthode lire()
@Override
public void lire() {
[Link]("Je suis un étudiant.");
[Link](); // Appel à la méthode lire() de Personne
}
// Surcharge de la méthode lire()
public void lire(String titreLivre) {
[Link]("J'ai lu le livre : " + titreLivre);
}
}
Etudiant possède une méthode lire() (redéfinie) et une méthode lire(String) (surcharge).
Une méthode static ne peut pas être redéfinie.
Le Mot-clé final
final pour une méthode : Empêche la redéfinition de cette méthode dans les sous-classes.
public final void methodeFinale() { /* ... */ }
final pour une classe : Empêche l'héritage de cette classe. Aucune autre classe ne peut
hériter d'une classe déclarée final. Toutes ses méthodes sont implicitement final. final class
ClasseFinale { /* ... */ }
Encapsulation et Visibilité
L'encapsulation consiste à masquer les détails d'implémentation d'une classe et à contrôler
l'accès à ses données via des méthodes publiques. Elle protège les données et simplifie l'API.
Les modificateurs d'accès en Java contrôlent la visibilité des membres d'une classe :
public : Accessible par toutes les classes, quel que soit le package.
protected : Accessible par la classe elle-même, les classes du même package, et les sous-
classes (même si elles sont dans un package différent). Il augmente la visibilité pour les
sous-classes.
Défaut (package-private) : Accessible par la classe elle-même et les classes du même
package.
private : Accessible uniquement par la classe elle-même.
Visibilité La classe Sous-classe, Pas sous- Sous-classe, Pas sous-classe,
elle-même même classe, même package package
package package différent différent
private Oui Non Non Non Non
Défaut Oui Oui Oui Non Non
protected Oui Oui Oui Oui Non
public Oui Oui Oui Oui Oui
Notez que protected ne protège pas mais augmente la visibilité pour les sous-classes et les
classes du même package.
L'héritage s'ajoute aux règles de visibilité liées aux packages. Par exemple, un membre protected
d'une classe dans le package A est accessible par une sous-classe dans le package B.
Polymorphisme
Le polymorphisme ("plusieurs formes") est la capacité d'une même écriture (un appel de
méthode) à correspondre à différents comportements d'exécution, en fonction du type réel de
l'objet. C'est une notion clé pour exploiter efficacement l'héritage.
Mécanisme
Le polymorphisme est rendu possible par le "late binding" (liaison retardée). La méthode qui sera
effectivement exécutée est déterminée à l'exécution, et non à la compilation, en se basant sur le
type réel de l'objet référencé, et non sur le type de la variable de référence.
Exemple
Animal a = new Horse(); // a est une référence de type Animal, mais pointe vers un objet Horse
[Link](); // La méthode eat() de Horse sera exécutée, pas celle de Animal.
Avantages
Le polymorphisme facilite l'extension des programmes. On peut ajouter de nouvelles sous-
classes sans modifier le code existant qui utilise la super-classe.
Exercice d'Application du Polymorphisme
Considérons le code suivant :
class Animal {
public void eat() {
[Link]("Generic Animal Eating Generically");
}
}
class Horse extends Animal {
@Override
public void eat() {
[Link]("Horse eating hay ");
}
public void eat(String s) {
[Link]("Horse eating " + s);
}
}
Analysons les invocations de méthodes :
1. Animal a = new Animal(); [Link]();
Résultat : Generic Animal Eating Generically
Explication : La référence a est de type Animal et pointe vers un objet Animal. La méthode
eat() de Animal est appelée.
2. Horse h = new Horse(); [Link]();
Résultat : Horse eating hay
Explication : La référence h est de type Horse et pointe vers un objet Horse. La méthode
eat() de Horse est appelée.
3. Animal ah = new Horse(); [Link]();
Résultat : Horse eating hay
Explication : Bien que la référence ah soit de type Animal, elle pointe vers un objet Horse.
Le polymorphisme assure que la méthode eat() de l'objet réel (Horse) est exécutée.
4. Horse he = new Horse(); [Link]("Apples");
Résultat : Horse eating Apples
Explication : La référence he est de type Horse. La méthode surchargée eat(String s) de
Horse est appelée.
5. Animal a2 = new Animal(); [Link]("treats");
Erreur de compilation
Explication : La référence a2 est de type Animal. La classe Animal n'a pas de méthode eat()
qui accepte un String comme paramètre. Le compilateur ne regarde que le type de la
référence.
6. Animal ah2 = new Horse(); [Link]("Carrots");
Erreur de compilation
Explication : La référence ah2 est de type Animal. Même si l'objet réel est un Horse, le
compilateur ne voit que la déclaration de la référence Animal, qui ne possède pas de
méthode eat(String).
La Méthode equals()
La méthode equals(Object obj) de la classe Object compare deux objets. Par défaut, elle vérifie si
les références des deux objets sont identiques. Souvent, il est nécessaire de redéfinir equals()
dans les sous-classes pour comparer les objets selon leur état (leurs attributs).
Pour que deux objets Personne soient considérés comme égaux, il faut qu'ils aient le même cin
et le même nom.
public class Personne {
private String nom;
private long cin;
// ... constructeurs et autres méthodes ...
@Override
public boolean equals(Object obj) {
if (this == obj) { // Vérification de l'identité de l'objet
return true;
}
if (obj == null || getClass() != [Link]()) { // Vérification du type
return false;
}
Personne personne = (Personne) obj; // Cast en Personne
// Comparaison des attributs
return [Link]([Link]) && cin == [Link];
}
}