Introduction aux types paramétrés (au travers des Collections)

images/whitebelt.png

Collections avec Object et le problème du typage

Certains objets sont des structures de données bâties pour stocker d’autres objets. Ces structures sont conçues pour avoir des modes d’accès privilégiés de nature différente.

Par exemple dans le package java.util :

ArrayList
est une sorte de tableau "extensible" (la taille grossit automatiquement en fonction des besoins). C’est une structure conforme au contrat de l’interface List (les objets sont stockés les uns derrière les autres) et elle a une caractéristique de type RandomAccess ("accès direct": on peut directement accéder à l’objet d’index x).
LinkedList
conforme List mais l’accès est séquentiel (l’accès à l’objet d’index x est proportionnel à x).
HashMap
est analogue à un dictionnaire: les objets sont stockés et recherchés en utilisant une clef (le stockage n’est pas ordonné, mais la recherche par clef est efficace -technique des tables de hachage-)

Dans la mesure où ces classes ont été baties en tant qu’utilitaires généralistes avant la version 5 de java elles étaient basées sur le type Object pour décrire les éléments contenus et les services afférents.

Dans les versions précédant la version 5 on avait pour ArrayList des méthodes comme boolean add(Object o) ou Object get(int index).

Bien que cela ait été nécessaire avec les caractéristiques des versions de java antérieure à la version 5 cela constituait un risque potentiel puisque le contrôle de type était circonvenu:

Prenons un exemple d’incident potentiel lorsque plusieurs programmeurs collaborent à la réalisation d’un application (et que, par inadvertance, un programmeur se trompe) :

   // code généraliste écrit par  le programmeur Marius
   public void completerRéunion(ArrayList réunion){
      réunion.add(RH.getEmployeDuMois()) ;
      //....
   }
   // code circonstanciel écrit par Fanny
   RH.setEmployeDuMois(petitGarsQuiTrieLeCourrier) ;
   // cet humble employé doit être récompensé pour 30 ans de service!
   // code généraliste écrit par  le programmeur Olive
   ArrayList conseilAdministration = new ArrayList() ;
   // on commence à remplir
   // puis ...
   RH.completerRéunion(conseilAdministration) ;
   // ....
   for(int ix=0; ix <conseilAdministration.size(); ix++) {
      Manager man = (Manager) conseilAdministration.get(ix) ;
      /* _runtime_exception_ quand on arrive au facteur! */
   }

Pris individuellement ces trois codes sont corrects, mais dans certaines circonstances un incident grave peut se produire quand on les combine! Le dernier code peut, au cours d’une invocation particulière, tomber en erreur! Il réalise un transtypage (on dit aussi "forçage de type") pour indiquer au compilateur que l’objet doit être considéré comme étant de type Manager. Si l’objet en question ne peut correspondre à aucun de contrats de type auquel il souscrit (par héritage ou réalisation d’une interface) une erreur de RunTime se produit!

Contrôle de type par réalisation d’une classe spécifique

Auparavant les programmeurs se prémunissaient contre ce genre d’incident en écrivant des codes spécifiques comme celui-ci:

public class Conseil {
   private ArrayList list = new ArrayList() ;

   public boolean add(Manager man) {
      return list.add(man) ;
   }

   public Manager get(int index) {
      return (Manager) list.get(index) ;
   }
[Note]

Il est absolument nécessaire d’utiliser la délégation et pas l’héritage dans ce cas (on ne doit pas écrire Conseil extends ArrayList).

Pourquoi?

Et bien parce qu’en définissant une méthode add(Manager man) on opére ainsi un surcharge pas une spécialisation ! La méthode add(Object obj) est héritée, existera toujours et donc peut être utilisée à mauvais escient!

Quand on regarde ce code on peut constater que toutes les classes spécifiques qui permettent de faire ce contrôle de type sont bâties selon le même plan. Au lieu d’encombrer nos librairies avec ces classes on peut, à partir de java 5, utiliser un type paramétré.

Contrôle de type avec un type paramétré

Une Collection comme ArrayList est maintenant définie comme un type paramétré (generics).

doc pour ArrayList noter que:

  • ArrayList est définie comme ArrayList<E> (ArrayList de "quelque chose" - la "variable-type" est nommée E-)
  • La méthode "add" est boolean add(E e) (elle prend ce "quelque chose" en paramètre)
  • La méthode "get" est E get(int index) (il est garanti qu’elle renvoie une instance de ce "quelque chose")

Utilisation d’un type paramétré. 

   ArrayList<Manager> conseil = new ArrayList<Manager>() ;

   // à partir de java 7 inférence de type possible :
   //    ArrayList<Manager> conseil = new ArrayList<>() ;

   conseil.add(pdg) ;
   // ok si "pdg" est de type Manager
   // ou de type PDG extends Manager

   conseil.add(RH.getEmployeDuMois()); /* _compiler_error_*/
   // "employé du mois" est de type Employe !!!

