4- Hiérarchies


1 Notion fondamentale en POO : l'héritage

2 Construction et initalisation des objets dérivés

3 Redéfinition et surdéfinition de membres

4 Polymorphisme et transtypage

5 Super Classe Object

6 Classes Abstraites

7 Interface


1 Notion fondamentale en POO : l'héritage

Les classes sont généralement organisées de manière hiérarchique. Le programmeur organise les classes dont il a besoin de manière hiérarchique ou bien il se rattache aux hiérarchies de l'API.

On peut définir une classe (classe dérivée, classe fille...) à partir d'une autre (classe de base, class mère etc...). La classe dérivée hérite des attributs et des méthodes de sa classe de base, et peut ajouter des attributs et des comportements, voire redéfinir une partie des comportements de la classe mère. Le mécanisme de l'héritage est fondamental : on dispose d'un certain nombre de classes dans l'API, il est possible de définir un programme en utilisant des dérivatifs des classes de l'API et sans avoir beaucoup à coder.
On suppose disposer d'une classe Point :

public class Point
{

public void initialise(int abs,int ord)
	{
	x=abs;
	y=ord;
	}

public void deplace(int dx,int dy)
	{
	x+=dx;
	y+=dy;
	}

public void affiche()
	{
	System.out.println("Point en "+x+" "+y);
	}

	private int x,y;
}


On suppose avoir besoin d'une class PointCol, destinée à la manipulation des points colorés du plan. On crée une classe dérivée avec un attribut supplémentaire pour enregistrer la couleur :

public class PointCol extends Point
{

public void colore(byte couleur)
	{
	this.couleur=couleur;
	}

	private byte couleur;
}


