Héritage et Polymorphisme

Héritage et Polymorphisme.

Dans ce TP nous vous proposons de développer les concepts P.O.O en Java : l’héritage et le polymorphisme. On rappelle que la programmation Java est basée sur la manipulation d’objets composés de données membres et de méthodes. Les objets sont des instances de classes correspondant à des descriptions d’ensembles d’objets ayant des structures de données communes et disposant des mêmes méthodes. Ces classes se définissent par des relations d’héritage, toute classe Java hérite implicitement de la classe racine Object. De même, ces différentes classes sont structurées sous forme de packages correspondant à un regroupement de classes, sous un identificateur commun (correspondant au nom du package). 2) Héritage Mise en œuvre de l’héritage Une relation d’héritage entre classes se définit en Java à l’aide du mot clé extends. Le code suivant donne un exemple d’héritage entre deux classes « vides » : class I1 {} class I2 extends I1 {} En Java, l’héritage multiple n’est pas permis : une classe ne peut hériter que d’une seule autre classe tout au plus. Evidemment, l’intérêt de l’héritage en P.O.O réside dans la re-exploitation des données membres et des méthodes de la classe mère dans la classe dérivée. Cette re-exploitation est le plus souvent contrôlée par des droits d’accès entre les classes mères et les classes dérivées. En Java, il existe en fait quatre déclarations de droit d’accès définissables dans une classe pour les données membres et les méthodes : private, pas de déclaration, protected, et public. Les déclarations protected et public concernent « majoritairement » la définition de package, elles seront abordées plus en détails dans les TP suivants. En ce qui concerne les relations d’héritage, seules les déclarations private et pas de déclaration sont utilisées. Implémenter le code suivant : class A1{ private int u; int v; void set1(int u) {setB(); this.u = u;} void set1(int u, int v) {setB(); this.u = u; this.v = v;} int get1() {return u;} private int back; private void setB() {back = u;} void undo() {u = back;} void print1() {System.out.println(u+ »; »+v);} } class A2 extends A1 { private int w; void set2(int u, int v, int w) {set1(u); this.v = v; this.w = w;} int get2() {return w;} void print2() {System.out.println(get1()+ »; »+v+ »; »+w);} } class A3 extends A2{ void print3() {System.out.println(get1()+ »; »+v+ »; »+get2());} } Le mettre en œuvre de la façon suivante : A2 my2 = new A2(); my2.set2(1,2,3);my2.set2(4,5,6); my2.v = 8; my2.undo(); my2.print2(); A3 my3 = new A3(); my3.set1(7); my3.v = 8; my3.print3(); Cet exemple met en œuvre les droits d’accès aux données membres et méthodes via des relations d’héritage. • private : Dans cet exemple, la donnée membre u a été déclarée à l’aide du mot clé private dans la classe A1. Ceci signifie qu’elle est accessible uniquement par les méthodes de la classe A1. La relation d’héritage entre les classes A1 et A2 ne lève en rien l’encapsulation de la donnée membre u dans la classe A2. Il en est de même pour la méthode setB() de la classe A1, elle est uniquement accessible par les méthodes de la classe A1. Ce constat est également vrai en ce qui concerne les classes A1 et A3, et A2 et A3. • pas de déclaration : En l’absence de déclaration les méthodes et données membres de la classe A1 sont directement accessibles par la classe dérivée A2 : v, get1(). De même, cette propriété se reconduit entre les classes A1 et A3 (v, get1()), et A2 et A3(v, get1(),get2()). Ces données membres et méthodes sont également accessibles à l’extérieure des classes. On peut en effet les invoquer à partir d’un objet instance et de l’opérateur ., comme par exemple : my.v, my.print().

Construction d’objets dérivés.

