Année 2016-2017 Polytech’Paris-Sud – 4ème année Département Informatique « Polymorphisme et Généricité en Java » Frédéric Voisin Département Informatique Co-variance vs Contra-variance (rappel !) Soit Figures Id (X: Figures ) { … } dans Figures Dans Cercle on redéfinit la méthode Id en gardant son en-tête: Figures Cercle::Id (X: Figures ) { … } -- OK Peut-on lors d’une redéfinition spécialiser/élargir le type des paramètres ou du résultat ? Id Id Id Id (X: (X: (X: (X: Cercle) return Figures is … Figures) return Cercle is ... FigureAbstraite) return Figures is … Figures) return FigureAbstraite is … ----- KO OK OK KO Co-variance pour le résultat (ex : clone() en Java) Contra-variance pour les paramètres (pas implémenté en Java, vu comme une surcharge) 2016-2017 La généricité en Java 2 Polymorphisme paramétrique à la Ada (rappel) paramètres generic type Elem is private; type Table is array(integer range <>) of elem; with function "<="(E1,E2 : Elem) return boolean; procedure Tri(t: in out Table); – tri générique... Idée ? Pouvoir paramétrer une procédure ou un type par : des types, des procédures, des fonctions préciser les « propriétés » attendues de ces paramètres Instantiation explicite à la Ada ou implicite à la C++ pour associer un composant concret à un paramètre de généricité. procedure TriIntDecr is -- à la Ada new Tri(Elem => integer, "<=" => ">=", Table => TableInt); 2016-2017 La généricité en Java 3 La généricité à partir de Java 5.0 public class Paire<T1, T2> { // détail dans le prochain transparent protected T1 first; protected T2 second; public Paire(T1 a, T2 b) { first = a; second = b; } … } Paire<String, Integer> maPaire = new Paire<String, Integer>("a", 1); Paire<String, Integer> maPaire = new Paire<>("a", 1); // Java 7 ! Contraintes possibles sur le paramètre : public class C <T extends T1 & I1 & I2> { … } C<A> monObj; // OK si A sous-classe de T1 et implémente I1 et I2 Au plus une classe, forcément en tête, et des interfaces arbitraires; Si seulement des interfaces, l'ordre importe (voir après) ! 2016-2017 La généricité en Java 4 Une classe générique simple public class Paire<T1, T2> { private T1 first; private T2 second; public Paire() { first = null; second = null; } public Paire(T1 a, T2 b) { first = a; second = b; } public T1 first() { return first; } public T2 second() { return second; } public void setFirst(T1 val) { first = val; } public void setSecond(T2 val) { second = val; } … etc … } Ici, simple car il n'y a pas de contraintes sur T1 et T2. En pratique, moins simple à concevoir qu'on croit, si on veut qu'elle soit réellement réutilisable 2016-2017 La généricité en Java 5 Autres éléments génériques Java 5.0 Interfaces génériques interface Comparable<T> { int compareTo(T o); } public class C implements Comparable<C> { int compareTo(C c) {...} } Méthodes génériques (ici supposées dans une classe Utilitaires) public static <T> T anyOne(T[] tab)… // version provisoire de l'en-tête … public static <T extends Comparable<T>> T max(Collection<T> c) String[] maTable = { "Hello", "world" }; Collection<String> l = Arrays.asList("Hello", "world"); //Java 7 String s = Utilitaires.<String>anyOne(maTable); String s = Utilitaires.max(l); // <String> implicite 2016-2017 La généricité en Java 6 La généricité en Java : Principes de base Renforcer le typage statique et diminuer le besoin en transtypage explicite Compatibilité avec le code existant, d’où sous-typage « osé » avec contrôle dynamique à l’exécution et compatibilité avec la JVM Les librairies ont été mises à jour pour être génériques. La généricité est traitée uniquement à la compilation Pas de classe générique à l'exécution : la JVM reste non générique. Le compilateur convertit les génériques en non-génériques ● Ajout des contrôles statiques ● Ajout automatique et silencieux de transtypages Paire<String, String> ps; ps.setFirst(e); String s = ps.first(); À l’exécution une seule version de la classe, basée sur le « type limitatif » du paramètre : les paramètres de type sont « effacés » ! Différent donc du modèle Ada/C++ : Dynamiquement, il existe une unique version non générique 2016-2017 La généricité en Java 7 Exemples d'effacement de type class ArrayList<E> implements List<E>, Cloneable { … } ArrayList<E> où E est une classe arbitraire: la version « effacée » est équivalente à ArrayList<Object> public class Paire<T1, T2>: T1 et T2 n'ont pas de contraintes. la version « effacée » est Paire i.e. Paire<Object, Object> public class C <T extends C1 & I1 & I2, … > { … } la version « effacée » est C, qui représente C<C1> public class Intervalle <T extends Comparable> { … } la version « effacée » est Intervalle, i.e. Intervalle<Comparable> C++: Paire ou Paire<T1, T2> n’aurait pas de sens à l’exécution ! Java : Paire est une classe standard. Dynamiquement il existe une unique version non générique ! Paire p; – compile, mais représente quoi ? ☞ ArrayList ≠ ArrayList<Object> ≠ ArrayList< ? > 2016-2017 La généricité en Java 8 Polymorphisme générique et par inclusion (rappel) Figure Cercle 2016-2017 Set Set Figure X Set Cercle La généricité en Java Java 1.5 : comme C++ 9 Polymorphisme générique et par inclusion (2) Syntaxe à la Java mais même problème dans tout langage avec héritage+généricité public class Set<Elem> { public Set() { … } public void insert(Elem e){ … } … etc … }; par instantiation du paramètre, on peut obtenir les classes: Set<Figure> // définit void insert(Figure e) { … } Set<Cercle> // définit void insert(Cercle e) { … } static void teste(Set<Figure> s) { s.insert(new Rectangle()); } teste(new Set<Cercle>()); // OK ou KO ? // OK ou KO ? Avec OK partout on aurait une absurdité => où doit-on dire KO ? 2016-2017 La généricité en Java 10 Polymorphisme générique et par inclusion (3) Figure Set Set Cercle X Set Cercle Figure Cercle Set Set X Set 2016-2017 Figure Figure Java 1.5 ? Comme en C++ Java 1.5 : OK ?! Set<Figure> et Set<Cercle> n’existent pas dynamiquement mais Set existe ! Set Cercle La généricité en Java 11 Le poids du passé ... Java 5 : compatibilité délicate avec le code existant non générique static ArrayList f(ArrayList arg) { return arg; } // Java 4 ArrayList<Integer> a1 = new ArrayList<Integer>(); ArrayList<Object> a2 = new ArrayList<Object>(); a1 = f(a1); // OK ? a2 = f(a1); // OK ? a2 = a1; // OK ? a1 = a2; // OK ? a1 = (ArrayList<Integer>) a2; // OK ? a1 = f(a2); // OK ? a2.add("Sad world"); Integer i = a1.get(0); Dans la suite on s'en sert pour illustrer certains aspects ! Pas de problème de type si on ne mélange pas Java4- et Java5+ 2016-2017 La généricité en Java 12 Cas particuliers des tableaux (rappel) Figure[] tf = new Figure[2]; Cercle[] tc = new Cercle[2]; Rectangle r = new Rectangle(…); Figure f ; tf[0] = r; // OK tf = tc; // OK en Java ! tf référence donc un tableau de cercles... tf[0] = r; tf[0] = f; // KO à l’exécution(ArrayStoreException) // KO possible à l’exécution(ArrayStoreException) Les tableaux mémorisent dynamiquement le type d’éléments stockés. Contrôle explicite à l'exécution… et risque de levée d'exception ! Était-ce une bonne idée d'autoriser tf = tc ? La notion de tableaux est vue comme une opération covariante en Java 2016-2017 La généricité en Java 13 Quelques restrictions sur les génériques Pas de classe générique d’exceptions. Pas de type primitif dans les instanciations : Paire<int, double> p; // KO : serait Paire<Object, Object> Paire<Integer, Double> p; // OK Inconvénient allégé par « boxing/unboxing » automatique ! Perte de fiabilité des informations dynamiques de type ArrayList<Integer> ai = new ArrayList<Integer>(); ArrayList<String> as = new ArrayList<String>(); ArrayList a = ai; // on passe par du Java4ai = (ArrayList<Integer>) a; // OK ? as = (ArrayList<String>) a; // OK ? as = (ArrayList<String>) ai; // KO ! ! instanceof est interdit sur les classes paramétrées : dynamiquement, pas de différence entre Paire<String,String> et Paire<Integer,Integer> 2016-2017 La généricité en Java 14 Quelques restrictions (suite) Pas de tableaux d'instantiation de génériques Paire<String, Integer>[] t = … // KO pb avec l'effacement de types et la gestion de ArrayStoreException Pas d’objet d’un type paramètre public class Paire<T1, T2> { public static Paire<T1, T2> getFirst() { return new T1(...); // le new est dynamique : créerait quoi ? } } Pas de variable de type générique pour méthodes ou attributs static : public class Singleton<T> { static T theInst; // KO. static T getInst() { … } // KO. } Ferait un drôle de Singleton après instantiation ! 2016-2017 La généricité en Java 15 Retour sur un exemple simple... public class Paire<T1, T2> { private T1 first; private T2 second; public Paire() { first = null; second = null; } public Paire(T1 a, T2 b) { first = a; second = b; } public T1 first() { return first; } public T2 second() { return second; } public void setFirst(T1 val) { first = val; } public void setSecond(T2 val) { second = val; } } Que se passe-t-il avec l'effacement de type ? 2016-2017 La généricité en Java 16 Après effacement ... Tout se passe comme si dans la JVM il y avait en fait: public class Paire { private Object first; private Object second; public Paire() { first = null; second = null; } public Paire(Object a, Object b){ first=a; second=b;} public Object first() { return first; } public Object second() { return second; } public void setFirst(Object v) { first = v; } public void setSecond(Object v) { second = v; } } sauf que le compilateur ajoute contrôles et transtypages dans votre code pour garantir le typage sûr. 2016-2017 La généricité en Java 17 Une utilisation de Paire static int teste(Paire<Integer, Integer> pi) { return pi.first() + pi.second(); // Compile ? } static void t2(Paire<Integer, Integer> pi) { pi.second(); } public static void main(String[] argv) { Paire<Integer> pi = new Paire<Integer, Integer>(1, 2); Paire p = pi ; // OK ! teste(pi); pi.setSecond("Coucou"); // KO à la compilation p.setSecond("Coucou"); // OK ou KO ? teste(pi); // OK ou KO ? t2(pi) ; // OK ou KO ? } La « vraie » forme de setSecond à l'exécution est : public void setSecond(Object val) { second = val; } 2016-2017 La généricité en Java 18 Une sous-classe de Paire public class PaireInteger extends Paire<Integer, Integer> { public setSecond(Integer i) { // redéfinition ou pas ? … super.setSecond(2*i) ; } } Avec l'effacement de types, s'agit-il d'une redéfinition ou une surcharge ? Dans la JVM pour PaireInteger il y a deux méthodes setSecond : public void setSecond(Object v) – héritée de Paire<Integer,Integer> public void setSecond(Integer i)– locale ! PaireInteger pi = new PaireInteger(); Paire p = pi ; – OK ? p.setSecond(1); – Quelle méthode setSecond ? System.out.println(pi.first() + pi.second()); – OK ? Bien sûr, si on ne passe jamais par Paire on ne se pose pas la question. 2016-2017 La généricité en Java 19 Méthodes « bridges » (1) But : Régler le problème lié à l'effacement de type et assurer la cohérence avec le polymorphisme... Pour rétablir le polymorphisme, le compilateur ajoute une méthode (« bridge ») dans la classe PaireInteger qui redéfinit la version qu'on hériterait par défaut de la classe générique et fait le lien avec la méthode surchargée! public void setSecond(Object v) { this.setSecond((Integer) v); // Cast risqué ou pas? } Méthode ajoutée automatiquement par le compilateur, non référençable par le programmeur (n’existe que dans le bytecode, mais visible via introspection) 2016-2017 La généricité en Java 20 Méthodes « bridges » (2) Même problème pour gérer la covariance du type de retour ! public class A { public A f() { return new A(); } public class B extends A { public B f() { B res1 = this.f(); A res2 = super.f(); B res3 = ((A) this).f(); A res4 = ((A) this).f(); } // Redéfinition? Surcharge ? // Boucle à l’exécution: Normal. // OK, version A de f appelée // Compile pas: OK. // Compile mais exécute quoi ? // “caster” est une notion statique ! } } Génération automatique d’une méthode “bridge” dans B public A f() { return this.f();} Deux méthodes f() sans paramètre dans (la version JVM de) B: une à valeurs dans A, l'autre à valeurs dans B. 2016-2017 La généricité en Java 21 Écriture de méthodes/classes génériques La « non-compatibilité » entre instantiation et héritage complique l’écriture de méthodes réutilisables … static void dessineTout (Collection<Figure> c) { for(Figure f: c) f.dessine(); // OK } Collection<Cercle> ac = new ArrayList<>(); // Java 7.0 dessineTout(ac); // KO ! dessineTout((Collection<Figure>) ac); // KO ! Pourtant dessineTout ne met pas en danger le typage Avec les librairies génériques (Collection, Comparable, etc.), cela reste généralement transparent ! Merci les concepteurs des bibliothèques ! Attention à la spécification parfois surprenante des paramètres ;-| Il faut des mécanismes pour différencier les cas « problématiques » 2016-2017 La généricité en Java 22 Tentatives de solutions… Retour à la version 4 ? Perte d'information statique = risque d'exceptions dynamiques public static void dessine (Collection c) { for(Object o: c) ((Figure) o).dessine() ; } Passage par une procédure générique ? public static <T extends Figure> void dessine (Collection<T> c) { for(Figure f: c) f.dessine(); } artificiel : dessine n'a pas besoin d'être générique. ☞ Introduction de « joker » (wildcard) dans la description des paramètres ou du résultat d'une méthode pour mieux faire cohabiter instantiation et héritage ! 2016-2017 La généricité en Java 23 Types « Joker » Possibilité d’avoir des types « joker » bornés inférieurement ou supérieurement : < ? > correspond à « n’importe quel type » (mais pas un type précis comme avec une méthode générique) < ? extends Figure> n’importe quel sous-type de Figure (inclus) < ? super Figure> n’importe quel super-type de Figure (inclus) Différentes usages/limitations associés à ces types joker Notions similaires en Scala, liées à covariance/contravariance 2016-2017 La généricité en Java 24 Hiérarchies de sous-typage avec Joker… Collection Collection<Object> Collection<?> Collection<? extends Figure> super: contravariant ! Collection<? super Figure> extends : covariant ! Collection<Cercle> Collection<Figure> : « est sous-type de » List<Cercle> List<E> extends Collection<E> { … } 2016-2017 La généricité en Java – OK ! 25 Hiérarchies de sous-typage avec Joker… ArrayList ArrayList<?> class ArrayList<E> implements Collection<E> { boolean add(E elt); E get(int i) ; } Compatibilité avec Java 1.4 : typage et contraintes plus « souples » pour ArrayList que pour ArrayList<?> et ArrayList<Object> public static void f(ArrayList<?> arg, Object o) { arg.add(o); // KO: pas de type spécifique à la place de E } public static void f(ArrayList arg, Object o) { arg.add(o); // perte d’information statique // donc recours à des cast… comme en 1.4 } Distinction entre positions covariantes et contra-variantes... 2016-2017 La généricité en Java 26 Restrictions pour les Joker f(ArrayList<? extends Figure> arg) class ArrayList<E> implements Collection<E> { boolean add(E elt); E get(int i) ; } Si une méthode de ArrayList<E> prend un paramètre de type E on ne peut pas passer un type spécifique (Figure, Cercle, …) dans la version instantiée. Par contre si une méthode renvoie un résultat de type E, on peut l’utiliser là où on peut utiliser une instance de type Figure ! f(ArrayList < ? extends Figure> arg): covariant => interdit l'usage de arg en position contravariante (donc comme paramètre). « ? extends Figure » représente un sous-type inconnu de Figure et ne peut pas correspondre à une instance d’un type spécifique. On ne peut que parcourir la collection, pas la modifier (sauf par des méthodes comme clear qui ne se soucient pas du type des objets stockés) Dans Collection de nombreuses méthodes (contains, remove, …) ne prennent par un E en paramètre mais un Object. Donc « ça compile» ! public static void f(ArrayList<Figure> a, Object o) { a.contains(o);} 2016-2017 La généricité en Java 27 Restrictions pour les Joker (2) ArrayList<? super Figure> : restrictions duales… Pas d'usage en position contravariante, seulement en position covariante. Ajouter à une ArrayLst les éléments d’une ArrayList : static void toList(ArrayList<? extends Figure> src, ArrayList<? super Figure> dst) { for(Figure f: src) dst.add(f); // OK. } On peut parcourir src et modifier dst ! ArrayList<Cercle> ac = new ArrayList<Cercle>(); Array<Object> os = new ArrayList<Object>(); ac.add(new Cercle()); ac.add(new Cercle()); os.add("coucou"); os.add(1); toList(ac, os); // OK : introduit les deux cercles dans os. 2016-2017 La généricité en Java 28 Joker ? Méthode paramétrée ? static <T> ArrayList<T> add(ArrayList<T> al, ArrayList<? super T> l) { pour l'instant l ne sert pas ! ArrayList<T> r = new ArrayList<T>();// T ? viendra d'où ? Compile ? for(T e: al) r.add(e); // Compile ?? à l’exécution ?? return r; } static <T> ArrayList<T> add2(ArrayList<? extends T> al, ArrayList<T> l){ ArrayList<T> r = new ArrayList<T>(); // T ? viendra d'où ? Compile ? for(T e: al) r.add(e); // Compile ?? return r; } ArrayList<Figure> af = new ArrayList<>(); ArrayList<object> ao = new ArrayList<>(); af.add(New Cercle()); af.add(new Rectangle()); ao.add("coucou"); ao.add(1); 2016-2017 La généricité en Java 29 Pour voir si on a compris ;-( Qu’est-ce qui est correct ? Il faut vérifier les appels à add et add2 ainsi que l’affectation ArrayList<Figure> s1 = add(af, ao); // OK ou KO ? ArrayList<Object> s2 = add(af, ao); // OK ou KO ? ArrayList<Figure> s3 = add2(af, ao); // OK ou KO ? ArrayList<Object> s4 = add2(af, ao); // OK ou KO ? 2016-2017 La généricité en Java 30 interface Comparator<E> { class TreeSet<E> { ? super again int compare(E o1, E o2); TreeSet(Comparator c) … } } TreeSet : Nécessite une relation d’ordre sur E ou un comparateur de E Usage ? TreeSet<Cercle> s = new TreeSet(unComparateur); Quels comparateurs peut-on utiliser pour unComparateur ? ☞ Ça dépend de ce qu’a prévu le concepteur de TreeSet ! Version naïve : TreeSet(Comparator<E> c) { … } ☞ int compare(Cercle o1, Cercle o2); N'accepte que les comparateurs entre cercles :-( Version améliorée : TreeSet(Comparator<? super E> c) { … } accepte aussi un comparateur plus général qu'on aurait défini: int compare(Figure o1, Figure o2); ou 2016-2017 int compare(Object o1, Object o2); La généricité en Java 31 max revisitée (cf transparent 7) static <T extends Comparable<T>> T max(Collection<T> tab) { … } Trop restrictif : impose à C une méthode int compareTo(C arg) alors qu’on dispose peut-être dans C de int compareTo(Object arg) public class C implements Comparable { // ou Comparable<Object> ? public int compareTo(Object o) { … } } Collection<C> maTab = … ; C monC = max(maTab); // KO Une meilleure interface de max ? static <T extends Comparable<? super T>> T max(Collection<T> tab) L’interface exacte de max ? static <T extends Object & Comparable<? super T> > T max(Collection<? extends T> tab) Sinon la version « effacée » serait : Comparable max(Collection arg) La généricité en:Java or 2016-2017 la version Java 4 a pour en-tête Object max(Collection arg) 32 Pour résumer (Java) Les librairies sont bien conçues et peuvent être réutilisées facilement mais … Paramètres parfois surprenants : ex. dans Collection<E> méthode contains(Object e) plutôt que contains(E e) Méthodes « bridges » qui peuvent être nécessaires pour avoir le « comportement attendu » du polymorphisme Le mélange avec du code 4.0 réintroduit les risques d'exceptions à l'exécution Concevoir une classe générique réutilisable ? Nécessite de bien maîtriser les types « Joker » Méthode générique d’une classe standard vs méthode standard d’une classe générique ? 2016-2017 La généricité en Java 33 Que manque-t-il encore ? Passage de méthodes en paramètre (« Lambda expressions » Java 1.8) Plus de flexibilité pour gérer co- et contra-variance à la Scala. Figure Cercle Set Set Figure X Set Cercle Le problème vient que insert de Set<Figure> peut insérer une Figure arbitraire qui polluerait un Set<Cercle> Plus de modification d’un ensemble => plus de problème ! Déclaration de la variance (l'usage attendu !) au niveau du paramètre, pas globalement ! Par exemple, décrire l'usage effectué de c dans dessineTout(Collection<Figure> c) 2016-2017 La généricité en Java 34