Ici, la classe PointCol dérive la classe Point (Il n'est pas nécessaire que les classes soient dans le même package). Pour un objet de type PointCol, il a accès aux méthodes publiques et aux attributs publiques de Point : de fait, il est possible d'appeller deplace(int,int) depuis une objet de type PointCol :
PointCol pt=new PointCol();
pt.deplace(5,6);
pt.affiche();


Une classe dérivée ne peut pas accéder aux membres privés de la classe au dessus d'elle, peut accéder aux membres public ou protected de la classe au dessus d'elle.

En java, une classe ne peut hériter que d'une autre classe, en C++ l'héritage multiple est possible. Par ailleurs, une classe mère peut elle même être classe dérivée d'une autre classe : classe1 -> classe2 -> classe3... Dans ce cas là, classe 3 accède les attributs et méthodes publics ou protected de classe1 de la même manière que pour ceux de classe2.


2 Construction et initalisation des objets dérivés

Dans l'exemple précédent, les classes ont été construites sans constructeurs. Ce cas ne pose pas de difficultés. Le constructeur défini dans la classe dérivée peut ne pas faire appel au constructeur de la classe au dessus. Le constructeur peut également faire réference au constructeur de la classe mère. Dans ce cas, on utilise le mot clé super. et ce doit être la première instruction du constructeur de la classe dérivée

public class Point
{

public Point(int x,int y)
	{
	...
	}
}



public class PointCol
{
public PointCol(int x,int y,int color)
	{
	super(x,y);
	this.couleur=color;
	}
}


3 Redéfinition et surdéfinition de membres

Dans le chapitre 3, nous avons vu la notion de surdéfinition à l'intérieur d'une même classe. La surdéfinition concerne des méthodes de même nom mais de signatures différentes (signature : nom+attributs). Une classe dérivée peut sur-définir les méthodes de sa classe de base. Il devient en plus possible de rédéfinir une méthode de même signature dans une classe dérivée. Soit un premier cas :

public class Point
{
...
public void affiche()
	{
	System.out.prinltn("Point : "+x+" "+y);
	}

	private int x,y;
}

/*Autre fichier : */
public PointCol extends Point
{
...
}

/*Autre fichier avec le main : */
public static void main(String[] args)
	{
	PointCol pc=new PointCol();
	Point p=new point();
	...
	}


Dans ce cas, l'appel p.affiche() affiche les coordonnées de l'objet p de type Point et de la même manière la méthode pc.affiche(); affiche également les coordonnées de l'objet pc de type PointCol puisqu'il est de type Point.

Il est possible de modifier le fichier PointCol dans le fichier précédent, en redéfinissant la méthode affiche() :

public class Point
{
...
public void affiche()
	{
	System.out.prinltn("Point : "+x+" "+y);
	}

	private int x,y;
}

/*Autre fichier : */
public PointCol extends Point
{
...

	private byte color;

public void affiche()
	{
	System.out.prinltn("Point : "+x+" "+y+" "+color);
	}
}

/*Autre fichier avec le main : */
public static void main(String[] args)
	{
	PointCol pc=new PointCol();
	Point p=new point();
	...
	}


Dans ce cas, l'appel pc.affiche() renvoie vers la méthode de PointCol. Au sein des méthodes de la classe fille, il est toujours possible d'appeller les méthodes de la classe mère, la syntaxe devient alors : super.affiche();.

La redefintion d'une méthode ne doit pas diminuer les droits d'accès à cette méthode et doit donner la même valeur de retour alors que dans le cas de la surdéfinition, les valeurs de retour de deux fonctions de même nom peuvent être différentes.


4 Polymorphisme et transtypage

Le polymorphisme consiste dans le fait de manipuler des objets sans en connaître tout à fait le type. Par exemple, il est possible de manipuler une collection d'élements Point sans savoir si ce sont des objets Point ou PointCol et appeller affiche(). Un objet de type PointCol est toujours manipulable comme un objet de type Point.

Par ailleurs, ce genre d'affectation est possible :

Point p=new PointCol(5,6,(byte)2);. 


De manière générale, on peut affecter à une variable objet non seulement une réference à un objet du type de la variable, mais aussi une référence à un objet d'un type dérivé.

Pour résumer : quel choix de méthode en fonction de la signature ? Sachant qu'il peut y avoir surdéfinition, redéfinition et qu'on peut être en train de manipuler une varibale de type PointCol en utilisant en réference de type Point. Dans le cas de la redéfinition : si l'objet manipulé est de type PointCol, alors la méthode affiche() appellée sera celle de PointCol non pas celle de Point. Dans le cas d'une méthode surdéfinie : on recherche d'abord une méthode de signature correspondante dans PointCol puis dans Point.

Traiter un objet de type PointCol comme un objet de type Point ne pose pas de problème, on peut parler de compatibilité ascendante.

L'opération inverse est plus complexe : supposons qu'on traite d'objet de type Point mais dont on ne sait pas si ils sont des PointCol, des objets de la class Point, ou des objets d'une autre classe dérivée de Point : PointNoir. Le code suivant ne pose pas de problème :

PointCol pC=new PointCol(5,6,(byte)3);
Point p=pC;


En revanche, le code suivant ne sera pas accepté à la compilation.

Point p=new Point();
PointCol pC=p;


Transtypage. Dans certaines circonstance, le développeur est assuré du type de l'objet qu'il manipule. Soit par exemple la manipulation d'un tableau d'objets de la class Point : en tant que développeur, vous pouvez savoir que le second objet du tableau est forcément de type PointCol, dans ce cas, vous pouvez le manipuler comme tel :
PointCol pC=(PointCol)tab[1];


Ce code ne produit pas d'erreur à la compilation : si l'objet tab[1] n'est pas de type PointCol, vous aurez une erreur à l'exécution en revanche. On parle de forcer le type ou transtypage. Des transtypage sur des types simples avaient déjà été vus dans le chapitre 3 : mais dans le cas du chapitre 3, ils ne provoquaient pas d'erreur à l'exécution : si on essayait de manipuler 0.5 comme un int avec int a=(int)0.5, il y avait conversion par arrondi de 0.5 en 0 pour permettre la manipulation.

Pour éviter les erreurs à l'exécution ou dans le cas ou on veut manipuler tab[1] en fonction de son type, mais sans le connaître, on peut utiliser l'opérateur spécifique à Java : instanceof. Cet opérateur spécifique renvoie vrai si un objet appartient une classe : si tab[1] est de type PointCol alors tab[1] instanceof PointCol renvoie vrai. On peut alors envisager un code du type suivant en supposant avoir une méthode affichagePtNoir() dans la classe PointNoir, une méthode affichagePtCol() dans la classe PointCol :
	if(tab[1] instanceof PointNoir)
	{
	((PointNoir)tab[1]).affichagePointNoir();
	}
	else if(tab[1] instanceof PointCol)
	{
	((PointCol)tab[1]).affichagePointCol();
	}
	else
	{
	tab[1].affichage();
	}


De manière générale, on doit pouvoir se passer de instanceof : il s'agit d'un opérateur propre à Java, dont C++ ne dispose pas et donc, il présente une facilité de conception à éviter.


5 Super Classe Object

En Java, il existe une hiérarchie explicite qu'on retrouve à travers le mot clé extends sur la ligne public class PointCol extends Point. Il existe aussi une hiérarchie implicite et toute classe dérive d'une super-class Object, ie tout objet peut utiliser les méthodes de la class Object. De fait, le code suivant ne pose aucun problème d'exécution ou de compilation :

Point p=new Point(...);
PointCol pC=new Point(...);
Facture fac=new Facture(...);
Object ob;
/*L'ensemble des instructions suivantes ne posent pas de pb : */
ob=p;
ob=pC;
ob=fac;


De fait, pour tout objet, vous disposez a minima des méthodes de la classe Object.

Par exemple toString() qui renvoie la classe et l'adresse en mémoire, sous forme de chaine hexaécimale, de l'objet. Vous pouvez par exemple tester le code suivant :

Point p=new Point();
String st=p.toString();
System.out.println("Ce que donne la methode toString() : "+st);
/*Ces deux dernieres instructions sont en fait équivalentes à :*/
System.out.println("Ce que donne la methode toString() : "+p);


L'équivalence s'explique par le fait que la concaténation d'un objet à une chaine se fait en récupérant une chaine à partir de l'objet par appel de la fonction toString() de cet objet. Bien sûr, cette classe est redefinissable. On peut par exemple introduire :

public class Point{

	int abs;
	int ord;
	
public Point(int x,int y)
	{
	abs=x;
	ord=y;
	}

public String toString()
	{
	return (abs+" "+ord);
	}
}



6 Classes Abstraites

Certaines classes sont particulières au sein des hiérarchies : les classes abstraites. Ce type de classe ne supporte pas l'instanciation. Elle sont définies en introduisant le mot-clé abstract dans la définition de la classe : public abstract class ...

Ces classes ne servent que pour la dérivation de classe. On peut trouver des attributs et des méthodes qui seront utilisables par des classes dérivées. Certaines méthodes sont abstraites : public abstract String getName() : les classes dérivées devront obligatoirement définir ces méthodes si elles ne sont pas abstraites.

Dès qu'une classe contient au moins une méthode abstract, elle est abstract, sans qu'il faille le préciser dans la ligne de déclaration de la classe.

Quel intérêt des classes abstraites ? On peut utiliser une classe abstraite pour stocker toutes les fonctionnalités dont on souhaite disposer pour toutes les classes descendantes :

Cette certitude de disposer de méthode rend le polymorphisme encore plus puissant. Soit la classe :

public abstract class X
{
	public abstract void f();//ici f n'est pas encore définie
}


On est alors assuré de pouvoir définir une méthode générale pour tous les objets X sans avoir à se préoccuper du type précis de X :

void algo(X x)
	{
	...
	x.f();
	...
	}


Un exemple complet :

/*un premier fichier : Affichable.java*/
public abstract class Affichable
{
abstract public void affiche();
}

/*Un deuxième fichier : Entier.java*/
public class Entier extends Affichable
{

public Entier(int n)
	{
	valeur=n;
	}	

public void affiche()
	{
	System.out.println("Je suis un entier de valeur "+valeur);
	}

private int valeur;
}


/*Un troisième fichier : Reel.java*/
public class Reel extends Affichable
{

public Reel(double x)
	{
	valeur=x;
	}	

public void affiche()
	{
	System.out.println("Je suis un réel de valeur "+valeur);
	}

private double valeur;
}

/*Une classe avec un main pour le lancement : */
public class TestAbstractCl
{

public static void main(String[] args)
	{
	Affichable[] tab;
	tab=new Affichable[3];
	tab[0]=new Entier(25);
	tab[1]=new Reel(1.25);
	tab[2]=new Entier(50);
	int i;
		for(i=0;i<3;i++)
		tab[i].affiche();
	}
}


7 Interface

On peut considérer une classe abstraite n'implémentant aucune méthode et aucun champ : il s'agit d'une interface. La notion d'interface a un certain nombre d'avantages :



Pour définir une interface :
public interface I
{

void f(int n);
void g();

}


Dès qu'on utilise un objet I, il est instance d'une classe implémentant l'interface et donc, cet objet dispose des méthodes f() et g(). Pour dire qu'une classe dérive une interface, on dira qu'elle l'implémente :

public class A implements I
	{	
	//A doit alors définir une méthode f et une méthode g
	}


Cas d'une classe implémentant plusieurs interfaces :

public interface I1
{
void f(int n);
}

public interface I2
{
void g();
}

public class A implements I1,I2
{
	//A doit implementer les méthodes des deux interfaces : g() et f(int)
}


Il est possible de manipuler des variables de type I :
I1=new A();