Un objet donné, instance d’une classe dérivée peut donc exploiter les données membres et les méthodes définies dans la classe mère de cette classe dérivée. Ceci induit que cet objet instance de la classe dérivée est lié à un objet instance de la classe mère. Durant la construction d’un objet dérivé, il y a donc construction des objets pères associés à cet objet dérivé par les différentes relations d’héritage entre classes. On se propose d’étudier ici la construction des objets dérivés, implémenter et mettre en œuvre le code suivant : class A { A() {System.out.println(« A »);} } class B extends A { B() {System.out.println(« B »);} } class C extends A { C() {System.out.println(« C »);} } class D extends C { D() {System.out.println(« D »);} } class E extends C { E() {System.out.println(« E »);} } A travers ces différentes classes, vous avez redéfini les constructeurs par défaut. Indiquer le diagramme d’héritage de cet exemple, que pouvez vous conclure sur le processus de construction des objets dérivés en ce qui concerne l’ordre de la construction. On se propose de vérifier cet ordre en ce qui concerne la construction et l’initialisation des données membres d’une classe. Implémenter les classes suivantes, les mettre en œuvre, commenter : class V1 { int v; int v1=1; V1() {print1();v=v1;print1();} void print1() {System.out.println(v+ »; »+v1+ »;? »);} } class V2 extends V1 { int v2=2; V2() {print2();v=v2;print2();} void print2() {System.out.println(v+ »; »+v1+ »; »+v2);} } En Java, un objet dérivé doit impérativement prendre en charge la construction de l’objet père. Cette prise en charge est assurée par l’appel des constructeurs de l’objet père via le mot clé super. Implémenter et mettre en œuvre le code suivant : class S1 { int v; S1() {v=1;} S1(boolean t) {v=2;} void print() {System.out.println(v);} } class S2 extends S1 { S2() {print();} } class S3 extends S1 { S3() {super(true);print();} } Dans cet exemple, vous avez redéfini dans la classe S1 le constructeur par défaut. Vous pouvez voir dans la mise en œuvre de la classe S2 que ce constructeur par défaut est également appelé par défaut dans une relation d’héritage. Que pouvez vous conclure sur la prise en charge de la construction de l’objet père via le mot clé super. Redéfinition des données membres et des méthodes, surcharge de méthodes Lorsque qu’une classe dérivée déclare des méthodes et des données membres de même signature que celles d’une de ses classes mères, on dit que la classe dérivée redéfinit les données membres et les méthodes. Cette signature correspond en ce qui concerne les données membres à {nomVariable}, et en ce qui concerne les méthodes {nomMéthode, arguments effectifs}. Implémenter le code suivant : class R1{ int u; char v=’b’; void set(int u) {this.u = u;} void print() {System.out.println(v);} } class R2 extends R1{ char u=’a’; void set(int u) {System.out.println(« R2 class »);} void print(char w) {System.out.println(u+ », »+v+ », »+w);} } Le mettre en œuvre de la façon suivante : R1 my1 = new R1(); my1.set(2); System.out.println(my1.u); R2 my2 = new R2(); my2.set(2); System.out.println(my2.u); my2.print(); my2.print(‘c’); A travers cet exemple, vous pouvez voir que la donnée membre u et la méthode set(int) ont été redéfinies de la classe R1 à la classe R2. De même, vu que la classe dérivée hérite des méthodes de la classe mère la notion de surcharge de méthodes reste vraie dans une relation d’héritage. Par exemple la méthode print(char) de la classe R2 correspond à la méthode surchargée print() de la classe R1. La redéfinition des données membres et des méthodes dans la classe dérivée est possible de par leur autorisation d’accès par la classe mère. Dans le cas où ces données membres et ces méthodes sont déclarées privées, leur déclaration est ignorée par la classe dérivée. Implémenter le code suivant : class RP1{ private int i=0; private void ic() {i++;} void update1() {System.out.println(i);ic();} } class RP2 extends RP1{ char i=’a’; void ic() {i++;} void update2() {System.out.println(i);ic();} } Le mettre en oeuvre de la façon suivante, commenter : RP2 my = new RP2(); my.update.

