I. Rappel▲
La redéfinition ou la surcharge suivent des règles précises dans les relations d'héritage.
Dans le cadre de la redéfinition d'une méthode, il faut que la signature de la méthode de la classe fille ait exactement la même signature.
//Classe Mère
public
String maMethod
(
String a,String b){
//...}
//Classe fille
public
String maMethod
(
String a,String b){
//...}
Dans le cadre de la surcharge d'une méthode, la méthode de la classe fille se distinguera par ses paramètres (leur nombre et leur type).
//Classe Mère
public
String maMethod
(
String a,String b){
//...}
//Classe fille
public
String maMethod
(
String a,Integer c){
//...}
II. La covariance▲
Dans les versions antérieures au JDK 1.5 ces deux règles étaient valables. Depuis la version 1.5, ces règles ont légèrement changé.
En effet, il est maintenant possible de modifier le type de retour d'une méthode que l'on redéfinit. Dans un premier temps, cette nouvelle possibilité peut paraître quelque peu déstabilisante.
Après réflexion, la covariance des types de retour donne la possibilité de s'affranchir de la conversion de type explicite (cast). Le cast, une technique qui introduit des faiblesses dans les programmes et qui met en évidence une faiblesse du typage du langage Java.
Une contrainte existe dans la mise en œuvre de cette technique, il faut que le nouveau type de retour soit un sous-type du type déclaré dans la super classe. Cette contrainte reste logique et cohérente. En effet, l'héritage raffine (spécialise) le comportement d'une classe, il est logique que les méthodes redéfinies dans les sous-classes spécialisent aussi les types de retour de ses méthodes.
L'utilisation de la covariance ne doit pas être systématique, il faut que celle-ci présente un réel intérêt.
Dans la suite de l'article, nous allons voir plusieurs cas d'utilisation de ce [nouveau] principe qui est maintenant autorisé.
III. Le principe▲
Nous allons voir ici un exemple simple.
III-A. UML▲
III-B. Explications▲
Dans cet exemple, le type de retour a été spécialisé (MonType -> SousType).
Entre les deux, il y a une relation de sous-type. SousType est un sous-type de MonType.
Cet exemple n'est peut-être pas très parlant et on ne saisit pas forcement l'intérêt de faire cette modification.
Ne vous inquiétez pas, dans la suite de l'article, nous verrons un exemple concret permettant de voir l'intérêt de cette pratique.
IV. La mise œuvre▲
IV-A. Exemple 1▲
Ci-dessous, nous allons voir un exemple plus parlant avec la méthode clone() définie par l'interface Cloneable. Lorsque l'on souhaite cloner en profondeur un objet, on implante cette interface et on redéfinit la méthode clone().
Voilà la signature de la méthode :
public
Object clone
(
) throws
CloneNotSupportedException;
On notera que cette méthode renvoie un type Object. Si l'objet à cloner est de ce type, aucun problème. Par contre, si le l'objet renvoyé n'est pas de ce type, il faudra caster l'objet renvoyé dans le type de l'objet cloné.
package
covariance;
public
class
ClassCloneable implements
Cloneable {
private
String objecName =
null
;
/**
*
@return
Renvoie objecName.
*/
public
String getObjectName
(
) {
return
objecName;
}
/**
*
@param
objecName
objecName à définir.
*/
public
void
setObjectName
(
String objecName) {
this
.objecName =
objecName;
}
/**
* redéfinition de la méthode clone
*/
@override
public
Object clone
(
) throws
CloneNotSupportedException {
ClassCloneable newRef =
(
ClassCloneable) super
.clone
(
);
return
newRef;
}
}
public
class
Main {
public
static
void
main
(
String[] args) {
ClassCloneable classCloneable =
new
ClassCloneable
(
);
classCloneable.setObjectName
(
"nouvel Objet Name"
);
try
{
AutreClassCloneable newRef =
(
AutreClassCloneable)classCloneable.clone
(
);
}
catch
(
Exception err){
err.printStackTrace
(
);
}
}
Dans cet exemple, nous voyons que nous devons passer par un cast explicite de l'objet renvoyé par la méthode clone.
Nous sommes obligés de passer par là afin de retomber sur le type d'instance de l'objet cloné.
Cette pratique peut comporter des risques. Dans le cas ci-dessus, le code se compilera et s'exécutera correctement.
En revanche, un développeur peu scrupuleux (ou tête en l'air) aurait pu écrire le code suivant :
package
covariance;
public
class
ClassCloneable implements
Cloneable{
private
String objecName =
null
;
/**
*
@return
Renvoie objecName.
*/
public
String getObjectName
(
) {
return
objecName;
}
/**
*
@param
objecName
objecName à définir.
*/
public
void
setObjectName
(
String objecName) {
this
.objecName =
objecName;
}
/**
* redéfinition de la méthode clone
*/
@override
public
Object clone
(
) throws
CloneNotSupportedException {
ClassCloneable newRef =
(
ClassCloneable) super
.clone
(
);
return
newRef;
}
}
package
covariance;
public
class
AutreClassCloneable implements
Cloneable{
private
String objectName =
null
;
/**
*
@return
Renvoie objectName.
*/
public
String getAutreObjectName
(
) {
return
objectName;
}
/**
*
@param
objecName
objecName à définir.
*/
public
void
setAutreObjectName
(
String objectName) {
this
.objectName =
objectName;
}
/**
* surdéfinition de la méthode clone
*
*/
@override
public
Object clone
(
) throws
CloneNotSupportedException {
ClassCloneable newRef =
(
ClassCloneable) super
.clone
(
);
return
newRef;
}
}
public
class
Main {
/**
*
@param
args
the command line arguments
*/
public
static
void
main
(
String[] args) {
ClassCloneable classCloneable =
new
ClassCloneable
(
);
classCloneable.setObjectName
(
"nouvel Objet Name"
);
try
{
AutreClassCloneable newRef =
(
AutreClassCloneable)classCloneable.clone
(
);
}
catch
(
Exception err){
err.printStackTrace
(
);
}
}
Dans l'exemple ci-dessus, la compilation se passera sans problème. Le type de retour de la méthode est bien un sous-type de la classe Object.
En revanche, au moment de l'exécution, la liaison tardive posera quelques problèmes de cast. Le type renvoyé par la méthode est ClassCloneable et on veut le transtyper en AutreClassCloneable. Il n'y a aucune relation de type entre ces deux classes. L'exécution de ce programme aboutira à une exception de type ClassCastException.
Ce type de situation montre une faiblesse du typage en Java. En effet, le fait de devoir forcer le typage d'un objet peut donner lieu à des problèmes qui interviennent uniquement à l'exécution.
L'utilisation de la covariance sur les types de retour nous permet de sécuriser notre code.
Nous allons reprendre l'exemple vu précédemment en introduisant la covariance sur le type de retour de la méthode clone.
package
covariance;
public
class
ClassCloneable implements
Cloneable{
private
String objecName =
null
;
/**
*
@return
Renvoie objecName.
*/
public
String getObjectName
(
) {
return
objecName;
}
/**
*
@param
objecName
objecName à définir.
*/
public
void
setObjectName
(
String objecName) {
this
.objecName =
objecName;
}
/**
* redéfinition de la méthode clone
*
*/
@override
protected
ClassCloneable clone
(
) throws
CloneNotSupportedException {
return
(
ClassCloneable) super
.clone
(
);
}
}
Dans cette nouvelle implémentation de la classe ClassCloneable, nous pouvons remarquer que le type de retour de la méthode clone() a été spécialisé.
Malgré la modification de la signature, cette méthode redéfinit bien la méthode clone de l'interface Cloneable.
IV-B. Exemple 2▲
Voici un exemple plus général :
IV-B-1. UML▲
IV-B-2. Code▲
public
class
A {
A method
(
A x){
return
this
;
}
}
La classe A ci-dessus déclare une méthode de type method :A->A.
public
class
B extends
A {
B method
(
A x){
return
this
;
}
}
La classe B ci-dessus étend la classe A en redéfinissant la méthode method déclarée.
Le type de cette méthode dans la classe B est method :A->B.
public
class
C extends
A {
C method
(
A x){
return
this
;
}
}
La classe C ci-dessus étend la classe A en redéfinissant la méthode method déclarée.
Le type de cette méthode dans la classe C est method :A->C.
Dans cet exemple, on voit qu'au fur et à mesure que l'on descend dans l'arbre d'héritage, le type de retour de la méthode method évolue et se spécialise.
Par conséquent, on limite la portée (« scope ») de compatibilité dans les classes filles.
En revanche, on pourra s'affranchir de l'utilisation du cast pour manipuler les types retournés par la méthode method. Dans les deux cas (Classe B et C), la méthode est redéfinie et non surchargée.
Ce code compilera avec le compilateur 1.5. En revanche, des erreurs de type seront générées avec la version 1.4 du compilateur.
V. La covariance : un exemple concret▲
Nous allons voir, dans ce paragraphe, un cas d'utilisation concret mettant en évidence l'intérêt de la covariance.
Ci-dessous, une interface représentant un panier ainsi que deux implémentations de celle-ci :
public
interface
Bag {
public
void
addItem
(
Item i);
public
Bag addItems
(
Bag b);
}
//Premiere implantation l'interface Bag
public
class
BooksBag implements
Bag {
public
void
addItem
(
Item i) {
//code d'ajout d'un item au panier'
}
public
void
printTitles
(
){
//code d'affichage du titre de chaque livre
}
public
Bag addItems
(
Bag b) {
//Code d'ajout des items au panier
return
this
;
}
}
//Deuxième implantation du panier.
public
class
FlowersBag implements
Bag{
public
void
addItem
(
Item i) {
}
public
void
compterNombreDePetale
(
){
//Code permettant de compter le nombre de pétales
}
public
Bag addItems
(
Bag b) {
//Code d'ajout au panier
return
this
;
}
}
Dans cette première version, nous n'utilisons pas le principe de covariance.
En effet, la méthode susceptible d'appliquer ce principe est addItems :Bag->Bag.
Nous allons mettre en évidence les problèmes sous-jacents dans ce cas.
Ici, nous mettons en évidence les problèmes que l'on peut rencontrer :
public
class
Main {
public
static
void
main
(
String[] args) {
BooksBag bbAutres =
new
BooksBag
(
);
FlowersBag fbAutres =
new
FlowersBag
(
);
BooksBag bb =
new
BooksBag
(
);(
1
)
FlowersBag fb =
new
FlowersBag
(
);(
2
)
BooksBag bag =
(
BooksBag)bb.addItems
(
bbAutres); (
3
)
FlowersBag fbag =
(
FlowersBag)fb.addItems
(
fbAutres); (
4
)
FlowersBag bag =
(
FlowersBag)bb.addItems
(
bbAutres); (
5
)
BooksBag fbag =
(
BooksBag)fb.addItems
(
fbAutres); (
6
)
}
}
Nous commençons par instancier un objet de type BooksBag et un objet de FlowersBag (lignes 1 et 2).
Ensuite, nous appliquons la méthode addItems sur les deux objets instanciés précédemment
(lignes 3 et 4).
La remarque qui peut être faite concerne la nécessiter de caster les deux objets dans leur type respectif lors de la récupération de la référence de l'objet modifié. Ce qui n'est pas une bonne chose.
Remarques : il est vrai que nous pourrions être plus abstraits et utiliser le type de l'interface Bag ; cela nous éviterait d'utiliser le mécanisme de cast.
En faisant ça, nous perdrions de l'information pour chaque type :
- la méthode compterNombreDePetale pour la classe FlowersBag ;
- la méthode printTitles pour la classe BooksBag.
On peut constater aussi qu'il est possible de faire des erreurs de typage (ligne 5 et 6). Ici, la compilation se passera sans problème ; par contre, une exception de type ClassCastException sera levée lors de l'exécution.
Maintenant que nous avons mis en évidence ce problème, voyons comment la covariance va nous aider à obtenir un code plus propre, c'est-à-dire que l'on pourra se passer du mécanisme de cast et surtout d'interdire dès la compilation le mélange de types comme vu ci-dessus.
Nous allons maintenant utiliser le mécanisme de covariance afin d'éviter les problèmes vus précédemment :
public
interface
Bag {
public
void
addItem
(
Item i);
public
Bag addItems
(
Bag b);
}
//Premiere implantation l'interface Bag
/**
*
*
@author
fabszn
*/
public
class
BooksBag implements
Bag {
public
void
addItem
(
Item i) {
//code d'ajout d'un item au panier
}
public
void
printTitles
(
){
//code d'affichage du titre de chaque livre
}
public
BooksBag addItems
(
Bag b) {
//Code d'ajout des items au panier
return
this
;
}
}
//Deuxième implantation du panier.
public
class
FlowersBag implements
Bag{
public
void
addItem
(
Item i) {
}
public
void
compterNombreDePetale (
){
//Code permettant de compter le nombre de pétales
}
public
FlowersBag addItems
(
Bag b) {
//Code d'ajout au panier
return
this
;
}
}
Dans cette première version, nous n'utilisons pas le principe de covariance. la méthode impactée est addItems :Bag->Bag.
Selon la sous-classe, cette méthode aura un type de retour différent. Les méthodes sont bien redéfinies et non surchargées.
public
class
Main {
public
static
void
main
(
String[] args) {
BooksBag bbAutres =
new
BooksBag
(
);
FlowersBag fbAutres =
new
FlowersBag
(
);
BooksBag bb =
new
BooksBag
(
);(
1
)
FlowersBag fb =
new
FlowersBag
(
);(
2
)
BooksBag bag =
bb.addItems
(
bbAutres); (
3
)
FlowersBag fbag =
fb.addItems
(
fbAutres); (
4
)
FlowersBag bag =
bb.addItems
(
bbAutres); (
5
)
BooksBag fbag =
fb.addItems
(
fbAutres); (
6
)
FlowersBag bag1 =
(
FlowersBag)bb.addItems
(
bbAutres); (
7
)
BooksBag fbag2 =
(
BooksBag)fb.addItems
(
fbAutres); (
8
)
}
}
Dans le code ci-dessus, nous n'avons plus recours au mécanisme de cast. Les erreurs de typage des lignes (5) (6) (7) (8) seront détectées à la compilation et à l'exécution.
Nous avons maintenant un code plus cohérent et plus robuste. Cependant, il reste une faille au niveau des arguments. Le principe de la contravariance n'est pas encore implanté dans le langage Java.
Brièvement : ce principe permet de spécialiser les arguments d'une méthode sur le même principe que la covariance.
VI. La covariance et le JDK 5 (et versions ultérieures)▲
La covariance n'est pas utilisée dans le cadre du JDK 5 sur les classes historiques.
On utilise bien souvent les interfaces pour laisser des points d'entrée aux développeurs souhaitant étendre les fonctionnalités de l'API.
Par exemple, l'interface List est implémentée de plusieurs façons. Ici, nous retiendrons deux classes l'ArrayList et la LinkedList. Chacune implante une méthode de type clone : Object (méthode issue de l'interface Cloneable).
Dans un contexte autre que celui d'une API (ici le JDK), il aurait été judicieux de remplacer le type de retour Object par ArrayList dans un cas et LinkedList dans l'autre. Ceci afin de pouvoir manipuler le bon type sans avoir à caster l'objet retourné.
Or, c'est bien le type Object qui a été utilisé. Lors de l'introduction de la covariance à partir de la version 5 du JDK, les développeurs de Sun ont décidé de ne pas modifier les classes existantes. Ceci sûrement dans un souci de compatibilité ascendante. Néanmoins, ils auraient pu créer de nouvelles classes ou adapter les anciennes comme ils l'ont fait pour les Générics.
VII. Conclusion▲
Le mécanisme de la covariance apporte une nouvelle perspective au sein du langage Java. Comme les Generics, ce principe donne la possibilité de détecter les erreurs de typage plus tôt durant le développement. Cela permet de diminuer les erreurs au moment de la liaison tardive.
J'espère que cet article vous donnera un bon aperçu du principe de la covariance de type en Java.
VIII. Remerciements▲
Je tiens à remercier wichtounet, adiGuba, ®om et ricky81 pour leurs conseils et leurs relectures.