La version 5 du JDK (Tiger) a introduit une notion qui est passée inaperçue face aux autres nouveautés (généricité, auto-boxing, etc…) Cette nouveauté est la covariance des types retours. Cet article aura pour but de présenter les concepts qu’introduit cette notion. Je vous proposerai quelques exemples permettant de mettre en évidence son utilité.
Rappel : Surchage et redéfinition
Un petit rappel rapide sur la redéfinition et la surcharge. Elles 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.
Redéfinition
//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 (leurs nombres et leurs types).
Surcharge
//Classe Mère public String maMethod(String a,String b){//...} //Classe fille public String maMethod(String a,Integer c){//...}
La covariance, qu’est ce que c’est?
Dans les versions antérieures au JDK 1.5 ces deux règles étaient valables. Depuis la version 1.5 et supérieures ces règles ont légèrement changées.
En effet, il est maintenant possible de modifier le type 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 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 oeuvre de cette technique, il faut que le nouveau type 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 retours 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é.
Le principe de la covariance
Nous allons voir ici un exemple simple.
Entre les deux, il y a une relation de sous type. SousType est un sous type de MonType.
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.
Premier exemple
public Object clone() throws CloneNotSupportedException;
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(); } }
Nous sommes obligés de passer par là afin de retomber sur le type d’instance de l’objet cloné.
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(); } }
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(); } }
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.
Deuxième exemple
Ci-dessous le code correspondant :
public class A { A method(A x){ return this; } }
public class B extends A { B method(A x){ return this; } }
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; } }
Le type de cette méthode dans la classe C est method :A->C.
Par conséquent on limite le 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.
Un exemple concret de mise en oeuvre de la covariance
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étale } public Bag addItems(Bag b) { //Code d'ajout au panier return this; } }
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.
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) } }
Ensuite, nous appliquons la méthode addItems sur les deux objets instanciés précédemment (ligne 3 et 4).
lors de la récupération de la référence de l’objet modifié. Ce qui n’est pas une bonne chose.
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
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 type comme vu ci-dessus.
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étale } public FlowersBag addItems(Bag b) { //Code d'ajout au panier return this; } }
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) } }
La covariance dans le java 5 (et versions ultérieures)
pour laisser des points d’entrer aux développeurs souhaitant étendre les fonctionnalités de l’API.
de la version 5 du JDK, les développeurs de Sun on décider de ne pas modifier les classes existantes. Ceci surement dans un soucis de compatibilité ascendante. Néammoins, ils auraient pu créer de nouvelles classes ou adapter les anciennes comme ils l’ont fait pour les Générics.
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.
merci , pour l’article ,
cependant , il parait que c’est pour un niveau plus avancé , je n’ai pas capté le principe de la covariance .
pourquoi Les erreurs de typage des lignes (5) (6) (7) (8) seront détectées à la compilation et à l’exécution.
Bonsoir,
Désolé pour le temps que j’ai mis à répondre..
pour les lignes suivantes :
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)
Pour re situer le contexte, Le principe de covariance a été appliqué sur les classes (FlowersBag, BooksBag).
Dans tous les cas, une erreur de typage sera révélé du fait de la spécification des types retours des méthodes addItems. Typiquement, pour la ligne (5), cette méthode renvoie un objet de type BooksBag et l’affectation se fait sur un objet de type FlowersBag. Au moment de la compilation, un erreur de typage sera levée. idem pour les autres lignes.
Par extension, cette erreur se retrouvera aussi à l’exécution même si l’on sait que si le code ne compile pas il ne pourra pas s’exécuter…
bonjour,
-cet article ma aidé a comprendre un peut plus la covariance alors merci, je suis etudiante en architecture logicielle a nantes .en eiffel la contravariance existe est ce que vous avez plus d’info sur ça.
– autre question: vous avez dit » 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. » est ce que la contravariance c’est plutôt d’élargir les arguments d’une methode??