Héritage en Java ressource de formation avance

Héritage en Java ressource de formation avancé
Pourquoi hériter ?
- Pour relier des classes entre elles
- lorsqu’une classe étend les fonctionnalités d’une autre classe
- lorsqu’une classe adapte le fonctionnement d’une autre classe à une situation particulière
- Pour exploiter des classes existantes
- sans en modifier le code
- même si ce code est inaccessible
- la documentation de la classe héritée suffit
- Pour organiser le développement
- regrouper les champs communs à plusieurs classes
- regrouper les méthodes communes
- Pour des applications plus évolutives
- moins de programmation, plus de conception
…
Fonctionnement de l’héritage
- Un objet hérite des champs et des méthodes de toutes ses classes ascendantes
- champs hérités
- méthodes héritées
- De plus, un objet dispose des champs et des méthodes de sa propre classe (nouveaux noms)
- champs supplémentaires
- méthodes supplémentaires
- Dans une classe, on peut redéfinir une méthode d’une classe ascendante
- méthodes redéfinies
Héritage en Java
Les langages objets sont fondés sur la connaissance d’une seule catégorie d’entité informatique : l’objet. Dans un objet, traditionnellement ce sont les données qui deviennent prépondérantes. On se pose d’abord la question : "de quoi parle-t-on ?" et non pas la question "que veut-on faire ?", comme en programmation algorithmique. C’est en ce sens que les machines abstraites de la programmation structurée modulaire peuvent être considérées comme des pré-objets. En fait la notion de TAD est utilisée dans cet ouvrage comme spécification d’un objet, en ce sens nous nous préoccupons essentiellement des services offerts par un objet indépendamment de sa structure interne.
- Concepts fondamentaux de La P.O.O
Nous écrirons P.O.O pour : programmation orientée objet.
Voici trois concepts qui donnent toute sa puissance à la P.O.O.
Concept de modélisation à travers la notion de classe et d’instanciation de ces classes.
Concept d’action à travers la notion d’envoi de messages et de méthodes à l’intérieur des objets.
Concept de construction par réutilisation et amélioration par l’utilisation de la notion d’héritage.
1.1 les objets
Définition
Un module représente un objet ou une classe d’objet de l’espace du problème et non une étape principale du processus total, comme en programmation descendante.
Recenser les objets du monde réel
Lors de l’analyse du problème, faire l’état de l’existant en recensant les objets du monde réel. On établit des classes d’objets et pour chaque objet on inventorie les connaissances que l’on a sur lui :
Les connaissances déclaratives,
les connaissances fonctionnelles,
l’objet réel et les connaissances que l’on a sur lui sont regroupés dans une même entité.
On décrit les systèmes en classes d’objets plutôt qu’en terme de fonction.
Exemple :
Une application de gestion bancaire est organisée sur les objets comptes, écritures, états.
Les objets rassemblent une partie de la connaissance totale portant sur le problème. Cette connaissance est répartie sur tous les objets sous forme déclarative ou procédurale.
Les objets sont décrits selon le modèle des structures abstraites de données (TAD) : ils constituent des boîtes noires dissimulant leur implantation avec une interface publique pour les autres objets. Les interactions s’établissant à travers cette interface.
Un objet :
Encapsulation
c’est le fait de réunir à l'intérieur d'une même entité (objet) le code (méthodes) + données (champs). Il est donc possible de masquer les informations d'un objet aux autres objets.
2 niveaux d’encapsulation :
Privé
les champs et les méthodes masqués sont dans la partie privée de l’objet.
Public
les champs et les méthodes visibles sont dans la partie interface de l’objet.
Les notions de privé et de public comme dans un objet n'ont trait qu'à la communication entre deux objets, à l'intérieur d'un objet elles n'ont pas cours.
Figure sur la visibilité entre deux objets
Les méthodes de public ou privé de l'objet A accèdent et peuvent utiliser les méthodes et les champs public de B.
Les méthodes de public ou privé de l'objet B accèdent et peuvent utiliser les méthodes et les champs public de A.
1.2 les classes
Postulons une analogie entre les objets matériels de la vie courante et les objets informatiques. Un objet de tous les jours est souvent obtenu à partir d’un moule industriel servant de modèle pour en fabriquer des milliers. Il en est de même pour les objets informatiques.
Définition
Une classe est une sorte de moule ou de matrice à partir duquel sont engendrés les objets réels qui s’appellent des instances de la classe considérée.
Remarque
En POO, programmer revient donc à décrire des classes d’objets, à caractériser leur structure et leur comportement, puis à instancier ces classes pour créer des objets réels. Un objet réel est matérialisé dans l’ordinateur par une zone de mémoire que les données et son code occupent.
Une classe est composée :
D’attributs (ou champs, ou variables d’instances).
Les attributs de la classe décrivent la structure de ses instances (les objets).
De méthodes (ou opérations de la classe).
Les méthodes décrivent les opérations qui sont applicables aux instances de la classe.
Un exemple : les étudiants
Supposons que chaque étudiant soit caractérisé par sa note en mathématiques (NoteMath) et sa note en informatique (NoteInfo). Un étudiant doit pouvoir effectuer éventuellement des opérations de calcul de ses moyennes dans ces deux matières (MoyMath, MoyInfo)et connaître sa moyenne générale calculée à partir de ces deux notes (MoyTotale).
La classe Etudiant a été créée. Elle ne possède que les attributs NoteMath et NoteInfo. Les méthodes de cette classe sont par exemple MoyMath, MoyInfo, MoyTotale.
Nous avons créé deux objets étudiants (deux instances : Julien et Claudie) de la classe Etudiant.
Nous n’opposons pas cette méthode de conception à la méthode structurée par machines abstraites. Nous la considérons plutôt comme complémentaire (en appliquant à des débutants une idée contenue dans la méthode HOOD). La méthode structurée sert à faire élaborer par l’étudiant des algorithmes classiques comme des actions sur des données. La COO permet de définir le monde de l’environnement de façon modulaire. Nous réutiliserons les algorithmes construits dans des objets afin de montrer la complémentarité des deux visions. Le principe de l'héritage
Comme je vous l'ai dit dans l'introduction, la notion d'héritage est l'un des fondements de la programmation orientée objet. Grâce à elle, nous pourrons créer des classes héritées (aussi appelées classes classes dérivées) de nos classes mères (aussi appelées classes classes de base). Nous pourrons créer autant de classes dérivées, par rapport à notre classe de base, que nous le souhaitons. De plus, nous pourrons nous servir d'une classe dérivée comme d'une classe de base pour élaborer encore une autre classe dérivée.
Reprenons l'exemple dont je vous parlais dans l'introduction. Nous allons créer une nouvelle classe, nomméeCapitale, héritée deVille. Vous vous rendrez vite compte que les objetsCapitaleauront tous les attributs et toutes les méthodes associés aux objetsVille!
class Capitale extends Ville {
}
C'est le mot cléextendsqui informe Java que la classeCapitaleest héritée deVille. Pour vous le prouver, essayez ce morceau de code dans votremain:
Capitale cap = new Capitale();
System.out.println(cap.decrisToi());
Vous devriez avoir la figure suivante en guise de rendu.
Objet Capitale
C'est bien la preuve que notre objetCapitalepossède les propriétés de notre objetVille. Les objets hérités peuvent accéder à toutes les méthodespublic(ce n'est pas tout à fait vrai… Nous le verrons avec le mot cléprotected) de leur classe mère, dont la méthodedecrisToi()dans le cas qui nous occupe.
En fait, lorsque vous déclarez une classe, si vous ne spécifiez pas de constructeur, le compilateur (le programme qui transforme vos codes sources en byte code) créera, au moment de l'interprétation, le constructeur par défaut. En revanche, dès que vous avez créé un constructeur, n'importe lequel, la JVM ne crée plus le constructeur par défaut.
Notre classeCapitalehérite de la classeVille, par conséquent, le constructeur de notre objet appelle, de façon tacite, le constructeur de la classe mère. C'est pour cela que les variables d'instance ont pu être initialisées ! Par contre, essayez ceci dans votre classe :
public class Capitale extends Ville{
public Capitale(){
this.nomVille = "toto";
}
}
Vous allez avoir une belle erreur de compilation ! Dans notre classeCapitale, nous ne pouvons pas utiliser directement les attributs de la classeVille.
Pourquoi cela ? Tout simplement parce les variables de la classeVillesont déclaréesprivate. C'est ici que le nouveau mot cléprotectedfait son entrée. En fait, seules les méthodes et les variables déclaréespublicouprotectedpeuvent être utilisées dans une classe héritée ; le compilateur rejette votre demande lorsque vous tentez d'accéder à des ressources privées d'une classe mère !
Remplacerprivateparprotecteddans la déclaration de variables ou de méthodes de la classeVilleaura pour effet de les protéger des utilisateurs de la classe tout en permettant aux objets enfants d'y accéder. Donc, une fois les variables et méthodes privées de la classe mère déclarées enprotected, notre objetCapitaleaura accès à celles-ci ! Ainsi, voici la déclaration de nos variables dans notre classeVillerevue et corrigée :
public class Ville {
public static int nbreInstances = 0;
protected static int nbreInstancesBis = 0;
protected String nomVille;
protected String nomPays;
protected int nbreHabitants;
protected char categorie;
//Tout le reste est identique.
}
Notons un point important avant de continuer. Contrairement au C++, Java ne gère pas les héritages multiples : une classe dérivée (aussi appelée classe fille) ne peut hériter que d'une seule classe mère ! Vous n'aurez donc jamais ce genre de classe :
class AgrafeuseBionique extends AgrafeuseAirComprime, AgrafeuseManuelle{
}
La raison est toute simple : si nous admettons que nos classesAgrafeuseAirComprimeetAgrafeuseManuelleont toutes les deux une méthodeagrafer()et que vous ne redéfinissez pas cette méthode dans l'objetAgrafeuseBionique, la JVM ne saura pas quelle méthode utiliser et, plutôt que de forcer le programmeur à gérer les cas d'erreur, les concepteurs du langage ont préféré interdire l'héritage multiple.
À présent, continuons la construction de notre objet hérité : nous allons agrémenter notre classeCapitale. Comme je vous l'avais dit, ce qui différenciera nos objetsCapitalede nos objetsVillesera la présence d'un nouveau champ : le nom d'un monument. Cela implique que nous devons créer un constructeur par défaut et un constructeur d'initialisation pour notre objetCapitale.
Avant de foncer tête baissée, il faut que vous sachiez que nous pouvons faire appel aux variables de la classe mère dans nos constructeurs grâce au mot clé super. Cela aura pour effet de récupérer les éléments de l'objet de base, et de les envoyer à notre objet hérité. Démonstration :
class Capitale extends Ville {
private String monument;
//Constructeur par défaut
public Capitale(){
//Ce mot clé appelle le constructeur de la classe mère
super();
monument = "aucun";
}
}
Si vous essayez à nouveau le petit exemple que je vous avais montré un peu plus haut, vous vous apercevrez que le constructeur par défaut fonctionne toujours… Et pour cause : ici, super()appelle le constructeur par défaut de l'objetVilledans le constructeur de Capitale. Nous avons ensuite ajouté un monument par défaut.
Cependant, la méthode de crisToi()ne prend pas en compte le nom d'un monument. Eh bien le mot clésuper()fonctionne aussi pour les méthodes de classe, ce qui nous donne une méthodedecrisToi()un peu différente, car nous allons lui ajouter le champmonumentpour notre description :
class Capitale extends Ville {
private String monument;
public Capitale(){
//Ce mot clé appelle le constructeur de la classe mère
super();
monument = "aucun";
}
public String decrisToi(){
String str = super.decrisToi() + "\n \t ==>>" + this.monument+ " en est un monument";
System.out.println("Invocation de super.decrisToi()");
return str;
}
}
Si vous relancez les instructions présentes dans lemaindepuis le début, vous obtiendrez quelque chose comme sur la figure suivante.
Utilisation de super
Utilisation de super
J'ai ajouté les instructionsSystem.out.printlnafin de bien vous montrer comment les choses se passent.
Bon, d'accord : nous n'avons toujours pas fait le constructeur d'initialisation deCapitale. Eh bien ? Qu'attendons-nous ?
public class Capitale extends Ville {
private String monument;
//Constructeur par défaut
public Capitale(){
//Ce mot clé appelle le constructeur de la classe mère
super();
monument = "aucun";
}
//Constructeur d'initialisation de capitale
public Capitale(String nom, int hab, String pays, String monument){
super(nom, hab, pays);
this.monument = monument;
}
/**
* Description d'une capitale
* @return String retourne la description de l'objet
*/
public String decrisToi(){
String str = super.decrisToi() + "\n \t ==>>" + this.monument + "en est un monument";
return str;
}
/**
* @return le nom du monument
*/
public String getMonument() {
return monument;
}
//Définit le nom du monument
public void setMonument(String monument) {
this.monument = monument;
}
}
Les commentaires que vous pouvez voir sont ce que l'on appelle des commentaires JavaDoc (souvenez-vous, je vous en ai parlé dans le tout premier chapitre de ce cours) : ils permettent de créer une documentation pour votre code. Vous pouvez faire le test avec Eclipse en allant dans le menuProject/Generate JavaDoc.
Dans le constructeur d'initialisation de notreCapitale, vous remarquez la présence de super(nom, hab, pays);. Cette ligne de code joue le même rôle que celui que nous avons précédemment vu avec le constructeur par défaut. Sauf qu'ici, le constructeur auquel super fait référence prend trois paramètres : ainsi, super doit prendre ces paramètres. Si vous ne lui mettez aucun paramètre, super()renverra le constructeur par défaut de la classeVille.
Testez le code ci-dessous, il aura pour résultat la figure suivante.
Capitale cap = new Capitale("Paris", 654987, "France", "la tour Eiffel");
System.out.println("\n"+cap.decrisToi());
Classe Capitale avec constructeur
Classe Capitale avec constructeur
Je vais vous interpeller une fois de plus : vous venez de faire de la méthode decrisToi() une méthode polymorphe, ce qui nous conduit sans détour à ce qui suit.
Le polymorphisme
Voici encore un des concepts fondamentaux de la programmation orientée objet : le polymorphisme. Ce concept complète parfaitement celui de l'héritage, et vous allez voir que le polymorphisme est plus simple qu'il n'y paraît. Pour faire court, nous pouvons le définir en disant qu'il permet de manipuler des objets sans vraiment connaître leur type.
Dans notre exemple, vous avez vu qu'il suffisait d'utiliser la méthodedecrisToi()sur un objetVilleou sur un objetCapitale. On pourrait construire un tableau d'objets et appelerdecrisToi()sans se soucier de son contenu : villes, capitales, ou les deux.
D'ailleurs, nous allons le faire. Essayez ce code :
//Définition d'un tableau de villes null
Ville[] tableau = new Ville[6];
//Définition d'un tableau de noms de villes et un autre de nombres d'habitants
String[] tab = {"Marseille", "lille", "caen", "lyon", "paris", "nantes"};
int[] tab2 = {123456, 78456, 654987, 75832165, 1594, 213};
//Les trois premiers éléments du tableau seront des villes,
//et le reste, des capitales
for(int i = 0; i < 6; i++){
if (i <3){
Ville V = new Ville(tab[i], tab2[i], "france");
tableau[i] = V;
}
else{
Capitale C = new Capitale(tab[i], tab2[i], "france", "la tour Eiffel");
tableau[i] = C;
}
}
//Il ne nous reste plus qu'à décrire tout notre tableau !
for(Ville V : tableau){
System.out.println(V.decrisToi()+"\n");
}
La figure suivante vous montre le résultat.
Test de polymorphisme
Test de polymorphisme
Nous créons un tableau de villes contenant des villes et des capitales (nous avons le droit de faire ça, car les objetsCapitalesont aussi des objetsVille) grâce à notre première bouclefor. Dans la seconde, nous affichons la description de ces objets… et vous voyez que la méthode polymorphedecrisToi()fait bien son travail !
Vous aurez sans doute remarqué que je n'utilise que des objetsVille dans ma boucle : on appelle ceci la covariance des variables ! Cela signifie qu'une variable objet peut contenir un objet qui hérite du type de cette variable. Dans notre cas, un objet de typeVille peut contenir un objet de typeCapitale. Dans ce cas, on dit que Ville est la superclasse de Capitale. La covariance est efficace dans le cas où la classe héritant redéfinit certaines méthodes de sa superclasse.
Attention à ne pas confondre la surcharge de méthode avec une méthode polymorphe.
Une méthode surchargée diffère de la méthode originale par le nombre ou le type des paramètres qu'elle prend en entrée.
Une méthode polymorphe a un squelette identique à la méthode de base, mais traite les choses différemment. Cette méthode se trouve dans une autre classe et donc, par extension, dans une autre instance de cette autre classe.
Vous devez savoir encore une chose sur l'héritage. Lorsque vous créez une classe (Ville, par exemple), celle-ci hérite, de façon tacite, de la classeObjectprésente dans Java.
Toutes nos classes héritent donc des méthodes de la classeObject, commeequals()qui prend un objet en paramètre et qui permet de tester l'égalité d'objets. Vous vous en êtes d'ailleurs servis pour tester l'égalité deString()dans la première partie de ce livre.
Donc, en redéfinissant une méthode de la classeObjectdans la classeVille, nous pourrions utiliser la covariance.
La méthode de la classeObjectla plus souvent redéfinie esttoString(): elle retourne unStringdécrivant l'objet en question (comme notre méthodedecrisToi()). Nous allons donc copier la procédure de la méthodedecrisToi()dans une nouvelle méthode de la classeVille:toString(). Voici son code :
public String toString(){
return "\t"+this.nomVille+" est une ville de "+this.nomPays+", elle comporte : "+this.nbreHabitants+" => elle est donc de catégorie : "+this.categorie;
}
Nous faisons de même dans la classeCapitale:
public String toString(){
String str = super.toString() + "\n \t ==>>" + this.monument + " en est un monument";
return str;
}
Maintenant, testez ce code :
//Définition d'un tableau de villes null
Ville[] tableau = new Ville[6];
//Définition d'un tableau de noms de Villes et un autre de nombres d'habitants
String[] tab = {"Marseille", "lille", "caen", "lyon", "paris", "nantes"};
int[] tab2 = {123456, 78456, 654987, 75832165, 1594, 213};
//Les trois premiers éléments du tableau seront des Villes
//et le reste des capitales
for(int i = 0; i < 6; i++){
if (i <3){
Ville V = new Ville(tab[i], tab2[i], "france");
tableau[i] = V;
}
else{
Capitale C = new Capitale(tab[i], tab2[i], "france", "la tour Eiffel");
tableau[i] = C;
}
}
//Il ne nous reste plus qu'à décrire tout notre tableau !
for(Object obj : tableau){
System.out.println(obj.toString()+"\n");
}
Vous pouvez constater qu'il fait exactement la même chose que le code précédent ; nous n'avons pas à nous soucier du type d'objet pour afficher sa description. Je pense que vous commencez à entrevoir la puissance de Java !
Attention : si vous ne redéfinissez pas ou ne « polymorphez » pas la méthode d'une classe mère dans une classe fille (exemple detoString()), à l'appel de celle-ci avec un objet fille, c'est la méthode de la classe mère qui sera invoquée !
Une précision s'impose : si vous avez un objetvde typeVille, par exemple, que vous n'avez pas redéfini la méthodetoString()et que vous testez ce code :
System.out.println(v);
… vous appellerez automatiquement la méthode toString()de la classeObject! Mais ici, comme vous avez redéfini la méthode toString()dans votre classeVille, ces deux instructions sont équivalentes :
System.out.println(v.toString());
//Est équivalent à
System.out.println(v);
Pour plus de clarté, je conserverai la première syntaxe, mais il est utile de connaître cette alternative.
Pour clarifier un peu tout ça, vous avez accès aux méthodes public et protected de la classe Object dès que vous créez une classe objet (grâce à l'héritage tacite). Vous pouvez donc utiliser lesdites méthodes ; mais si vous ne les redéfinissez pas, l'invocation se fera sur la classe mère avec les traitements de la classe mère.
Si vous voulez un exemple concret de ce que je viens de vous dire, vous n'avez qu'à retirer la méthode toString() dans les classesVilleetCapitale: vous verrez que le code de la méthode main fonctionne toujours, mais que le résultat n'est plus du tout pareil, car à l'appel de la méthodetoString(), la JVM va regarder si celle-ci existe dans la classe appelante et, comme elle ne la trouve pas, elle remonte dans la hiérarchie jusqu'à arriver à la classeObject…
Vous devez savoir qu'une méthode n'est « invocable » par un objet que si celui-ci définit ladite méthode.
Ainsi, ce code ne fonctionne pas :
public class Sdz1 {
public static void main(String[] args){
Ville[] tableau = new Ville[6];
String[] tab = {"Marseille", "lille", "caen", "lyon", "paris", "nantes"};
int[] tab2 = {123456, 78456, 654987, 75832165, 1594, 213};
for(int i = 0; i < 6; i++){
if (i <3){
Ville V = new Ville(tab[i], tab2[i], "france");
tableau[i] = V;
}
else{
Capitale C = new Capitale(tab[i], tab2[i], "france", "la tour Eiffel");
tableau[i] = C;
}
}
//Il ne nous reste plus qu'à décrire tout notre tableau !
for(Object v : tableau){
System.out.println(v.decrisToi()+"\n");
} }}
Pour qu'il fonctionne, vous devez dire à la JVM que la référence de typeObjectest en fait une référence de typeVille, comme ceci :((Ville)v).decrisToi();. Vous transtypez la référencevenVillepar cette syntaxe. Ici, l'ordre des opérations s'effectue comme ceci :
vous transtypez la référencevenVille;
vous appliquez la méthodedecrisToi()à la référence appelante, c'est-à-dire, ici, une référenceObjectchangée enVille.
Vous voyez donc l'intérêt des méthodes polymorphes : grâce à elles, vous n'avez plus à vous soucier du type de variable appelante. Cependant, n'utilisez le typeObjectqu'avec parcimonie.
Il y a deux autres méthodes qui sont très souvent redéfinies :
public boolean equals(Object o), qui permet de vérifier si un objet est égal à un autre ;
public int hashCode(), qui attribue un code de hashage à un objet. En gros, elle donne un identifiant à un objet. Notez que cet identifiant sert plus à catégoriser votre objet qu'à l'identifier formellement.
Il faut garder en tête que ce n'est pas parce que deux objets ont un même code de hashage qu'ils sont égaux (en effet, deux objets peuvent avoir la même « catégorie » et être différents…) ; par contre, deux objets égaux ont forcément le même code de hashage ! En fait, la méthodehashcode() est utilisée par certains objets (que nous verrons avec les collections) afin de pouvoir classer les objets entre eux.
La bonne nouvelle, c'est qu'Eclipse vous permet de générer automatiquement ces deux méthodes, via le menuSource/Generate hashcode and equals. Voilà à quoi pourraient ressembler ces deux méthodes pour notre objetVille.