Introduction
En programmation orientée objet, l’héritage est un des concepts clés. Il permet, entre autres, le polymorphisme, la généralisation, une meilleure encapsulation de l’information, une diminution du nombre de méthodes et attribut dans les classes (ce qui les rend plus simples) ainsi qu’une diminution substantielle de la duplication de code du système.
C’est quoi, l’héritage
Les mécaniques d’héritages permettent de spécifier des classes (et donc des types) qui sont des généralisations d’une ou plusieurs autres classes (et types).
Nous obtenons donc des classes qui sont plus générales et des classes qui sont plus spécifiques. Un lien d’héritage est donc un lien qui permet une généralisation/spécialisation d’une classe vers une autre.
La classe spécifique possède (ou hérite de) toutes les méthodes et tous les attributs que la classe générale possède, mais elle peut y ajouter de nouvelles méthodes, de nouveaux attributs ou de nouvelle implémentation des méthodes déjà présentes dans la classe générale.
Dans une relation d’héritage, on appel parent la classe générale et enfant la classe spécifique.
L’héritage dans un diagramme UML
Dans un diagramme UML, l’héritage est représenté par une flèche dont la pointe est fermée et blanche. L’origine de la flèche est la classe spécifique et la destination est la classe générale.
Un exemple
Voici un diagramme de classe avec héritage:
On voit ici que la classe générale est la classe « Animal » et les classes « Chien » et « Chat » sont des classes spécifiques de la classe « Animal ». Puisque « Chien » et « Chat » hérite de « Animal », ces classes (« Chien » et « Chat ») contiennent, par héritage les attributs « nom » et « age ». Inversement, puisque les méthodes « aboyer » et « miauler » sont déclarées seulement dans les classes spécifiques, on en déduit donc que « Chat » a la méthode « miauler », mais pas la méthode « aboyer » et que la classe « Chien » a la méthode « aboyer », mais pas la méthode « miauler ».
L’héritage en Java
Dans le langage Java, on spécifie l’héritage en utilisant la clause « extends ». Voici l’exemple présenté précédemment (« Animal », « Chien » et « Chat ») en Java:
Voici la classe « Animal »:
/**
* Représente tous les spécimens animal.
*
* @author Louis Marchand (prog@tioui.com)
* @version 0.1, mercredi mar 10, 2021 16:58:57 EST
*
* Distribué sous licence MIT.
*/
public class Animal {
/**
* L'identificateur de l'Animal
*/
public String nom;
/**
* Retourne nom.
* @return nom
*/
public String getNom() {
return nom;
}
/**
* Assigne nom.
* @param aNom Nouvelle valeur de nom.
*/
public void setNom(String aNom) {
nom = aNom;
}
/**
* Le nombre d'années depuis la naissance de l'Animal
*/
public int age;
/**
* Retourne age.
* @return age
*/
public int getAge() {
return age;
}
/**
* Assigne age.
* @param aAge Nouvelle valeur de age.
*/
public void setAge(int aAge) {
age = aAge;
}
}
Voici la classe « Chien »:
/**
* Animal de race canine.
*
* @author Louis Marchand (prog@tioui.com)
* @version 0.1, mercredi mar 10, 2021 17:03:49 EST
*
* Distribué sous licence MIT.
*/
public class Chien extends Animal{
/**
* Cri du chien.
**/
public void aboyer() {
System.out.println("Wouf Wouf!");
}
}
Voici la classe « Chat »:
/**
* Animal de race féline.
*
* @author Louis Marchand (prog@tioui.com)
* @version 0.1, mercredi mar 10, 2021 17:03:49 EST
*
* Distribué sous licence MIT.
*/
public class Chat extends Animal{
/**
* Cri du chat.
**/
public void miauler() {
System.out.println("Miaou!");
}
}
Et finalement, pour tester, la classe « Programme »:
/**
* Programme principal de l'exemple des animaux.
*
* @author Louis Marchand (prog@tioui.com)
* @version 0.1, mercredi mar 10, 2021 17:14:16 EST
*
* Distribué sous licence MIT.
*/
public class Programme {
/**
* Exécution du programme.
*
* @param aArguments Les paramêtres du programme.
*/
public static void main(String[] aArguments) {
Chien lChien = new Chien();
Chat lChat = new Chat();
lChien.setNom("Rex");
lChat.setNom("Minou");
lChien.setAge(10);
lChat.setAge(3);
System.out.println("L'age du chien " + lChien.getNom() + " est " +
lChien.getAge());
System.out.println("L'age du chat " + lChat.getNom() + " est " +
lChat.getAge());
lChien.aboyer();
lChat.miauler();
}
}
Le résultat du programme est:
L'age du chien Rex est 10 L'age du chat Minou est 3 Wouf Wouf! Miaou!
L’exemple nous montre en effet que, malgré que mes variables « lChien » et « lChat » ne sont pas typées comme « Animal », ils ont tout de même accès aux « getters » et « setters » des attributs « nom » et « age ».
À noter
Il est important de comprendre que les liens d’héritages peuvent s’appliquer sur plusieurs niveaux. En effet, une classe qui est la classe générale d’une autre classe peut elle aussi hériter d’une classe. Par exemple dans le diagramme de classe suivant:
On voit que la classe « Chien » hérite de « Animal » (comme c’était le cas précédemment), mais maintenant, puisque « Animal » hérite de « Espèce », on peut également dire que « Chien » hérite de « Espèce ». En d’autres mots, tous les attributs et les méthodes de « Espèce » seront hérités dans « Animal », « Insecte », « Chien », « Chat » et « Abeille ».
On dit qu’une classe est l’ancêtre d’une autre classe s’il y a un « chemin » permettant d’atteindre l’autre classe en suivant les liens d’héritage parent vers enfant. Également, si une classe est l’ancêtre d’une autre classe, cette autre classe est dite descendant de la classe ancêtre.
La structure parent/enfant que créé l’héritage simple génère un arbre appelé arbre d’héritage.
À noter à propos de l’arbre d’héritage
En Java, la classe à la racine d’arbre d’héritage est la classe Object.
Il est également important de noter que toutes les classes héritent, directement ou indirectement, de la classe Object; et ce, même si l’héritage n’est pas spécifié. Par exemple, la classe suivante hérite de « Object »:
public class Test {
}
Les constructeurs
Dans l’exemple sur les Animaux vu plus haut, on voit que je n’ai pas mis de constructeur à ma classe. Par contre, puisque cette classe possède des attributs, j’aurais donc dû lui mettre un constructeur afin d’initialiser ces attributs. Donc, voici la classe « Animal » avec un constructeur permettant de préassigner les valeurs des attributs « nom » et « age ».
/**
* Représente tous les spécimens animal.
*
* @author Louis Marchand (prog@tioui.com)
* @version 0.1, mercredi mar 10, 2021 16:58:57 EST
*
* Distribué sous licence MIT.
*/
public class Animal {
/**
* L'identificateur de l'Animal
*/
public String nom;
/**
* Retourne nom.
* @return nom
*/
public String getNom() {
return nom;
}
/**
* Assigne nom.
* @param aNom Nouvelle valeur de nom.
*/
public void setNom(String aNom) {
nom = aNom;
}
/**
* Le nombre d'années depuis la naissance de l'Animal
*/
public int age;
/**
* Retourne age.
* @return age
*/
public int getAge() {
return age;
}
/**
* Assigne age.
* @param aAge Nouvelle valeur de age.
*/
public void setAge(int aAge) {
age = aAge;
}
/**
* Constructeur de Animal
*
* @param aNom Valeur de nom
* @param aAge Valeur de age
*/
public Animal(String aNom, int aAge) {
nom = aNom;
age = aAge;
}
}
Par contre, nous voyons que lorsque nous instancions des objets de type « Chien » et « Chat », comme ceci:
Chien lChien = new Chien();
Chat lChat = new Chat();
les constructeurs utilisés sont ceux de « Chien » et de « Chat » et non celui de « Animal ». Il est donc nécessaire de lancer le constructeur de « Animal » à partir des constructeurs de « Chien » et de « Chat » pour s’assurer que les attributs de la classe « Animal » soient bel et bien initialisés.
Pour lancer le constructeur de la classe parent, nous utilisons l’appel de méthode spéciale super. Voici les classes « Chien » et « Chat » modifiées avec leurs constructeurs.
La classe « Chien »:
/**
* Animal de race canine.
*
* @author Louis Marchand (prog@tioui.com)
* @version 0.1, mercredi mar 10, 2021 17:03:49 EST
*
* Distribué sous licence MIT.
*/
public class Chien extends Animal{
/**
* Cri du chien.
**/
public void aboyer() {
System.out.println("Wouf Wouf!");
}
/**
* Constructeur du Chien
*
* @param aNom Valeur de nom
* @param aAge Valeur de age
*/
public Chien(String aNom, int aAge) {
super(aNom, aAge);
}
}
La classe « Chat »:
/**
* Animal de race féline.
*
* @author Louis Marchand (prog@tioui.com)
* @version 0.1, mercredi mar 10, 2021 17:03:49 EST
*
* Distribué sous licence MIT.
*/
public class Chat extends Animal{
/**
* Cri du chat.
**/
public void miauler() {
System.out.println("Miaou!");
}
/**
* Constructeur du Chat
*
* @param aNom Valeur de nom
* @param aAge Valeur de age
*/
public Chat(String aNom, int aAge) {
super(aNom, aAge);
}
}
Finalement, le programme doit être modifié afin d’utiliser les nouveaux constructeurs de nos classes « Chien » et « Chat »:
/**
* Programme principal de l'exemple des animaux.
*
* @author Louis Marchand (prog@tioui.com)
* @version 0.1, mercredi mar 10, 2021 17:14:16 EST
*
* Distribué sous licence MIT.
*/
public class Programme {
/**
* Exécution du programme.
*
* @param aArguments Les paramêtres du programme.
*/
public static void main(String[] aArguments) {
Chien lChien = new Chien("Rex", 10);
Chat lChat = new Chat("Minou", 3);
System.out.println("L'age du chien " + lChien.getNom() + " est " +
lChien.getAge());
System.out.println("L'age du chat " + lChat.getNom() + " est " +
lChat.getAge());
lChien.aboyer();
lChat.miauler();
}
}
Un petit truc
Souvent, les débutants en « desing » orienté objet ont de la misère a savoir s’ils doivent utiliser un lien client/fournisseur ou un lien d’héritage entre deux classes. Voici un petit truc qui vous permettra d’utiliser le bon lien:
-
- Lorsqu’on peut dire: est un ou est une en parlant de nos deux classes, on obtient un lien d’héritage.
- Par exemple, si j’ai les classes « Fruit » et « Pomme », je peux dire « une pomme est un fruit ». Donc « Pomme » héritera de « Fruit ».
- Lorsqu’on peut dire: a un, a une, a des, utilise un, utilise une ou utilise des, on a un lien client/fournisseur.
- Par exemple, si j’ai la classe « Pommier » et « Pomme », je peux dire « un pommier a des pommes ». Donc, « Pommier » est client de « Pomme ».
- Lorsqu’on peut dire: est un ou est une en parlant de nos deux classes, on obtient un lien d’héritage.
À propos des Exceptions
Nous avons vue dans une présentation précédente, que pour gérer les erreurs dans les méthodes des classes, nous utilisons des Exceptions. Pour se faire, nous avons jusqu’à maintenant utilisée des instances de la classe « Exception » comme ceci:
throw new Exception("Message");
Par contre, ce n’est généralement pas une bonne pratique. Le problème ici, c’est que ça force le client de la classe à faire un code comme ceci:
try {
maMethode();
} catch (Exception exception) {
System.out.println("Erreur!!!");
}
Dans le code suivant, le « catch » sera utilisé peu importe le type d’Exception que la méthode pourra lancé. Il n’est donc pas possible pour le client de distinguer de quel type d’erreur il s’agit. Afin de remédier à cette problématique, nous pouvons créer un nouveau type d’Exception, propre à notre programme qui sera utilisé à la place de la classe « Exception ». Pour se faire, nous devons créer une nouvelle classe qui hérite de « Exception ». Voici un exemple:
/**
* Exception à lancer lorsqu'un String n'est pas valide.
*
* @author Louis Marchand (prog@tioui.com)
* @version 0.1, lundi mar 15, 2021 09:59:26 EDT
*
* Distribué sous licence MIT.
*/
public class StringNonValideException extends Exception {
/**
* Constructeur de StringNonValideException
*/
public StringNonValideException(String aMessage) {
super(aMessage);
}
}
Voici maintenant un exemple de code qui utilise cet exceptions:
/**
* Un individu.
*
* @author Louis Marchand (prog@tioui.com)
* @version 0.1, lundi mar 15, 2021 20:03:19 EDT
*
* Distribué sous licence MIT.
*/
public class Personne {
/**
* Le nom de famille de la personne.
*/
private String nom;
/**
* Retourne nom.
* @return nom
*/
public String getNom(){
return nom;
}
/**
* Assigne nom.
* @param aNom Nouvelle valeur de nom.
* @throws StringNonValideException Si aNom est null ou vide.
*/
public void setNom(String aNom) throws StringNonValideException {
if (aNom != null && !aNom.isEmpty()){
nom = aNom;
} else {
throw new StringNonValideException("Le nom ne peut pas être null");
}
}
/**
* L'identificateur de la personne.
*/
private String prenom;
/**
* Retourne prenom.
* @return prenom
*/
public String getPrenom(){
return prenom;
}
/**
* Assigne prenom.
* @param aPrenom Nouvelle valeur de prenom.
* @throws StringNonValideException Si aPrenom est null ou vide.
*/
public void setPrenom(String aPrenom) throws StringNonValideException {
if (aPrenom != null && !aPrenom.isEmpty()){
prenom = aPrenom;
} else {
throw new StringNonValideException(
"Le prenom ne peut pas être null");
}
}
/**
* L'identificateur compet de la Personne.
*/
public String getNomComplet(){
return prenom + " " + nom;
}
/**
* Assigne nom et prenom.
* @param aNom nouvelle valeur de nom.
* @param aPrenom nouvelle valeur de prenom.
* @throws StringNonValideException Si aNom ou aPrenom est null ou vide.
*/
public void setNomComplet(String aNom, String aPrenom)
throws StringNonValideException {
setNom(aNom);
setPrenom(aPrenom);
}
/**
* Constructeur de Personne
*
* @param aNom Valeur de nom.
* @param aPrenom Valeur de prenom.
* @throws StringNonValideException Si aNom ou aPrenom est null ou vide.
*/
public Personne(String aNom, String aPrenom)
throws StringNonValideException {
setNomComplet(aNom, aPrenom);
}
}
Enfin, voiçi un exemple de programme utilisant cette classe Personne avec l’exception:
import java.util.Scanner;
/**
* Exemple d'utilisation d'exception.
*
* @author Louis Marchand (prog@tioui.com)
* @version 0.1, lundi mar 15, 2021 20:08:45 EDT
*
* Distribué sous licence MIT.
*/
public class Programme {
/**
* Exécution du programme.
*
* @param aArguments Les paramêtres du programme.
*/
public static void main(String[] arguments) {
Scanner lScanner = new Scanner(System.in);
String lPrenom, lNom;
System.out.print("Entrez le prenom: ");
lPrenom = lScanner.nextLine();
System.out.print("Entrez le nom: ");
lNom = lScanner.nextLine();
try {
Personne lPersonne = new Personne(lPrenom, lNom);
System.out.println("La personne: " + lPersonne.getPrenom() + " " +
lPersonne.getNom());
} catch (StringNonValideException exception) {
System.out.println("Les prénoms et noms ne peuvent être vide.");
}
}
}
À propos de la portée protégée
Comme nous l’avons vu dans la théorie sur le masquage de l’information, la portée protégée en Java sert, entre autre, à permettre aux descendants d’une classe d’accéder à une méthode ou à un attribut de la classe. En effet, parfois, il peut être pratique de permettre aux descendants de la classe d’accéder à une méthode ou à un attribut, sans nécessairement rendre cette méthode ou cet attribut publique.
Voici un exemple. J’ai trois menus qui effectue du travail très similaire. Afin de diminuer au maximum la duplication de code, voici une classe parent qui sera utilisée pour tous les menus:
import java.util.Scanner;
/**
* Ancêtre commun à tous les menus.
*
* @author Louis Marchand (prog@tioui.com)
* @version 0.1, mardi mar 16, 2021 09:49:04 EDT
*
* Distribué sous licence MIT.
*/
public class Menu {
protected Scanner scanner;
/**
* Dernier choix sélectionné par la méthode `selectionChoix`.
* @see #selectionChoix
*/
protected int choixSelectionne;
/**
* Sélectionne une valeur entre `aMinimum` et `aMaximum`.
*
* Sélectionne une valeur entre `aMinimum` et `aMaximum` et place sa
* valeur dans {@link choixSelectionne}.
*
* @param aMinimum La valeur minimum que l'utilisateur peut entrer.
* @param aMaximum La valeur maximum que l'utilisateur peut entrer.
*
* @see #selectionChoix
*/
protected void selectionChoix(int aMinimum, int aMaximum) {
boolean lValide = false;
String lEntree;
int lValeur;
while (!lValide) {
System.out.print("Choix: ");
lEntree = scanner.nextLine();
try {
lValeur = Integer.parseInt(lEntree);
if (lValeur >= aMinimum && lValeur <= aMaximum) {
choixSelectionne = lValeur;
lValide = true;
} else {
System.out.println("Vous devez entrer une valeur entre " +
aMinimum + " et " + aMaximum);
}
} catch (NumberFormatException exception) {
System.out.println("La valeur " + lEntree + " n'est pas valide.");
}
}
}
/**
* Constructeur de Menu
*/
public Menu() {
choixSelectionne = 0;
scanner = new Scanner(System.in);
}
}
On voit que puisque la méthode « selectionChoix » est protégée, on comprend que les descendants de la classe Menu pourront utiliser la méthode. Également, on peut voir que si les différents menus nécessitent un « Scanner », ils pourront utiliser celui prévus dans cette classe.
Voici deux menus permettant de gérer un programme dans deux langues différentes:
En français:
/**
* Le menu principal en français.
*
* @author Louis Marchand (prog@tioui.com)
* @version 0.1, mardi mar 16, 2021 12:59:12 EDT
*
* Distribué sous licence MIT.
*/
public class MenuFrancais extends Menu {
/**
* Constructeur de MenuFrancais
*/
public MenuFrancais() {
super();
}
/**
* Affiche le menu.
**/
public void afficherMenu() {
System.out.println("Que voulez-vous faire:");
System.out.println("\t1 - Dire Allo,");
System.out.println("\t2 - Dire Salut,");
System.out.println("\t3 - Dire Bonjour,");
System.out.println("\t0 - Annuler.");
selectionChoix(0, 3);
if (choixSelectionne == 1){
System.out.println("Allo");
} else if (choixSelectionne == 2){
System.out.println("Salut");
} else if (choixSelectionne == 3){
System.out.println("Bonjour");
}
}
}
En anglais:
/**
* Le menu principal en Anglais.
*
* @author Louis Marchand (prog@tioui.com)
* @version 0.1, mardi mar 16, 2021 12:59:12 EDT
*
* Distribué sous licence MIT.
*/
public class MenuAnglais extends Menu {
/**
* Constructeur de MenuAnglais
*/
public MenuAnglais() {
super();
}
/**
* Affiche le menu.
**/
public void afficherMenu() {
System.out.println("What do you want:");
System.out.println("\t1 - Say Hello,");
System.out.println("\t2 - Say Hi,");
System.out.println("\t3 - Say Good day,");
System.out.println("\t0 - Cancel.");
selectionChoix(0, 3);
if (choixSelectionne == 1){
System.out.println("Hello");
} else if (choixSelectionne == 2){
System.out.println("Hi");
} else if (choixSelectionne == 3){
System.out.println("Good day");
}
}
}
Voici un autre menu permettant de sélectionner une langue pour le programme:
/**
* Le menu des langues
*
* @author Louis Marchand (prog@tioui.com)
* @version 0.1, mardi mar 16, 2021 12:59:12 EDT
*
* Distribué sous licence MIT.
*/
public class MenuLangue extends Menu {
/**
* Constructeur de MenuLangue
*/
public MenuLangue() {
super();
}
/**
* Affiche le menu.
**/
public void afficherMenu() {
System.out.println("Langue/Language:");
System.out.println("\t1 - Français,");
System.out.println("\t2 - English,");
System.out.println("\t0 - Annuler.");
selectionChoix(0, 2);
if (choixSelectionne == 1){
MenuFrancais lMenu = new MenuFrancais();
lMenu.afficherMenu();
} else if (choixSelectionne == 2){
MenuAnglais lMenu = new MenuAnglais();
lMenu.afficherMenu();
}
}
}
Enfin, voici la classe principale du programme qui démarre les différents menus:
/**
* Exemple d'utilisation de la portée protégée.
*
* @author Louis Marchand (prog@tioui.com)
* @version 0.1, lundi mar 15, 2021 20:08:45 EDT
*
* Distribué sous licence MIT.
*/
public class Programme {
/**
* Exécution du programme.
*
* @param aArguments Les paramêtres du programme.
*/
public static void main(String[] arguments) {
MenuLangue lMenu = new MenuLangue();
lMenu.afficherMenu();
}
}
Auteur: Louis Marchand
Sauf pour les sections spécifiées autrement, ce travail est sous licence Creative Commons Attribution 4.0 International.