TP de Java
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
...
part of the document
TP de Java
Héritage et Polymorphisme
_____________________
Sujets abordés dans ce TP :
Mise en uvre de lhéritage
Construction dobjets dérivés
Redéfinition des données et méthodes
Compatibilité ascendante, instanceof
Ligature dynamique
Classes abstraites et interfaces
Chargement dynamique de pile
1) Introduction
Dans ce TP nous vous proposons de développer les concepts P.O.O en Java : lhéritage et le polymorphisme. On rappelle que la programmation Java est basée sur la manipulation dobjets composés de données membres et de méthodes. Les objets sont des instances de classes correspondant à des descriptions densembles dobjets ayant des structures de données communes et disposant des mêmes méthodes. Ces classes se définissent par des relations dhé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 lhéritage
Une relation dhéritage entre classes se définit en Java à laide du mot clé extends. Le code suivant donne un exemple dhéritage entre deux classes « vides » :
class I1 {}
class I2 extends I1 {}
En Java, lhéritage multiple nest pas permis : une classe ne peut hériter que dune seule autre classe tout au plus. Evidemment, lintérêt de lhé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 daccès entre les classes mères et les classes dérivées.
En Java, il existe en fait quatre déclarations de droit daccè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 dhé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 daccès aux données membres et méthodes via des relations dhéritage.
private : Dans cet exemple, la donnée membre u a été déclarée à laide du mot clé private dans la classe A1. Ceci signifie quelle est accessible uniquement par les méthodes de la classe A1. La relation dhéritage entre les classes A1 et A2 ne lève en rien lencapsulation 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 labsence 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 à lextérieure des classes. On peut en effet les invoquer à partir dun objet instance et de lopérateur ., comme par exemple : my.v, my.print().
Construction dobjets dérivés
Un objet donné, instance dune 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 dun objet dérivé, il y a donc construction des objets pères associés à cet objet dérivé par les différentes relations dhé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 dhéritage de cet exemple, que pouvez vous conclure sur le processus de construction des objets dérivés en ce qui concerne lordre de la construction. On se propose de vérifier cet ordre en ce qui concerne la construction et linitialisation des données membres dune 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 lobjet père. Cette prise en charge est assurée par lappel des constructeurs de lobjet 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 dhéritage. Que pouvez vous conclure sur la prise en charge de la construction de lobjet père via le mot clé super.
Redéfinition des données membres et des méthodes, surcharge de méthodes
Lorsque quune classe dérivée déclare des méthodes et des données membres de même signature que celles dune 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 dhé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 daccè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.update1();
my.update2();
my.ic();
my.update1();
my.update2();
Java permet dinterdire la redéfinition de méthodes et de données membres dans les classes dérivées à laide du mot clé final. Implémenter les classes suivantes, relever et commenter les erreurs de la compilation de ce code.
class RF1 {
final int i=0;
final void ic() {}
}
class RF2 extends RF1{
char i='a';
void ic() {i++;}
}
3) Polymorphisme
Introduction, compatibilité ascendante, instanceof
Le polymorphisme est un des concepts important de la P.O.O, fortement lié au concept dhéritage. On peut caractériser le polymorphisme en disant quil 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 :
SHAPE \* MERGEFORMAT
Tout objet en Java hérite implicitement de classe racine Object, vous pouvez donc reconduire lexemple 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 lutilisation 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é lallocation dun objet de type DL2 affecté à une référence de type DL1, cest 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é lallocation dun objet de type DS3 affecté à une référence de type DS2, cest 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 lensemble. 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, à lexécution le choix se reporte sur la méthode print() de la classe DS3. Cet exemple illustre ce que lon nomme la ligature dynamique, la règle suivante en résume les concepts:
SHAPE \* MERGEFORMAT
Classes abstraites
En P.O.O une classe abstraite est une classe qui ne permet pas dinstancier dobjets, elle ne peut servir que de classe de base pour une dérivation. En Java une classe abstraite se déclare à laide 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 quune 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 dune classe non abstraite (ici AbImpl1 et AbImpl2) dérivant de votre classe abstraite.
Lexemple précédent ne présente que peut dintérêt sur lutilisation de classes mères abstraites par rapport aux classes mères non abstraites. Lintérêt des classes abstraites réside dans la possibilité de définir des méthodes abstraites. Implémenter les classes suivantes :
abstract class Co {
int a = 1;
abstract void print(int b);
}
class CoImpl1 extends Co {
void print(int b)
{System.out.println(a+b);}
}
class CoImpl2 extends Co {
void print(int b)
{System.out.println(a-b);}
}
Les mettre en oeuvre de la façon suivante :
Co c1,c2,c3;
c1 = new CoImpl1();c1.print(1);
c2 = new CoImpl2();c2.print(1);
Dans cet exemple, vous venez de définir votre première méthode abstraite print(int a). Dans la classe abstraite, cette méthode est définie à laide du mot abstract sans corps de fonction. Elle est redéfinie par la suite dans les classes dérivées CoImpl1 et CoImpl2.
La définition de ces méthodes abstraites dans la classe mère correspond à un contrat dimplémentation des méthodes pour les classes dérivées. Ce contrat doit être respecté par les classes dérivées, elles se doivent obligatoirement de redéfinir les méthodes abstraites.
Cest cette certitude de la présence des méthodes abstraites redéfinies qui permet dexploiter le polymorphisme. Dans lexemple précédent vous manipulez des objets de type Co, en faisant appel à la méthode print(int a). Vous êtes en effet sûr que cette méthode est redéfinie dans vos objets dérivés, avec un comportement propre à chacune de ces objets (ici addition pour CoImpl1 et soustraction pour CoImpl2).
Vous pouvez également définir des relations dhéritage entre classes abstraites, et entre classes abstraites et non abstraites. Implémenter et mettre en oeuvre le code suivant, commenter :
class Ha1 {
void f() {}
}
abstract class Ha2 extends Ha1 {
abstract void g();
}
abstract class Ha3 extends Ha2{
abstract void h();
}
class Ha4 extends Ha3{
void g() {}
void h() {}
}
Interfaces
Une interface peut être considérée comme une classe abstraite nimplémentant que des méthodes abstraites. Cependant, la notion dinterface est plus riche quun simple cas particulier de classe abstraite, en effet :
Une classe peut implémenter plusieurs interfaces
Une classe peut étendre une autre classe tout en implémentant des interfaces
Les interfaces peuvent se dériver
Les variables peuvent être de type interface
Implémenter les interfaces et classes suivantes :
interface In1 {
/* public abstract*/ void f();
}
interface In2 {
/* public abstract*/ void g();
}
class Impl1 implements In1 {
public void f() {
System.out.println("In1");
}
}
class Impl2 implements In1,In2 {
public void f() {
System.out.println("In1");
}
public void g() {
System.out.println("In2");
}
}
class In3 {
public void h() {
System.out.println("In3");
}
}
class Impl3 extends In3 implements In1,In2 {
public void f() {
System.out.println("In1");
}
public void g() {
System.out.println("In2");
}
}
Les mettre en uvre de la façon suivante :
Impl1 mya = new Impl1(); mya.f();
Impl2 myb = new Impl2(); myb.f(); myb.g();
Impl3 myc = new Impl3(); myc.f(); myc.g(); myc.h();
Dans cet exemple, vous avez mis en oeuvre vos premières interfaces In1 et In2. Il est possible dimplémenter ces interfaces individuellement (Impl1), ou simultanément (Impl2). De même, ces interfaces peuvent être utilisées en complément dun héritage (Impl3).
Dans cet exemple nous avons placé en commentaire les instructions public abstract. Dans une interface, toutes les méthodes sont implicitement déclarées de cette manière, il nest donc pas nécessaire de re-préciser cette déclaration. La partie public de cette déclaration implicite impose la déclaration public des redéfinitions des méthodes des interfaces dans les classes les implémentant (public f(), public g()). Nous admettrons cette déclaration public dans ce TP, elle sera abordée plus en détails dans les TP suivants.
Compléter la mise en oeuvre de la façon suivante :
In1 myd = new Impl1(); myd.f();
In1 mye = new Impl2(); mye.f();
In2 myf = new Impl2(); myf.g();
Cet exemple illustre la manipulation de variables de type interface pour le polymorphisme. Vous pouvez manipuler nimporte quelle instance de classes différentes (Impl1, Impl2) via des variables dun type interface (In1), si cette interface est implémentée par les classes (Impl1, Impl2). Egalement, différentes instances dune même classe (Impl2) peuvent être manipulées via des variables de type interface différentes (In1, In2), si ces interfaces sont implémentées par la classe (Impl2).
Vous pouvez également définir des relations dhéritage entre interfaces, implémenter les interfaces suivantes :
interface Ii1 {
void f();
}
interface Ii2 {
void g();
}
interface Ii3 extends Ii2{
void h();
}
Mettre en uvre ces interfaces dans les différents cas dusage suivant, commenter :
implémente A, héritage
class Ig1 implements Ii1 {
public void f() {System.out.println("Ig1");}
}
class Ig2 extends Ig1 {}
implémente A, héritage, implémente B
class Ig3 implements Ii1 {
public void f() {System.out.println("Ig3");}
}
class Ig4 extends Ig3 implements Ii2 {
public void g() {System.out.println("Ig4");}
}
implémente A, héritage, implémente A
class Ig5 implements Ii1 {
public void f() {System.out.println("Ig5");}
}
class Ig6 extends Ig5 implements Ii1 {
public void f() {System.out.println("Ig6");}
}
implémente A, héritage, implémente AB
class Ig7 implements Ii2 {
public void g() {System.out.println("Ig7");}
}
class Ig8 extends Ig7 implements Ii3 {
public void g() {System.out.println("Ig8");}
public void h() {System.out.println("Ig8");}
}
implémente AB, héritage, implémente A
class Ig9 implements Ii3 {
public void g() {System.out.println("Ig9");}
public void h() {System.out.println("Ig9");}
}
class Ig10 extends Ig9 implements Ii2 {
public void g() {System.out.println("Ig10");}
}
4) Chargement dynamique de pile
Dans cette partie, on se propose de mettre en uvre les notions abordées dans ce TP pour limplémentation dun chargement dynamique de pile. La pile est une structure de données fonctionnant à la manière dun casier vide à dossier. Les dossiers sentassent les uns par-dessus les autres dans ce casier. A chaque fois quun dossier est traité, il est pris sur le dessus de la pile. Deux opérations élémentaires push() et pop() sont alors utilisées. La première pour lajout dune donnée, la seconde pour la suppression. On parle également de stratégie LIFO «Last In First Out ».
On se propose de mettre en oeuvre un chargement dynamique de pile pour des données graphiques. Ces données hériteront de la classe abstraite Graphics suivante :
abstract class Graphics {
abstract void read(BufferedReader flow) throws Exception;
abstract void write(BufferedWriter flow) throws Exception;
String getName() {return getClass().getName();}
void id() {System.out.println(getName());}
abstract void print();
}
A partir de cette classe abstraite, implémenter des classes Point, Line, Quadrilateral. La classe Point aura pour données membres un couple dentier x et y. Les classes Line et Quadrilateral exploiteront le principe dagrégation en P.O.O, elles seront composées respectivement de 2 objets points, et de 2 objets lignes. Dans une première étape, limitez-vous à linitialisation des données membres, et limplémentation des fonctions print() dans vos classes graphiques pour laffichage des données membres.
Pour limplémentation de la pile, nous vous proposons détendre une classe MyStack de la classe Stack existante du package java.util. Le listing suivant donne un squelette dimplémentation de cette classe. Aller consulter lAPI specification de cette classe afin de connaître les fonctionnalisées récupérées dans votre classe MyStack.
import java.io.*;
import java.net.*;
import java.util.*;
class MyStack extends Stack{
private ClassLoader loader;
MyStack() {
loader = new URLClassLoader(urlCreate());
}
private URL[] urlCreate() {
URL[] urlT = new URL[1];
try
{urlT[0] = new URL("file://");}
catch(MalformedURLException e)
{System.out.println("MalformedURLException");}
return urlT;
}
Object load(String gName) {
Object o = null;
try
{o = loader.loadClass(gName).newInstance();}
catch(ClassNotFoundException e)
{System.out.println("ClassNotFoundException");}
catch(InstantiationException e)
{System.out.println("InstantiationException");}
catch(IllegalAccessException e)
{System.out.println("IllegalAccessException");}
return o;
}
void read(String fName) throws Exception {
BufferedReader flow
= new BufferedReader (new FileReader(fName)) ;
String gName;
do
{
gName = flow.readLine() ;
Object g = load(gName)
/* ???? */
}
while (gName != null) ;
flow.close ();
}
void write(String fName) throws Exception {
BufferedWriter flow
= new BufferedWriter (new FileWriter(fName)) ;
flow.write("result");
/* ???? */
}
}
Dans ce squelette de classe, les méthodes urlCreate() et MyStack() sont utilisées pour linstanciation de lobjet loader et la création de la pile. Cet objet loader est utilisé dans la méthode load() pour le chargement dynamique des objets dans la méthode read(). Nous admettrons la définition de ces méthodes dans ce TP, vous pouvez consulter lAPI specification pour plus de détails sur la classe ClassLoader. Mettre en oeuvre votre pile pour la lecture et le chargement dynamique de classe à partir du fichier « base.txt » suivant :
Point
Line
Quadrilateral
Lintérêt du polymorphisme dans cet exemple réside dans la déportation des fonctionnalités de lecture au sein de chaque objet graphique à partir des fonctions read(). Redéfinir vos classes Point, Line, et Quadrilateral de façon à permettre la lecture du fichier « base.txt » suivant, compléter la méthode read() de la classe MyStack de façon à stoker les objets graphiques lus dans la pile.
Point
10
15
Line
10
20
20
20
Quadrilateral
30
40
40
40
30
50
40
50
De manière similaire, compléter les méthodes write() de vos objets graphiques. Complétez la méthode write() de la classe MyStack de façon à dé-stocker les objets graphiques de votre pile. Dans votre programme principal, remaniez votre pile de façon à la modifier en écriture comme par exemple. Illustrer le principe LIFO de la pile en commentant le résultat de cet exemple.
MyStack s = new MyStack();
s.read("base.txt");
s.pop();
s.push(new Point());
s.push(new Quadrilateral());
s.write("resu.txt");
PAGE
PAGE 1
Ligature dynamique : Dans un appel de la forme x.f() x est déclaré et supposé de classe Tn. Cette déclaration est telle que {T0
Tn
Tv} avec pour classes mères ascendantes de Tn les classes {T0
Tn-1 }, et pour classes filles descendantes de Tn les classes { Tn+1
Tv }. Dans une telle déclaration le choix de f() est déterminé ainsi :
à la compilation : on détermine dans {T0
Tn } la meilleure signature de f()
à lexécution : on détermine dans {Tn
Tv } la meilleure signature de f()
Comptabilité ascendante : Il existe une conversion implicite dune référence à un objet de classe T en une référence dune classe ascendante de T.