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 interfacesCollection
etIterator
).
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);
}