7. LA GENERICITE
 
 
Précédent Suivant
 
La généricité
Héritage vs. généricité
* La généricité est une notion récente en Java
* Présente depuis la version 1.5
* La généricité est complémentaire de l'héritage
* Contribuent tous les deux à développer du code générique
* Avec l'héritage
* Plus de flexibilité, mais moins de sûreté
* Contrôles de type effectués à l'exécution
* Peut entraîner des ralentissements significatifs
* Avec la généricité
* Moins de flexibilité, mais plus de sûreté
* Contrôles de type effectués à la compilation
* Moins de ralentissement (voire aucun) à l'exécution
Qu'est-ce que la généricité ?
* Dans une fonction, les paramètres sont des valeurs
* Dans sa définition, des valeurs sont inconnues
* Au moment de l'appel, ces valeurs sont fixées
* Dans un générique, les paramètres sont des types
* Dans sa définition, des types sont inconnus
* Au moment d'utiliser le générique, ces types sont fixés
* Un générique est un modèle
* Instanciation = création d'un élément à partir d'un modèle
* Instancier un générique ==> fixer le type de ses paramètres
* Composants pouvant être génériques en Java
* Classes, interfaces et méthodes
Motivation de la généricité
* Initialement: les structures de données génériques
* Sans la généricité
* Collection générique = collection d'objets de type "Object"
* Tout objet de type "Object" peut être ajouté dans la collection
* Sans contrôle préalable ==> contenu hétérogène
* Avec la généricité
* Collection générique = collection d'objets de type "T" (à définir)
* Seuls des objets de type "T" seront ajoutés dans la collection
* Contrôle préalable automatique ==> contenu homogène
* Mais plus récemment: la "programmation générique"
* Utilisation de la généricité pour les algorithmes
* Par exemple: les foncteurs en C++
* Version générique du design pattern visiteur
* Exploitation de l'instanciation partielle
* Une nouvelle forme de polymorphisme
Collection "générique" avec l'héritage
* Aucune précision du type des éléments contenus dans la collection
* ArrayList personnes = new ArrayList();
* L'ajout ne pose aucun problème
* Personne p1 = new Personne("Nawouak");
* personnes.add(p1); ==> ok, car "Personne" hérite de "Object"
* L'extraction est plus compliquée
* Personne p2 = personnes.get(0);
* Problème: "get" retourne un "Object"
* Obligation de faire un "downcast"
* Personne p2 = (Personne)personnes.get(0);
* Problème: en cas d'erreur, la détection ne se fera qu'à l'exécution
Collection "générique" avec la généricité
* Définition du type précis des éléments contenus dans la collection
* ArrayList<Personne> personnes = new ArrayList<Personne>();
* Toutes les vérifications sont faites à la compilation
* Personne p1 = new Personne("Nawouak");
* personnes.add(p1); ==> aucun problème
* Personne p2 = personnes.get(0); ==> aucun problème
* Intérêt
* Code générique / réutilisable
* Vérification des types à la compilation
* Evite les downcasts ==> moins de contrôle à l'exécution
Syntaxe pour la généricité
* Classe générique
* class nom_classe<liste_paramètres> { ... }
* Paramètres séparés par des virgules
* class Paire<T1,T2> { ... }
* Interface générique
* interface nom_classe<liste_paramètres> { ... }
* interface Collection<T> { ... }
* L'héritage est possible
* class ListeChaine<T> implements Collection<T> { ... }
* Méthode générique
* modifieurs <liste_paramètres> type_retour nom_méthode(...) { ... }
* class Collection<T> {
...
public <U> void copier(Collection<U> c) { ... }
}
Exemple de classe générique
public class Paire <T1,T2> {
protected T1 premier;
protected T2 second;
public Paire(T1 p,T2 s) {
premier = p;
second = s;
}
public T1 getPremier() { return premier; }
public T2 getSecond() { return second; }
public void setPremier(T1 p) { premier = p; }
public void setSecond(T2 s) { second = s; }
}
Instanciation d'une classe générique
* Instanciation ==> fixer le type des paramètres
* Instanciation impossible avec un type primitif
* Paire<int,double> p; ==> interdit
* Instanciation avec des classes uniquement
* Paire<Integer,Double> p;
* p = new Paire<Integer,Double>(7,3.5);
* Auto-"boxing"/"unboxing" depuis Java 1.5
* Encapsulation d'un primitif dans une classe
* Objet nécessaire: conversion automatique primitif ? classe
* p.setPremier(27); ==> ok
* Primitif nécessaire: conversion automatique classe ? primitif
* int i = p.getPremier(); ==> ok
Héritage avec généricité
* interface List<T> {
...
public void add(int index, T element);
public T get(int index);
}
* class ArrayList<T> implements List<T> {
...
public void add(int index, T element)
{ ... }
public T get(int index)
{ ... }
}
Attention aux contre-intuitions (1/2)
* ArrayList<String> hérite de List<String> ?
* List<String> liste = new ArrayList<String>();
Attention aux contre-intuitions (2/2)
* ArrayList<String> hérite de ArrayList<Object> ?
* Si c'était le cas...
* ArrayList<Object> liste = new ArrayList<String>();
* liste.add(0,new Object()); ==> doit être interdit
Méthodes génériques et polymorphisme
* Méthode générique: affichage du contenu d'une liste
public static <T> void afficher(List<T> liste) {
Iterator<T> i = liste.iterator();

while (i.hasNext()) System.out.println(i.next());
}
* Utilisation de la méthode générique
ArrayList<Integer> liste = new ArrayList<Integer>();
...
liste.add(...);
...
afficher(liste);
* Instanciation implicite ==> polymorphisme statique
* Le compilateur tente de déduire les types des paramètres
* A partir des types des arguments de la méthode
* Il est possible de forcer l'instanciation: <String>afficher(liste);
Les "concepts" (1/2)
* Algorithme de tri en C++
template <typename T>
void AlgoTri<T>::trier(T t[],int n) {
for (int i = 0; i < n-1; i++)
for (int j = i+1; j < n; j++)
if (t[j].estAvant(t[i]))
{ T x = t[i]; t[i] = t[j]; t[j] = x; }
}
* Hypothèse: le type "T" possède la méthode "estAvant"
* Vérification faite à la compilation, au moment de l'instanciation
* L'interface supposée de "T" est appelée un "concept"
Les "concepts" (2/2)
* Dans l'algo, "T" doit respecter le concept "Comparable"
* On dit: "T" modélise le concept "Comparable"
* En C++, les concepts sont pour l'instant implicites
* Seule une documentation permet de les identifier
* Voir la documentation de la STL par exemple
Les types contraints (1/2)
* Algorithme de tri en Java
class AlgoTri<T> {
public void trier(T t[]) {
for (int i = 0; i < t.length-1; i++)
for (int j = i+1; j < t.length; j++)
if (t[j].estAvant(t[i]))
{ T x = t[i]; t[i] = t[j]; t[j] = x; }
}
}
* On ne suppose pas que "T" possède la méthode "estAvant"
* On impose que "T" implémente une interface "Comparable"
* Mot-clé "extends"
* class AlgoTri<T extends Comparable>
Les types contraints (2/2)
* Concepts implémentés en Java par les "types contraints"
* Un type paramètre est contraint par un sous-type
* Une classe fille est un sous-type de sa classe mère
* Une classe est un sous-type d'une interface qu'elle implémente
* Une classe est sous-type d'elle-même
* Donc attention ! "extends" ne signifie pas "héritage" ici
* Indique que "T" est un sous-type de "Comparable"
* Si "Comparable" est une classe
* Soit "T" est la classe "Comparable"
* Soit "T" est une sous-classe de "Comparable"
* Si "Comparable" est une interface
* "T" implémente l'interface "Comparable"
* Possibilité d'imposer plusieurs sous-types: séparateur "&"
* <T1 extends Comparable & Clonable,T2>
* Il ne peut y avoir qu'une classe, et elle doit se trouver en tête
Le type paramètre "joker" (1/3)
* "?" désigne un type paramètre fixé mais inconnu
* On peut le contraindre
* <? extends Comparable>
* Désigne un type fixé inconnu qui est sous-type de "Comparable"
* Un seul sous-type peut contraindre le joker
* Souvenez-vous...
* ArrayList<Object> liste;
* liste = new ArrayList<String>(); ==> interdit
* Le joker est alors utile
* ArrayList<? extends Object> liste;
* liste = new ArrayList<String>(); ==> autorisé
Le type paramètre "joker" (2/3)
* Attention ! "?" n'est pas remplacé par "String"
* "?" est seulement un sous-type de "Object"
* Object o = liste.get(0); ==> autorisé
* Quand "?" est attendu, rien ne peut le remplacer
* liste.add(new String()); ==> interdit
* Le joker peut aussi servir à contraindre
* <T extends Collection<?> >
* Contraint "T" à être un sous-type d'une instance de "Collection"
* "?" est un type paramètre sans nom
* Possibilité d'écrire: <T extends Collection<U>,U>
* Mettre "?" à la place des types paramètres qui ne sont pas manipulés
Le type paramètre "joker" (3/3)
* Objectif: une méthode qui affiche toute liste d'objets
* Avant les génériques
* void afficher(List liste) {
Iterator i = liste.iterator();
while (i.hasNext()) System.out.println(i.next());
}
* Avec les génériques, on aimerait
* void afficher(List<Object> liste)
{ for (Object o : liste) System.out.println(o); }
* A noter: une nouvelle syntaxe "for" depuis Java 1.5
* Problème: cette méthode est incorrecte
* Toujours à cause du même problème d'héritage
* List<Object> n'est pas une super-classe pour List<...>
* Avec les génériques, voici la bonne solution
* void afficher(List<?> liste)
{ for (Object o : liste) System.out.println(o); }
Instanciation d'un générique en C++
* Classe générique ~ modèle de classe ==> pas de code binaire
template <typename T> class Liste {
protected: T * tab;
public: void set(int i,const T & e) { tab[i] = e; }
public: const T & get(int i) { return tab[i]; }
...
};
* A chaque instanciation: code recopié et "T" remplacé
class Liste<string> {
protected: string * tab;
public: void set(int i,const string & e) { tab[i] = e; }
public: const string & get(int i) { return tab[i]; }
...
};
* Une classe par instanciation différente ==> un code binaire par instance
* Peut engendrer des codes binaires importants
* Efficacité code généré = efficacité code dédié
Instanciation d'un générique en Java (1/2)
* Classe générique Java
public class Liste<T> {
protected T[] tab;
public void set(int i,T e) { tab[i] = e; }
public T get(int i) { return tab[i]; }
...
}
* A la compilation
* Contraintes de type vérifiées
* Informations de généricité retirées ==> mécanisme "type erasure"
* Conversions ajoutées pour garantir la cohérence
* Mécanisme "type erasure"
* Remplacement des types paramètres par leur sous-type
* Si aucun sous-type ==> remplacement par "Object"
* class Liste<T> ==> "T" remplacé par "Object"
* class Liste<T extends Integer> ==> "T" remplacé par "Integer"
Instanciation d'un générique en Java (2/2)
* Transformation de la classe générique
public class Liste {
protected Object[] tab;
public void set(int i, Object e) { tab[i] = e; }
public Object get(int i) { return tab[i]; }
...
}
* Transformation de l'instanciation
* Code source
Liste<Integer> liste = new Liste<Integer>();
liste.set(0,new Integer(5));
Integer i = liste.get(0);
* Code compilé
Liste liste = new Liste();
liste.set(0,new Integer(5));
Integer i = (Integer)liste.get(0);
* Une classe pour toutes les instanciations ==> code binaire unique
* Génère moins de code que C++
* Compatibilité avec ancien code (cf. conteneurs génériques)
* Efficacité du code limitée car héritage + downcast
Remarques sur les tableaux (1/2)
* Impossible de créer un tableau d'objets d'un type paramètre
* T[] tab = new T[100]; ==> interdit
* Type paramètre remplacé au moment du "type erasure"
* Impossible donc d'utiliser un type paramètre à l'exécution
* Une solution peu "propre" existe
* Si "T" n'est pas contraint
* T[] tab = (T[])(new Object[100]);
* Si "T" est contraint
* Par exemple: T extends Integer
* T[] tab = (T[])(new Integer[100]);
* Conversion ==> alerte "unchecked cast" à la compilation
* A partir de Java 1.6, cette alerte peut être masquée
* @SuppressWarnings("unchecked")
* Mais pourquoi ça marche ?
* "T" remplacé par "Object" lors du "type erasure"
* tab = (Object[])(new Object[100]);
Remarques sur les tableaux (2/2)
* Conseil: centraliser la création de tableau dans une méthode
* @SuppressWarnings("unchecked")
protected T[] newArray(int n)
{ return (T[])(new Object[n]); }
* Facile de remplacer le code le jour où une solution "propre" existe
* Une seule annotation est nécessaire pour masquer l'alerte
* La redéfinition de "newArray" peut aussi être nécessaire
class ListeTriee<T extends Comparable> extends Liste<T> {
...
@SuppressWarnings("unchecked") protected T[] newArray(int n)
{ return (T[])(new Comparable[n]); }
}
* Explications
* "tab" est déclaré comme "Object[]" dans "Liste"
* "tab" est considéré comme "Comparable[]" dans "ListeTriee"
* A la compilation, une conversion est donc ajoutée: (Comparable[])tab
* A l'exécution, il y tentative de conversion: Object[] ? Comparable[]
* Mais cette conversion est impossible ==> exception
En conclusion: différences avec C++
* Avec Java, contraintes de compatibilité et de cohabitation
* L'ancien code doit toujours fonctionner
* Ancien et nouveau codes doivent fonctionner ensemble
* La généricité en Java est bien différente de la généricité en C++
* L'accent n'a pas été mis sur l'efficacité du code
* Java intègre les concepts sous la forme des sous-types
* Pas de multiplication du code: un seul code pour toutes les instances
* L'instanciation partielle n'est pas présente en Java
* L'objectif est de fournir un code plus sûr
* Contrôle des types plus avancé
* Mais cela implique des interdits
* A suivre...
* Concepts en C++ (avec le standard C++0x)
* Instanciation partielle en Java ?
 
 
Copyright (c) 1999-2016 - Bruno Bachelet - bruno@nawouak.net - http://www.nawouak.net
La permission est accordée de copier, distribuer et/ou modifier ce document sous les termes de la licence GNU Free Documentation License, Version 1.1 ou toute version ultérieure publiée par la fondation Free Software Foundation. Voir cette licence pour plus de détails (http://www.gnu.org).