Le polymorphisme est un des concepts important de la P.O.O, fortement lié au concept d’héritage. On peut caractériser le polymorphisme en disant qu’il permet de manipuler des objets sans en connaître (tout à fait) le type. Implémenter les classes suivantes : class P1 { void pp1() {System.out.println(« P1 class »);} } class P2 extends P1 { void pp2() {System.out.println(« P2 class »);} } Mettre en œuvre ces classes de la façon suivante : P1 my = new P2(); my.pp1(); ((P2)my).pp2(); A travers ce court exemple vous avez vu le premier concept du polymorphisme : la compatibilité ascendante. En effet, vous avez manipulé un objet de type P2 via une référence de type P1 correspondant à la classe mère de P2. La règle suivante résume le concept de compatibilité ascendante : Tout objet en Java hérite implicitement de classe racine Object, vous pouvez donc reconduire l’exemple précédent, implémentez la classe suivante : class MyOb { void pp() {System.out.println(« MyOb class »);} } Mettre en œuvre cette classe de la façon suivante, commenter : Object my2 = new MyOb(); ((MyOb)my2).pp(); Java permet l’utilisation de opérateur instanceof pour la vérification de type, mettre en œuvre votre classe MyOb de la façon suivante, commenter Object my = new MyOb(); if(my instanceof MyOb) System.out.println(« Ok »); Ligature dynamique Dans la partie héritage de ce TP nous avons présenté les concepts de redéfinition de méthodes. La prise en compte du polymorphisme va compliquer la redéfinition, implémenter les classes suivantes class DL1{ void print() {System.out.println(« DL1 class »);} } class DL2 extends DL1{ void print() {System.out.println(« DL3 class »);} } Les mettre en oeuvre de la façon suivante : DL1 my1 = new DL1(); my1.print(); DL1 my2 = new DL2(); my2.print(); A travers cet exemple vous voyez que malgré l’allocation d’un objet de type DL2 affecté à une référence de type DL1, c’est bien la méthode print() de DL2 qui est appelée. Nous vous proposons de développer cet aspect, implémenter les classes suivantes : class DS1{ void print() {System.out.println(« DS1 »);} } class DS2 extends DS1{} class DS3 extends DS2{ void print() {System.out.println(« DS3 »);} } Les mettre en oeuvre de la façon suivante : DS2 myc = new DS3(); myc.print(); Cet exemple est similaire au précédent, malgré l’allocation d’un objet de type DS3 affecté à une référence de type DS2, c’est bien la méthode print() de DS3 qui est appelée. Reprendre cet exemple en plaçant en commentaire la méthode print() de la classe DS1 et en recompilant l’ensemble. Le compilateur vous indique une erreur de résolution concernant la méthode print(). Ceci signifie que la méthode print() initialement choisie à la compilation est celle de la classe DS1, à l’exécution le choix se reporte sur la méthode print() de la classe DS3. Cet exemple illustre ce que l’on nomme la ligature dynamique, la règle suivante en résume les concepts: Classes abstraites En P.O.O une classe abstraite est une classe qui ne permet pas d’instancier d’objets, elle ne peut servir que de classe de base pour une dérivation. En Java une classe abstraite se déclare à l’aide du mot clé abstract. Implémenter les classes suivantes : abstract class Ab { int a = 1; void print(int b) {System.out.println(a+b);} } class AbImpl1 extends Ab {} class AbImpl2 extends Ab {} Les mettre en oeuvre de la façon suivante : Ab a1,a2; //A = new Ab(); a1 = new AbImpl1(); a1.print(2); a2 = new AbImpl2(); a1.print(3); A travers cet exemple, vous venez de définir votre première classe abstraite Ab. Vous pouvez définir dans les classes abstraites différentes données membres et méthodes au même titre qu’une classe non abstraite. Vous pouvez déclarer des objets du type de votre classe abstraite dans votre programme principal (ici a1 et a2). Vous ne pouvez cependant pas directement instancier ces objets, il vous faut utiliser un constructeur d’une classe non abstraite (ici AbImpl1 et AbImpl2) dérivant de votre classe abstraite.

 

Cours gratuitTélécharger le document complet

Télécharger aussi :

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *