Artisan Numérique

/développement/java/generics/ Java - Plus loin avec les Generics

Si la version 1.5 de java a apporté un lot inhabituel de nouveautés, c'est principalement les annotations et les types génériques qui ont été le plus appréciés. Le but de cet article n'est pas d'être le nième tutorial sur le sujet, google en regorge. Il s'agit plus d'une approche plus pointue de ce qu'il est possible de faire avec cette nouvelle syntaxe et d'en connaître les limites.

Ce n'est pas du C++

Une remarque qui a son importance pour ceux qui viennent du C++, les types génériques ne sont pas des templates. Les generics sont une réelle extention du langage Java, pas un artifice (sans offence ;-) associé au pré-processeur. Aucun code n'est dupliqué par leur utilisation car c'est la classe elle-même qui "raisonne" sur un ou plusieurs type partiellement définis.

La base

Juste pour se rafraîchir la mémoire, rappelons que la syntaxe des types génériques en Java passent par ces drôles de < > contenant la liste des types "flou" que la classe va avoir à gérer. Ainsi une classe qui va travaille sur un type E va être déclarée comme cela :

public class Marmite
{
  public static class Recette <E>  { ... }
}

Ensuite il suffit d'utiliser E dans la classe comme s'il s'agissait d'un types "normal". Ainsi la fonction ajouterIngrédient pourrait prendre la forme suivante:

public void ajouterIngrédient(E ingrédient) { ... }

Oui, il est possible de mettre des accents dans des identifiants java ;-)

Ajouter des contraintes sur un type générique

Dans notre première version, la classe Recette va considèrer E comme étant de type Objet (la base quoi), ce qui limite fortement son champ d'action... En effet, on aimerait mieux manipuler des E qui seraient des classe Ingrédient par exemple, bénéficiant ainsi des méthodes propres à cette classe. Et c'est possible grâce au mot-clef extends. Nous alons donc ajouter ce détail important à notre exemple en complétant la classe Recette avec un vecteur de stockage de type E, une méthode ajouterIngrédient qui alimente ce vecteur et enfin une fonction getIngrédient dont le type de retour sera lui aussi E. Cela nous donne un tour d'horizon assez complet de ce qu'il est possible de faire sur une classe :

public class Marmite
{
  static public class Ingrédient {}
  static public class Recette <E extends Ingrédient>
  {
    private Vector<E> ingrédients = new Vector<E>();

    public void ajouterIngrédient(E ingrédient) 
    { 
      ingrédients.add(ingrédient);
    }

    public E getIngrédient(int index)
    {
      return ingrédients.get(index);
    }
  }
}

Grâce à tout cela, ma classe peux appeler les fonctions hérités d'Ingrédient. A noter qu'extends peut tout aussi bien viser une Classe qu'une Interface.

Ensuite ne nous reste plus qu'à instancier notre classe générique en "fixant" le type flou :

static class MortAuRat extends Ingrédient {   }
static class Aliment extends Ingrédient {   }
static class Végétal extends Aliment {   }
static class Animal extends Aliment {   }
static class Légume extends Végétal { }
static class Carotte extends Légume {  }
static class Tofu extends Végétal { }
static class Boeuf extends Animal { }

public static void main(String[] args)
{
  Recette<Aliment> potAuFeu = new Recette<Aliment>();
  potAuFeu.ajouterIngrédient(new Carotte());
  potAuFeu.ajouterIngrédient(new Boeuf());
  potAuFeu.ajouterIngrédient(new MortAuRat()); // <----- Ne marche pas car ce n'est pas un Aliment !!
  Ingrédient premierIngrédient = potAuFeu.get(0); // <-------- plus besoin de caster
}

Il faut savori que depuis la version 1.5, c'est que la grande majorité des classes de base de Java qui ont été ainsi passées à la moulinette générique.

Vector<Carotte> carottes = new Vector<Carotte>();
HashMap<Choux,Navet> = new HashMap<Choux,Navet>();
etc...

Et plus intéressant encore, les objets Class sont maintenant, eux aussi génériques. Ce qui ne sera pas sans rapeller des souvenirs à ceux qui ont fait du Delphi :

Class<Choux> classChoux = Choux.class;

Héritage d'une classe générique

Maintenons imaginons que nous désirions créer une créer une version "végétarienne" du pot au feu (sacrilège) en remplaçant le boeuf par du toffu (je sais, je suis un grand malade...). Nous allons donc créer RecetteVegétarienne qui serait un héritage de Recette en contraignant plus encore le type générique au seul héritage à Végétal. Comme cela :

static class RecetteVegétarienne<E extends Végétal> extends Recette<E> { }

new RecetteVegétarienne<Boeuf>(); // <------- Marche pas, logique ;-)
new RecetteVegétarienne<Tofu>(); // <------ CA fonctionne

Les méthodes génériques

Beaucoup pensent que seuls les classes peuvent profiter des generics. En réalité cela va un peu plus loin que cela avec les méthodes. Imaginons que nous voulions ajouter une fonction à qui l'on passe une classe d'ingrédient en paramètre et qui nous retourne la liste des ingrédients de cette classe. Le tout utilisable sans cast, bien évidement :

