Accès rapide :
La vidéo
Qu'est-ce que la généricité ?
Utilisation de types génériques du Java SE
La syntaxe traditionnelle
L'opérateur diamant
Quelques types génériques proposés par le Java SE
Les collections Java
Le moteur de réflexion Java
Cette vidéo vous présente le concept de généricité et de classe générique. Quelques classes, proposées par l'API du Java SE, sont utilisées pour appuyer la discussion.
En Programmation Orientée Object (POO), la généricité est un concept permettant de définir des algorithmes (types de données et méthodes) identiques qui peuvent être utilisés sur de multiples types de données. Cela permet donc de réduire les quantités de codes à produire.
Peut-être vous dites-vous qu'on peut traiter ce type problème via du polymorphisme : effectivement, c'est une possibilité. Mais la généricité permettra
d'avoir un code plus fortement typé et donc plus sûr. Pour s'en convaincre, analysons le code suivant : il crée une collection de type
java.util.ArrayList
pour y stocker un ensemble de chaînes de caractères.
Par défaut, ce type de collection manipule des instances de type Object
. Donc, par polymorphisme, on peut y stocker n'importe quoi, y compris des
chaînes de caractères. Le problème, c'est que rien ne nous'interdit d'y stocker des dates (car la classe java.util.Date
dérive d'Object
)
ou n'importe quoi d'autre.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import java.util.ArrayList; import java.util.Date; public class Demo { public static void main( String [] args ) { // Cette ligne produit un warning, mais pour le moment on l'ignore ArrayList collection = new ArrayList(); // On y stocke nos chaînes de caractères collection.add( "Essai" ); collection.add( "Cours Java" ); collection.add( "Tutoriel Java" ); // Mais rien nous interdit d'y mettre autre chose collection.add( new Date() ); // Maintenant on parcourt la collection for ( Object value : collection ) { System.out.println( value ); } } } |
Si nous utilisons la généricité, nous pouvons indiquer que la collection ne peut contenir que des chaînes de caractères. Si l'on cherche à contourner le typage, une erreur de compilation sera produite à la place des warnings précédents. Votre programme sera donc plus fortement typé et plus sûr. Voici un premier exemple d'utilisation de la généricité.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import java.util.ArrayList; public class Demo { public static void main( String [] args ) { // On crée une collection de chaînes de caractères ArrayList<String> collection = new ArrayList<String>(); // On y stocke nos chaînes de caractères collection.add( "Essai" ); collection.add( "Cours Java" ); collection.add( "Tutoriel Java" ); // Cette ligne ne compile plus //collection.add( new Date() ); // Maintenant on parcourt la collection : // elle contient des instances de type String. for ( String value : collection ) { System.out.println( value ); } } } |
Le Java SE propose un très grand nombre de classes génériques dans sa librairie et notamment les classes de collections. Les différentes classes de collections Java partagent certaines caractéristiques communes : elles implémentent donc des interfaces communes. Ces interfaces sont elles-mêmes génériques, comme nous allons le voir.
La syntaxe, en termes d'utilisation de types génériques, a évolué au cours du temps. D'ailleurs, les premières versions de Java ne supportaient pas la généricité : on utilisait donc à l'époque le polymorphisme. Le concept de généricité a été introduit dans le langage Java à partir de sa version 5.0. La version Java SE 7.0 a légèrement simplifié les choses avec l'apparition de l'opérateur diamant (diamond operator).
L'exemple proposé précédent utilise la syntaxe traditionnelle. L'information de généricité doit être mentionnée sur la déclaration de la variable, mais
aussi lors de son instanciation (lors de l'appel de l'opérateur new
). Pour rappel, voici la ligne de construction de la collection de type
java.util.ArrayList
.
1 2 |
// On crée une collection de chaînes de caractères ArrayList<String> collection = new ArrayList<String>(); |
Depuis le Java SE 7.0, la syntaxe utilisée pour produire une instance basée sur un type générique a été simplifiée grâce à l'opérateur diamant.
Pour faire simple, vous avez déjà introduit le type de généricité lors de la déclaration de la variable, vous pouvez donc demander au compilateur, via l'opérateur
diamant, de déduire le type de données à utiliser lors du new
à partir de la définition de la variable. Cette déduction du type à utiliser est aussi
appelée inférence de type. Voici un exemple d'utilisation de l'opérateur diamant (<>
).
1 2 |
// On créé une collection de chaînes de caractères ArrayList<String> collection = new ArrayList<>(); |
L'inférence de type (la déduction du type) peut aussi avoir lieu sur une instruction placée après la déclaration de la variable. En voici un exemple.
1 2 3 4 5 6 7 |
// On déclare la variable basée sur un type générique ArrayList<String> collection = null; // On imagine quelques lignes de code // On crée maintenant l'instance en utilisant l'opérateur diamant collection = new ArrayList<>(); |
On retrouve la généricité à différents endroits dans la librairie Java SE. En voici quelques exemples.
Beaucoup de collections Java implémentent l'interface générique java.util.Collection
.
Cette interface est définie ainsi (nous ne listerons pas l'intégralité de ses méthodes, je vous renvoie sur la Javadoc à ce sujet).
1 2 3 4 5 |
public interface Collection<E> extends Iterable<E> { // Spécification des méthodes de l'interface } |
Collection
implémente aussi l'interface générique java.util.Iterable
.
Deux interfaces majeures étendent l'interface Collection
: java.util.List
et java.util.Set
.
La première interface permet de représenter un ensemble d'éléments séquentiel : on y accède via un index de positionnement (à partir de zéro, bien entendu).
La seconde représente des collections à valeur unique (vous ne pouvez pas stocker deux fois la même valeur dans un Set
, contrairement à une
List
).
1 2 3 4 5 6 7 8 9 10 11 12 |
public interface List<E> extends Collection<E> { // Spécification des méthodes de l'interface } public interface Set<E> extends Collection<E> { // Spécification des méthodes de l'interface } |
Une autre interface importante, pour les collections Java, est l'interface java.util.Map
qui représente une collection associative clé-valeur.
Voici la définition de l'interface Map
.
1 2 3 4 5 |
public interface Map<Key, Value> { // Spécification des méthodes de l'interface } |
Mais pourquoi, les diverses collections Java sont basées sur des interfaces ? La raison en est simple : pour une même catégorie de collections, il peut exister plusieurs algorithmes. Par exemple, on peut choisir d'implémenter une collection d'éléments qui se suivent en séquence, soit par une liste chaînée, soit par un tableau (il y aurait même encore d'autres possibilités). Le fait de typer la collection par interface impose une API partagée par plusieurs implémentations et vous aurez toute liberté de changer la classe d'implémentation, si celle-ci n'est plus adaptée. La généricité et l'opérateur diamant fonctionnent parfaitement dans ce cas. En voici un exemple.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import java.util.ArrayList; import java.util.List; public class Demo { public static void main( String [] args ) { // On crée une collection de chaînes de caractères List<String> collection = new ArrayList<String>(); // On y stocke nos chaînes de caractères collection.add( "Essai" ); collection.add( "Cours Java" ); collection.add( "Tutoriel Java" ); // Maintenant on parcourt la collection : // elle contient des instances de type String. for ( String value : collection ) { System.out.println( value ); } } } |
Et voici un autre exemple appliqué à l'utilisation d'une table associative (clé/valeur).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import java.util.HashMable; import java.util.Map; public class Demo { public static void main( String [] args ) { Map<String, Person> agenda = new HashMap<>(); agenda.put( "007", new Person( 7, "James", "Bond", "bond@mi6.uk" ) ); agenda.put( "000", new Person( 0, "Johnny", "English", "bean@mi6.uk" ) ); Person person = agenda.get( "007" ); System.out.println( person ); } } |
La réflexion est un concept de Programmation Orientée Objet qui permet d'obtenir des informations structurelles (des méta-données) sur un type de données.
Le langage Java supporte la réflexion au travers de son API java.util.reflect
. On peut retrouver des méta-données sur tout objet Java ou toute classe.
Ces données seront stockées dans une instance de type java.lang.Class
et pourront être manipulées via l'API de réflexion.
Et la généricité la dedans, me direz-vous. Et bien, la classe java.lang.Class
est générique, car elle porte des informations descriptives
pour n'importe quel type Java. Voici un exemple d'utilisation qui liste l'ensemble des attributs définis directement sur la classe Demo
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import java.lang.reflect.Field; import java.util.Date; public class Demo { private String name; private int counter; private double price; private Date birthday; public static void main( String [] args ) throws Exception { // On accède aux méta-données descriptives du type Demo Class<Demo> metadata = Demo.class; // On récupère la liste des attributs directement définis dans Demo Field [] attributes = metadata.getDeclaredFields(); // On parcourt ces attributs et on les affiche for( Field attribute : attributes ) { System.out.printf( "%s of type %s - %b\n", attribute.getName(), attribute.getType().getName(), attribute.getType().isPrimitive() ); } } } |
Et voici les résultats produits par cet exemple.
name of type java.lang.String - false counter of type int - true price of type double - true birthday of type java.util.Date - false
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :