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