TP de Java

On rappelle que la programmation Java est basée sur la manipulation ... Ces classes se définissent par des relations d'héritage, toute classe Java hérite ...


un extrait du document



TP de Java
Héritage et Polymorphisme
_____________________

Sujets abordés dans ce TP :
Mise en œuvre de l’héritage
Construction d’objets 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 : 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.update1();
my.update2();
my.ic();
my.update1();
my.update2();

Java permet d’interdire la redéfinition de méthodes et de données membres dans les classes dérivées à l’aide 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 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 :

 SHAPE \* MERGEFORMAT 

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:

 SHAPE \* MERGEFORMAT 



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.

L’exemple précédent ne présente que peut d’intérêt sur l’utilisation de classes mères abstraites par rapport aux classes mères non abstraites. L’inté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 à l’aide 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 d’implé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.

C’est cette certitude de la présence des méthodes abstraites redéfinies qui permet d’exploiter le polymorphisme. Dans l’exemple 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 d’hé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 n’implémentant que des méthodes abstraites. Cependant, la notion d’interface est plus riche qu’un 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 d’implémenter ces interfaces individuellement (Impl1), ou simultanément (Impl2). De même, ces interfaces peuvent être utilisées en complément d’un 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 n’est 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 n’importe quelle instance de classes différentes (Impl1, Impl2) via des variables d’un type interface (In1), si cette interface est implémentée par les classes (Impl1, Impl2). Egalement, différentes instances d’une 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 d’hé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 d’usage 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 l’implémentation d’un chargement dynamique de pile. La pile est une structure de données fonctionnant à la manière d’un casier vide à dossier. Les dossiers s’entassent les uns par-dessus les autres dans ce casier. A chaque fois qu’un 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 l’ajout d’une 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 d’entier x et y. Les classes Line et Quadrilateral exploiteront le principe d’agré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 à l’initialisation des données membres, et l’implémentation des fonctions print() dans vos classes graphiques pour l’affichage des données membres.

Pour l’implé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 d’implémentation de cette classe. Aller consulter l’API 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 l’instanciation de l’objet 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 l’API 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

L’inté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()
à l’exécution : on détermine dans {Tn …Tv } la meilleure signature de f()

Comptabilité ascendante : Il existe une conversion implicite d’une référence à un objet de classe T en une référence d’une classe ascendante de T.