Il arrive souvent que l'on écrive du code qui mentionne un type précis de données, mais fonctionnerait tout aussi bien avec un autre type. Pour ne pas avoir à se répéter, on essaye de trouver une formulation polymorphe de ce code. Le principe de substitution de Liskov est un premier outil qui va dans ce sens, mais il est imparfait.
public static Object troisieme(Object[] tab) { return tab[2]; } public static void main(String[] args) { String argument = (String) troisieme(args); ... }
On constate qu'un transtypage est nécessaire car le type exact de la valeur de retour ne peut pas être précisé dans le code polymorphe. Il est possible de faire en mieux en donnant un nom au type inconnu :
public static <T> T troisieme(T[] tab) { return tab[2]; } public static void main(String[] args) { String argument = troisieme(args); ... }
Dans cet exemple T est un type paramètre de la méthode troisieme. Lors de l'invocation de la méthode, le compilateur détermine automatiquement que le type inconnu est en fait String, et renvoie directement le bon type de valeur.
Le même principe peut s'appliquer aux classes, tout particulièrement les classes conçues pour stocker des éléments de type quelconque.
public class Paire<E> { private E elementUn; private E elementDeux; public Paire(E e1, E e2) { this.elementUn = e1; this.elementDeux = e2; } public E get(int index) throws IndexOutOfBoundsException { if (index == 0) { return this.elementUn; } else if (index == 1) { return this.elementDeux; } else { throw new IndexOutOfBoundsException("La valeur "+index+" n'est pas acceptable !"); } } }
Remarque Les méthodes de cette classe peuvent mentionner le type paramètre de la classe sans pour autant devenir des méthodes génériques.
Pour employer une classe générique, il faut préciser le type qui va remplacer le type paramètre de la classe. Mais il arrive que l'on n'ait pas besoin (ni envie) de préciser ce type ; on emploie alors un type joker.
public static void afficherPaire(Paire<?> p) { System.out.println("{" + p.get(0) + ", " + p.get(1) + "}"); }
On peut également utiliser un type joker afin de rester souple, tout en garantissant un minimum de fonctionnalité grâce à une limite supérieure :
public static void ajouterComposants(LinkedList<? extends JComponent> liste, Container conteneur) { conteneur.setLayout(new GridLayout(liste.size(), 1)); for(JComponent composant : liste) { conteneur.add(composant); } }
Et enfin, il arrive (plus rarement) que l'on souhaite imposer au contraire une limite inférieure :
public static <T> void remplirPaire(Paire<? super T> destination, T e1, T e2) { destination.set(0, e1); destination.set(1, e2); }
Listes. La classe ArrayList<E> utilise un tableau redimensionnable pour stocker une liste d'éléments.
Tableaux. La classe Arrays contient de nombreuses méthodes facilitant la manipulation des tableaux.
Fréquence. On souhaite concevoir une méthode qui prend un tableau en argument et renvoie l'élément qui s'y répète le plus. En cas d'égalité, l'élément d'indice le plus faible sera privilégié. Cette méthode devra marcher pour des tableaux contenant tous types d'objet.
Association. La méthode de l'exercice précédent ne donne pas toutes les informations que l'on pourrait désirer. Pour lui permettre de renvoyer à la fois l'élément le plus répété et sa fréquence (le nombre de répétition), on va construire une classe Association contenant un élément (de type variable) et une fréquence (de type entier). Il faudra prévoir des accesseurs pour chaque attribut, un constructeur et une surcharge de la méthode toString. Dans un diagramme de classe, ça se représenterait ainsi :
E | |||||
Association | |||||
- element : E - frequence : entier |
|||||
+ getElement() : E + setElement(in elt : E) + getFrequence() : entier + setFrequence(in f : entier) + toString() : String |
Fréquences. On peut encore améliorer les résultats de la méthode de l'exercice précédent : on souhaite à présent obtenir la liste de tous les éléments du tableau, avec la fréquence de chacun d'entre eux.