Généricité
Introduction
La généricité est un concept de la programmation orientée objet qui permet d'appliquer des comportements communs mais à des objets de natures différentes.
Caractéristiques du langage Java :
La mise en œuvre de la généricité en Java s'appuie sur la notation en diamant qui prend en paramètre le nom d'une classe: <MyType>
.
Usage Basique
Le cas le plus courant de l'usage basique de la généricité est rencontrée avec les collections et les dictionnaires en Java.
Exemple :
// Création d'une liste simple ne pouvant contenir uniquement des objets de type 'java.lang.String'.
ArrayList<String> list = new ArrayList<String>();
Inférence de type :
// Depuis Java 1.7 on peut laisser le diamant vide lors de l'instanciation.
// La généricité est portée par la déclaration de la variable.
ArrayList<String> list = new ArrayList<>();
// Fonctionne sans problème avec le polymorphisme.
List<String> list = new ArrayList<>();
// En Java 10, la généricité doit être portée par le membre de droite avec le mot-clef 'var'.
var list = new ArrayList<String>();
var emptyList = List.of(); // Quel est le type générique de cette liste ???
// Dans de rare cas, il peut être nécessaire de forcer l'inférence.
// Sinon le compilateur utilisera 'java.lang.Object' par défaut.
var list = List.<String>of();
// Autre exemple
var otherList = Collections.<String>emptyList();
var list = new ArrayList<String>();
var message = "Hello World";
list.add(message);
^^^^^^^--------// La variable 'message' est de type 'java.lang.String'
// L'objet peut être inséré dans la liste
var code = 42;
list.add(code);
^^^^^----------// Erreur de compilation !
// La variable 'code' n'est pas de type 'java.lang.String'
Note
La généricité assure le typage statique de Java. Elle permet de factoriser du comportement.
Mise en œuvre
Vous pouvez construire vos propres codes génériques à partir de classes abstraites ou des interfaces. La déclaration de la généricité se fait dans la signature de la classe.
public interface Repository<E> { /* ... */ }
public abstract class AbstractDAO<E>{ /* ... */ }
Il est possible de placer autant de type générique que nécessaire:
public interface Repository<A, B, C, D, E> { /* ... */ }
Dans ces exemples de code ci-dessus, la généricité ne contient pas de nom de classe. En effet à ce stade, il s'agit de type abstrait. C'est à dire, dans la hiérarchie descendante des classes employant la généricité, il existe une classe qui résout cette généricité par un type concret.
// Le type 'E' est un type générique, il ne correspond à aucune classe Java.
public interface DAO<E> { /* ... */ }
// Classe réelle
public class Customer{ /* ... */ }
// Cette classe enfant, résout la généricité en remplaçant 'E' par un type réel: 'Customer'.
public class CustomerDAO implements DAO<Customer>{ /* ... */ }
Convention de nommage
En Java le type générique abstrait doit être une lettre en majuscule. Techniquement, il peut s'agir aussi d'un mot, mais éviter cette approche. Cette lettre n'est pas choisie au hasard, elle possède une signification.
Exemples:
- T => Type : Type quelconque
- A => Any : Type quelconque
- X => Unknow : Type inconnu
- E => Element : Type d'élément, ou d'entité
- K => Key : Type clef (pour les dictionnaires par exemple)
- V => Value : Type valeur (pour les dictionnaires par exemple)
- R => Result : Type de résultat, ou de retour
- I => Identifier : Type d'identifiant
- S => Source : Type source (pour des convertisseurs par exemple)
- D => Destination : Type destination (pour des convertisseurs par exemple)
À vous de décliner suivant le sens à donner dans votre architecture.
Comportement
Considérons le code suivant :
public abstract class AbstractDAO<E> {
// ...
public void save(E entity){
// ...
}
}
public class CustomerDAO extends AbstractDAO<Customer>{
// ...
@Override
public void save(Customer entity){
// ...
}
}
Point de vue de classe abstraite AbstractDAO:
La méthode save
prend en paramètre un objet de type E
.
A ce stade le type E
est abstrait, les seules méthodes visibles par l'objet entity
sont ceux de la classe java.lang.Object
.
En effet toutes classes héritent de cette dernière.
Le type E
est un type générique dont la portée est l'instance de cette classe : les méthodes peuvent utiliser E
comme type de paramètre, ou type de retour, les attributs peuvent être typés E
.
En revanche E
n'est pas utilisable pour les méthodes et attributs statiques.
Point de vue de classe abstraite CustomerDAO:
Dans cette classe la généricité est résolue, la méthode save
prend en paramètre un objet de type Customer
.
Toutes les méthodes de ce dernier sont visibles.
Généricité au niveau d'une méthode
Communément la généricité se déclare au niveau de la classe. Mais il est possible de réduire cette portée au niveau d'une méthode. La notation en diamant est donc portée par la méthode:
public <T> T merge(T primary, T secondary){
// ...
return (T) result;
}
Note
Fonctionne aussi pour des méthodes statiques. Soyez prudent avec les méthodes génériques, dans certains cas le typage n'est résolu qu'a l'exécution ! Vous aurez dans ce cas une erreur de type
java.lang.ClassCastException
.
Limitation
La généricité ne peut pas s'appliquer aux types primitifs ! Utiliser l'équivalent objet !
Type:
- boolean => Boolean
- byte => Byte
- short => Short
- int => Integer
- long => Long
- float => Float
- double => Double
- void => Void
Pseudo-typage d'un type générique
Problématique :
public abstract class AbstractDAO<E>{ /* ... */ }
Un générique est perçu par la classe abstraite ou l'interface comme un type java.lang.Object
.
Comment changer cette perception de la JVM pour coder plus efficacement sans utiliser le transtypage ?
En effet, une classe qui hérite de AbstractDAO
peut remplacer E
par n'importe quelle classe, ce qui peut casser la cohérence de l'architecture du programme.
Remplacer le type générique par une interface ou une classe n'est pas la solution car cela bloquerait la résolution de la généricité dans les classes concrètes. Mais l'idée est là.
On souhaite restreinte le choix de E
par une famille de classe.
Pseudo-typage simple
public interface Entity{ /* ... */ }
public abstract class AbstractDAO<E extends Entity>{ /* ... */ }
Dans l'exemple ci-dessus, le générique E
est pseudo-typé par Entity
.
Cela signifie que E
pour être remplacé dans les classes enfants par n'importe quelle classe qui hérite de Entity
.
Note
Pour être précis,
<X extends Y>
comprend tous les sous-types deY
OU le typeY
lui-même.
Implémentation :
public class Customer implements Entity{ /* ... */ }
public class CustomerDAO extends AbstractDAO<Customer> { /* ... */ }
^^^^^^^^----------------// OK: Typage conforme.
// La classe 'Customer' est bien
// un sous-type de 'Entity'.
public class FakeDAO extends AbstractDAO<String> { /* ... */ }
^^^^^^----------------------// KO: Erreur de compilation !
// La classe 'java.lang.String'
// n'est pas un sous-type de 'Entity'.
Pseudo-typage multiple
Le pseudo-typage Java nous permet même d'être plus précis en prenant en compte l'héritage multiple.
public abstract class AbstractDAO<E extends Entity & Serializable>{ /* ... */ }
La notation en diamant, <E extends Entity & Serializable>
, nous indique que E
doit être un sous type qui hérite de Entity
ET de java.io.Serializable
.
Implémentation :
public class Customer implements Entity, Serializable{ /* ... */ }
public classe CustomerDAO extends AbstractDAO<Customer> { /* ... */ }
^^^^^^^^---------------// OK: Typage conforme.
// La classe 'Customer' est bien
// un sous-type de 'Entity'
// ET 'java.io.Serializable'.
public class Fake implements Entity{ /* ... */ }
public classe FakeDAO extends AbstractDAO<Fake> { /* ... */ }
^^^^------------------// KO: Erreur de compilation !
// La classe 'Fake' est bien
// un sous-type de 'Entity'
// MAIS PAS DE 'java.io.Serializable'.
Généricité en profondeur
Il est possible que le pseudo-typage d'un générique soit composé d'une classe utilisant elle-même la généricité (qui elle-même à son tour soit composée de génériques). Pour une cohérence maximale du code, il faut que les types génériques soit déclarés dans les classes abstraites ou les interfaces, puis résolu dans les classes enfants.
Exemple :
// Le type 'K' désigne le type générique d'un identifiant unique pour une entité.
public interface Entity<K>{
K getId();
}
// Notation en diamant contenant elle-même une autre notation en diamant.
// C'est la généricité en profondeur.
// Pour exploiter le générique 'K', il faut le déclarer au même niveau que 'E'.
// Ainsi cette classe possède deux génériques.
public abstract class AbstractDAO<E extends Entity<K>, K> {
// ...
// Sauvegarder une entité.
public void save(E entity){ /* ... */ }
// Rechercher une entité en fonction de son identifiant.
// Grâce à la généricité en profondeur, le typage est cohérent.
public E findById(K id){ /* ... */ }
}
Note
Il est important de résoudre la généricité en profondeur. En l'absence de cette déclaration, Java considèrera le générique manquant de type
java.lang.Object
. Ce qui peut amener à faire du transtypage, une pratique à éviter.
Avec pseudo-typage:
// Le type 'K' désigne le type générique d'un identifiant unique pour une entité.
// De plus 'K' doit être d'un sous-type de 'java.io.Serializable'.
public interface Entity<K extends Serializable>{
K getId();
}
// Exemple de classe d'entité
// Comme cette classe est sérialisable, imposer le pseudo-typage de 'K' est donc logique.
public class Customer implements Entity<UUID>, Comparable<Customer>, Serializable{
private UUID id;
private String familyName;
private String givenName;
// ...
// Typage par résolution de la généricité.
@Override
public int compareTo(Customer other) {
return Comparator
.comparing(Customer::getFamilyName)
.comparing(Customer::getGivenName)
.compare(this, other);
}
// Typage par résolution de la généricité.
@Override
public UUID getId(){
return id;
}
// Accesseurs & Mutateurs
// ...
}
// Généricité en profondeur avec pseudo-typage.
public class AbstractDAO<E extends Entity<K> & Comparable<E> & Serializable, K extends Serializable>{
// ...
}
Comme on peut le voir la syntaxe devient de plus en plus complexe.
La notation en diamant <E extends Entity<K> & Comparable<E> & Serializable, K extends Serializable>
impose :
- Le type
E
doit être un sous-type deEntity
ETjava.lang.Comparable
ETjava.io.Serializable
. - Le pseudo-typage
java.lang.Comparable
doit se faire avec le typeE
- Le type
K
doit être un sous-type de java.io.Serializable.
Caractère joker
Il arrive que dans certains cas la résolution de la généricité en profondeur n'apporte aucune plus-value dans les classes abstraites ou les interfaces.
Dans ce cas, plutôt que d’omettre la généricité en profondeur, on utilise le caractère ?
aussi connu sous le nom de //wildcard//.
Exemple :
public interface Entity<K>{ /* ... */ }
public abstract class AbstractDAO<E extends Entity<?>> { /* ... */ }
Dans cette déclaration <E extends Entity<?> >
, E
doit être un sous-type de Entity
.
En revanche, on ne se soucie pas du type générique porté par Entity
.
Note
Même si
?
se perçoit commejava.lang.Object
, en réalité, il faut le considérer comme un type quelconque. Lors de l'usage d'un<?>
dans la généricité, le développeur doit s'engager à ne pas faire référence à ce générique.
Exemple :
List<?> anyList = new ArrayList<>();
^^^---------// OK: Tout à fait possible d'instancier cette liste de type '?'.
// (Mais cela n'a pas intérêt en réalité)
anyList.add("Hello World");
^^^^^^^^^^^^^---------// KO: Erreur de compilation !
// La méthode 'add' prend en paramètre un type '?'.
// Ce paramètre est donc invalide.
anyList.add(null);
^^^^------------------// OK: 'null' n'a pas de type.
// Ce paramètre est donc valide.
// (Très moche en réalité)
List<?> anyList = List.of("Hello World");
^^^---------// OK: Tout à fait possible d'instancier cette liste de type '?'.
var item = anyList.get(0);
^^^---------// OK: Appel autorisé
// La méthode 'get' ne fait pas référence au type '?' dans sa signature.
// En revanche elle retourne un objet de type '?'.
// Par polymorphisme Java va typer 'item' en 'java.lang.Object'.
var text = (String) anyList.get(0);
^^^^^^-----------// OK: Transtypage valide
// On peut récupérer le type réel de l'objet contenu dans la liste.
Note
Soyez prudent lors d'un transtypage ! Le compilateur ne peut pas vérifier le typage pour vous !
Conclusion :
L'emploi du joker ?
dans la déclaration des génériques peut alléger son écriture si le développeur s'engage à ne pas faire référence à ce type ?
.
Si dans votre code vous êtes amené à faire du transtypage partout à cause du ?
c'est que ce n'est pas la bonne approche !
La généricité en Java est apparue en version 1.5 pour justement éviter le transtypage abusif.
L'usage du transtypage n'est pas prohibé en soit, il doit servir dans un périmètre restreint.
Java reste un langage à typage statique !