La covariance en java

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.

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.

Premier exemple

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().
Voila 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 renvoyer 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 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 retours nous permet de sécuriser notre code.
Nous allons reprendre l’exemple vu précédemment en introduisant la covariance sur le type 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.

Deuxième exemple

Ci-dessous le code correspondant :


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 retour de la méthode method évolue et se spécialise.
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.
warning
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.

Un exemple concret de mise en oeuvre de la covariance

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étale
 }

 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 (ligne 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.
info
Remarques : Il est vrai que nous pourrions être plus abstrait 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 typages (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 type comme vu ci-dessus.
Nous allons maintenant utiliser le mécanisme de covariance afin d’éviter les problèmes vu 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étale
    }

    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 retour différent. Les méthodes sont bien redéfinies et non surchargée.
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.
infoBrièvement : ce principe permet de spécialiser les arguments d’une méthode sur le même principe que la covariance.

La covariance dans le java 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’entrer 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 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é.
Hors 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 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.


3 réflexions sur “La covariance en java

  1. 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.

    1. 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…

  2. 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??

Laisser un commentaire