   ArrayList<Manager> conseil = new ArrayList<Manager>() ;
   // fill
   Manager premierManager = conseil.get(0) ; // pas de Transtypage !!!

   for(Manager man : conseil) {
      //****
      man.getSubordonnés() ;
   }

Noter que :

  • L’utilisation d’un type paramétré provoque des contrôles de compile time (l’information est conservée mais l’implantation dans le pseudo-code est toujours de type Object: les codes anciens sont compatibles [grosse différence avec les templates de C++]).

    Cette opération, qui consiste à faire remplacer par le compilateur le type-paramètre par Object, est un "effacement" (erasure).

    Du fait de ces mécanismes on ne peut pas (au niveau de la version 9 de java) utiliser un type primitif scalaire comme réalisation d’un paramètre type.

  • On n’a pas besoin de transtyper le résultat de la méthode get
  • La boucle foreach opére correctement sur cet objet qui n’est pourtant pas un tableau! … Comment cela est-il possible?

Enchainement des contrôles de compile time

  • ArrayList<E> implements List<E>, RandomAccess, Cloneable, Serializable
  • List<E> extends Collection<E> (les interfaces peuvent hériter les unes des autres -une interface peut d’ailleurs hériter de plusieurs autres: les contrats se complètent-)
  • Collection<E> extends Iterable<E>
  • Le contrat Iterable<T> implique une méthode: Iterator<T> iterator()
  • Iterator<E> (encore une interface) implique les méthodes :

    • boolean hasNext() (Y a t’il un autre élément dans la structure "contenante"?)
    • E next() (donnez le moi -et, tant qu’on y est, le type de cet élément a été controlé tout au long de la chaîne: donc si nous avons une ArrayList<Manager> alors next() renvoie un Manager!-)
  • La boucle foreach opére sur des objets Iterable (le compilateur génère l’appel à iterator() et les appels à hasNext() et next() ; il contrôle la validité des types au compile-time).

Définition d’un type paramétré

images/orangebelt.png

Un extrait des sources d'ArrayList:

public class ArrayList<E1> extends AbstractList<E2>
            implements List<E3>,°°°{
   private transient E4[] elementData;
   public E5 set(int index, E6 element) {
      // ....
      E7 oldValue = elementData[index];
      elementData[index] = element;
      return oldValue;
   }
   //....

1

déclaration de la variable-type formelle

2

mise en place de la variable-type comme type-paramètre de la super-classe

3

mise en place de la variable-type comme type-paramètre d’une interface à réaliser

4

mise en place de la variable-type comme type d’un membre.

5

mise en place de la variable-type comme type de retour d’une méthode

6

mise en place de la variable-type comme type d’un paramètre de méthode

7

mise en place de la variable-type comme type d’une variable locale

Une interface paramétrée. 

public interface CatalogueAccesDirect<X> extends Iterable<X>, RandomAccess {

   public X get(int index) throws IndexOutOfBoundsException ;

   // beaucoup plus complexe :
   // Iterator<X> get(Requete<X> requete) throws Exception
}

Ici on a fait appel à un héritage entre interfaces: CatalogueAccesDirect a tous les éléments du contrat de l’interface Iterable et rajoute des capacités. (l’interface "marqueur" RandomAccess indique qu’on s’engage à ce que la recherche avec un index soit tout à fait performante)

On pourra créer des CatalogueAccesDirect de "quelque chose" et un code "client" d’un tel objet saura que faire.

Variable-type propre à une méthode

méthode "asList" de java.util.Arrays

de signature :

static <T> List<T>    asList(T... a)

Ici le compilateur va garantir une cohérence entre le type-paramètre attribué à la liste rendue en résultat et les divers paramètres (ou le type du tableau) passés en argument (voir notation "…" et les méthodes varargs).

   List<Manager> conseil = Arrays.asList(pdg, chefComptable, chefMarketing ) ;
[Avertissement]Attention!
  • La manipulation de Arrays.asList peut parfois nécessiter des dispositifs syntaxiques qui sortent du cadre de ce chapitre. ("induction" de type : phénomène très complexe).
  • On ne peut pas utiliser une variable-type liée à une classe dans une partie statique du code.
  • Pour l’instant on ne peut pas créer une Exception paramétrée

Invariance

images/orangebelt.png

Soit un code générealiste:

   // code généraliste écrit par  le programmeur Marius
   public void completerRéunion(ArrayList<Employe> réunion){
      réunion.add(RH.getEmployeDuMois()) ;
      //....
   }

et une utilisation incorrecte par un code client:

   // code  écrit par Olive
   ArrayList<Manager> conseil = new ArrayList<Manager>() ;
   // remplit la liste
   // puis
   RH.completerRéunion(conseil) ; /* _compiler_error_ */

Bien que ça ne soit pas intuitif: une ArrayList<Manager> N’EST PAS une ArrayList<Employe> : Il est hors de question d’ajouter un Employé qui n’est pas un Manager à la liste! Donc ce principe, l'invariance, nous garantit une cohérence du contrôle de type.