public <F extends E> Vector<F> getIngrédients(Class<F> type)
{
  Vector<F> sousIngrédients = new Vector<F>();
  for (E ingrédient:ingrédients)
    if (ingrédient.getClass() == type)
      sousIngrédients.add(type.cast(ingrédient));
  return sousIngrédients;
}

Cela mérite que l'on s'y arrête un peu. E est le type générique de la classe que nous avions contraint à hériter d'Aliment. Nous introduisons dans la signature de la méthode, après le mot-clef public un nouveau type générique F que nous contraignons à hériter de E. Ensuite nous utilisons ce type générique pour indiquer que la fonction doit retourner un vecteur de F et prendre en paramètre une classe de F. La question est alors qu'est F ? Et bien, de la même manière que les types génériques des classes sont définis au moment de leur création (new), le type générique d'une fonction est définit... Au moment de sont appel, par ses paramètres. Et en l'occurence ce sera le type de classe passé en paramètre, qui va déterminer F :

Vector<Légume> légumes = potAuFeu.getIngrédients(Légume.class);

Je passe ici un Légume.class, il s'agit donc d'un objet de type Class<Légume>. Donc F est du coup, à l'appel de la méthode, définit comme étant Légume et la fonction retrounera donc... un Vector<Légume>. Ca marche tout seule et je n'ai rien eu à caster dans l'appel.

Le code de la fonction lui-même n'est pas inintéréssant. Dans son corps, F est utilisé pour créer un vecteur de type générique (sousIngrédients). Ensuite j'utilise nouvelle syntaxe de la boucle for qui correspond en gros à un foreach dans d'autres langages. Ensuite je prends la classe de l'ingrédient que je compare au type passé en paramétre. Enfin dans la mesure où ingrédient est de type E et que F extends E, je vais utiliser la nouvelle fonction cast pour transtype dynamiquement et en toute sécurité, mon ingrédient, de E vers F et ensuite l'ajouter à la liste des sousIngrédients.

Limites de la généricité

Les fabriques génériques

A ce stade, la tentation est grande d'utiliser les types génériques pour faire une factory, c'est à dire une classe qui fabrique des objets. Une factory de légumes générique pourait alors ressembler à cela :

Factory factory = new Factory<Légume>();
Légume légume = factory.newItem();

Et pour coder cette factory, on serait tenter de faire ainsi :

public static class Factory<E extends Aliment>
{
  public E newItem()
  {
    return new E(); // <--- marche pas !
    return E.class.newInstance(); // <--- marche pas non plus!
  }
}

Et là, surprise, ça casse... Le compilateur refuse catégoriquement notre new E(). La raison est qu'à la compilation, java ne sais pas ce qu'est E, il ne sait pas quel est son constructeur et donc ne peut honorer le new. Et c'est une régle à retenir, la limite des types génériques c'est qu'il ne sont jamais utilisable en création. Il ne peuvent servir qu'à transtyper des paramétres, des variables, des retours de fonction peu importe, mais jamais à créer.

Les tableaux de génériques

Comme pour les fabriques, on peut imagine des tableaux génériques de la forme

E[] monTableau;

Mais pour les mêmes raisons, tout va bien jusqu'à l'instanciation du tablea. En effet la syntaxe suivante ne passe pas :

E[] monTableau = new E[12];

Il n'y a guère de solution de contournement si ce n'est de... ne pas utiliser un tableau mais une autre structure comme un Vector qui supporte les generiques.

Cas des champs

Il est un seul cas (à ma connaissance) où il est possible de casser cette limite, c'est celui des champs. Imaginons le code suivant :

public class CollectionLégumes
{
  private Vector<Légume> legumes = new Vector<Légume>();
}

Imaginons maintenant que nous voulions, créer un item dans Légume et l'ajouter au vecteur. Pour cela nous utiliserions le code suivant :

CollectionLégumes collection = new CollectionLégumes();
Field field = collection.getClass().getDeclaredField("legumes");        // récupération du champ
Type genericType = field.getGenericType();  // récupération des informations génériques
ParameterizedType parameterizedType = (ParameterizedType) genericType; // Transtypage sauvage
Class clazz = parameterizedType.getActualTypeArguments()[0] // Récumération du premier type générique du vecteur
Object monLegume = clazz.newInstance(); // Création de l'objet

Les cas d'utilisation de cette technique ne sont pas légion. Elle s'appuie massivement sur l'API reflect et permet par exemple d'explorer des objets pour opérer de l'injection de dépendances. Je l'utilise dans mon cas pour générer des grappes d'objets par reflection à partir d'une structure XML. Mais l'idée est assez amusante pour être citée, non ?

Conclusion

Les types génériques sont une réelle avancée et d'une assez belle élégance. Elle permette avec un peu d'habitude d'écrire un code beaucoup plus conçis et lisible. Et s'il n'y avait pas cette vilaine limitation de la création d'objet, cette version serait même à mon sens parfaite ;-)