Skip to main content

Autres mécanismes (non objet) - Les types paramétrés

Les types paramétrés ("parameterized type") est un mécanisme de programmation générique introduit dans la version 5.0 du langage java.

Il permet de définir une classe/interface ou une méthode sans précision du type de variables, paramètres et la valeur de retour. La spécification des types manquants est faite au moment de l'utilisation (pour une classe à l'instanciation ou la déclaration d'une variable de cette classe, pour une méthode à son appel).

L'exemple standard de la programmation générique se trouve dans l'utilisation et la réalisation des classes conteneurs de données.

Les conetneurs de données

Définition : une classe "conteneur" encapsule une collection d'objets dans une structure de données particulière (liste, ensemble, pile, arbre, ...). Elle est chargée du stockage, de la gestion (ajouter, enlever,...) de l'accès à un objet et du parcours des objets de la collection (notion d'itérateur).

Un conteneur ne déclenche pas le comportement des objets de la collection (à part l'égalité et la comparaison tiré). Pourtant, dans un langage typé, la réalisation de ces conteneurs entraîne un problème de cohérence du type des éléments gérés par un conteneur.

Cohérence de type

En Java, les classes "conteneurs" les plus communes sont fournies :

  • par les tableaux
  • par des classes collection de l'API standard, dans le paquetage java.util (voir les interfaces Collection et Iterator).

Pour les tableaux

Exemple d'une erreur de cohérence de type avec un tableau

public class CoherenceTableau{
static void parcours(Porte[] portes){
for(int i = 0; i < portes.length; i++){
pourtes[i].ouvrir();
}
}

public static void main(String[] args){
Porte[] mesPortes = new Porte[3];

mesPortes[0] = new PorteCoulissante(20);
mesPortes[1] = new PorteCharniere();
mesPortes[2] = "Coucou";

parcours(mesPortes);
}
}

/*
% javac CoherenceTableau.java
Conteneur.java:17: incompatible types
found : java.lang.String
required : Porte
error
*/

La déclaration d'un tableau spécifie le type des objets acceptés par la collection. La cohérence de type est vérifiée à la compilation lors de l'ajout d'un objet.

Pour les classes "collection"

Dans la réalisation de ces classes, le type des éléments est déclaré du type Object puisque c'est la racine de la hiérarchie de classes en java. Les méthodes d'ajout et d'accès utilisent aussi le type Object. La cohérence de type est vérifiée à l'exécution lors de l'accès à un élément de la collection. Il est nécessaire d'inclure un code de vérification de la conversion de type (opérateur instanceof ou par capture de l'exception java.lang.ClassCastException).

Paramétrer les types

Le mécanisme des types paramétrés peut s'utiliser sur une interface, une classe, une méthode ou un constructeur. L'objectif est de détecter l'incohérence de type dans un conteneur à la compilation plutôt qu'à l'exécution. Pour cela, il est possible de définir le type des éléments à l'utilisation du conteneur.

ClasseParametree<Porte, PassagerStandard> var;

var = new ClasseParametree<Porte, PassagerStandard>();
var.methodeParametree(new DentBleu());

Dans la définition d'une classe paramétrée, il est nécessaire de déclarer des "paramètres formels" pour la spécification du type. Par convention, le nom du paramètre formel est une seule lettre en majuscule.

class ClassParametree<E,T>{
private E uneInstance;
public T nomMethode(E par1, String par2){
T t;
//...
return t;
}
public <U> void methodeParametree(U u){
//...
}
}

Chaque paramètre effectif précisé à l'utilisation va remplacer dans l'ordre chaque paramètre formel de la définition.

Cohérence de type pour la version avec type paramétré de la classe java.util.Vector.

Vector<Porte> portes = new Vector<Porte>(5);

portes.add(new PorteCoulissante(999));
portes.add(new PorteCharniere());
portes.add("Coucou") // erreur à la compilation

Vector<PorteCoulissante> coulissantes = new Vector<PorteCoulissante>(3);

coulissantes.add(new PorteCoulissante(666));
coulissantes.add(new PorteCharniere()); //erreur à la compilation

L'accès aux éléments se fait sans conversion de type. Le langage Java a introduit une boucle simplifiée pour le parcours d'un conteneur paramétré de l'API java.util.Collection.

void boucleSimpleifiee(Vector<Porte> portes){
for(Porte p : portes)
p.ouvir();
//équivalent à
for(Iterator<Porte> it = portes.iterator(); it.hasNext();){
Porte p = it.next();
p.ouvrir();
}
}

Remarque: Pour des raisons de compatibilité, il n'est pas obligatoire de préciser le type paramétré à la déclaration ou à l'instanciation (le type est alors Object).

Vector desObjets = new Vector(5);
// équivalent à
Vector<Object> desObjets = new Vector<Object>(5);

Compilation d'une classe paramétrée

En Java, tous les paramètres effectifs d'une classe paramétrée partagent le même code. Il y a toujours un seul fichier compilé (.class) par classe. Le mécanisme n'implique pas une recompilation du code pour chaque utilisation d'une classe paramétrée.

L'information sur une type paramétré est présente que pour la compilation de l'utilisation de la classe paramétrée. Dans la fichier compilé, l'information sur le type paramétré est remplacée par le type Object; il y a effacement du type ("erasure type").

Cette compilation unique aboutit à des contraintes sur l'utilisation des types paramétrés :

  • Un type paramétré n'est possible qu'avec des références.
  • Les instruction impliquant la classe ne sont pas autorisées sur un type paramétré : new T(),T.methodeDeClasse(), new T[initTaille].

Envoi de messages sur un paramètre formel

Le compilateur vérifie la validité d'un envoi de message par rapport au type de la variable. Il est possible de donner une contrainte sur le paramètre formel pour s'assurer d'un intervale de type. Par exemple tous les sous-types de l'interface Porte.

public <U extends Porte> void parametreUneMethode(U u){
u.estFerme();
}

Le compilateur vérifie cette contraintes sur le paramètre effectif. Il est possible de combiner ces contraintes, par exemple : <T extends Porte 1 Comparable<T>>

Relation de type/sous-type entre paramètres effectifs

Comment considérer les trois conteneurs Vector<Porte>, Vector<PorteCoulissante> et Vector<PorteCharniere> par rapprot à la relation de type / sous-type entre ProteCharniere, PorteCoulissante et Porte ?

Regardons le cas des tableaux, il est possible de passer le paramètre PorteCoulissante[] à la méthode parcours(Porte[] portes).

static void parcours(Porte[] portes){
for (Porte p : portes){
if(p != null)
p.ouvrir();
}
//...
PorteCoulissante[] coulissantes = new PorteCoulissante[3];

coulissantes[0] = new PorteCoulissante(20);
coulissantes[1] = new PorteCoulissante(10);
coulissantes[2] = new PorteCoulissante(11);

parcours(coulissantes);
}

Quand la relation de type/sous-type est transmise aux conteneurs, nous avons un mécanisme de covariance. C'est le cas pour les tableaux, PorteCoulissante[] est sous-type de Porte[]. Cela permet de factoriser la méthode parcours() à tous les tableaux correspondant à un sous-type de Porte.

Que se passe-t-il si le tableau référencé par la variable de type Porte[] est modifié ?

PorteCoulissante[] coulissantes = new PorteCoulissante[3];

Porte[] portes = coulissantes;
portes[0] = new PorteCoulissante(66);
portes[1] = new PorteCharniere();

coulissantes[1].coulisser();

Les conteneur typé par PorteCoulissante contient une instance de PorteCharniere. Pour les tableaux, cette incohérence est détectée non pas à la compilation mais à l'exécution : la vérification est faite au moment de l'affectation avec la levée de l'exception java.lang.ArrayStoreException.

La variance des types paramétrés

Par défaut, la relation de type/sous-type du paramètre effectif n'est pas transmise à la classe paramétrée. C'est l'invariance.

Mais, pour permettre la factorisation de code, le langage Java permet de modifier la variance des classes paramétrées sous certaines contraintes qui vont assurer la cohérence du conteneur.

Invariance

Dans notre exemple, Vector<PorteCharniere> et Vector<PorteCoulissante> ne sont pas sous-type de Vector<Porte>

static void parcours(Vector<Porte> portes){
for(Porte p : portes)
if(p != null)
p.ouvrir();
}
//...
parcours(new Vector<Porte>());
parcours(new Vector<PorteCoulissante>()); //erreur à la compilation
parcours(new Vector<PorteCharniere>()); //erreur à la compilation

Covariance

Le type de la classe paramétrée varie de la même manière que le type du paramètre effectif. La covariance est indiquée par le mot-clé extends au niveau du paramètre formel. Pour accéder aux élémnets du conteneur, c'est la borne supérieure qui est fixée (les éléments sont récupérés à travers ce type) : <D extends B> toute classe D sous-type de la classe B ou <? extends B> toute classe sous-type de la classe B. Avec la covariance, le code de la méthode parcours() est valide pour des instance de Vector<PorteCharniere> et Vector<PorteCoulissante>.

static void parcours(Vector<? extends Porte> portes){
for(Porte p : portes)
if(p != null)
p.ouvrir();
}

static void <T extends Porte> autreEcriture(Vector <T> portes){
for(T p : portes)
if (p != null)
p.ouvrir();
}

//...

parcours(new Vector<Porte>());
parcours(new Vector<PorteCoulissante>());
autreEcriture(new Vector<PorteCharniere>());

Pour éviter une incohérence des éléments du conteneur (le problème rencontré avec les tableaux), la covariance ne permet pas la modification du conteneur passé en paramètre. Dans le code de la méthode parcours(Vector<? extends Porte> portes), l'instruction portes.add(new PorteCharniere()); provoque une erreur de compilation.

Contravariance

Pour factoriser le code qui ajoute un élément à un conteneur, il faut utiliser la contravariance. La contravariance est indiquée par le mot-clé super au niveau du paramètre formel. Pour modifier le conteneur, il faut fixer la borne inférieure du type accepté : <? super C> toute classe dont la classe C est le sous-type.

Par exemple, pour ajouter des instances de PorteCoulissante, il faut que le type paramétré soit au moins PorteCoulissante.

static void ajouter(Vector <? super PorteCoulissante> portes){
portes.add(new PorteCoulissante(999));
portes.add(new PorteCoulissante(666));
portes.add(new PorteCharniere()); //erreur à la compilation
}

ajouter(new Vector<Porte>());
ajouter(new Vector<PorteCoulissante>());
ajouter(new Vector <PorteCharniere>()); //erreur à la compilation

La contravariance interdit l'accès aux éléments du conteneur passé en paramètre; cela provoque une erreur à la compilation.

Bivariance

La bivariance est indiquée par le caractère joker seul <?>. Dans ce cas, toutes les classes sont acceptées. Il n'est pas possible de modifier le conteneur et le élément sont récupérés sous le type Object.

void bivariant(Vector<?> v){
v.add(new PorteCoulissante(999)); //erreur à la compilation
v.add("coucou"); //erreur à la compilation
Porte p1 = v.get(0); //erreur à la compilation

Porte p2 = (Porte) v.get(0);
Object o = v.get(0);
}