IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

 

Développons en Java   2.30  
Copyright (C) 1999-2022 Jean-Michel DOUDOUX    (date de publication : 15/06/2022)

[ Précédent ] [ Sommaire ] [ Suivant ] [Télécharger ]      [Accueil ]

 

5. Les génériques (generics)

 

chapitre    5

 

Niveau : niveau 2 Elémentaire 

 

Les génériques en Java (generics) sont un ensemble de caractéristiques du langage liées à la définition et à l'utilisation de types et de méthodes génériques. En Java, les types ou méthodes génériques diffèrent des types et méthodes ordinaires dans le fait qu'ils possèdent des paramètres de type.

Les génériques ont été introduits dans le but d'ajouter une couche supplémentaire d'abstraction sur les types et de renforcer la sécurité des types. Les génériques permettent d'accroître la lisibilité du code et surtout d'en renforcer la sécurité grâce à un typage plus exigeant. Ils permettent de préciser explicitement le type d'un objet et rendent le cast vers ce type implicite. Cette fonctionnalité est spécifiée dans la JSR 14 et intégrée dans Java 1.5.

Les génériques permettent à un type ou à une méthode d'opérer sur des objets de différents types tout en assurant la sécurité des types au moment de la compilation. Les génériques permettent de définir certains types et des méthodes pour lesquelles un ou plusieurs types utilisés sont précisés lors de leur utilisation en tant que paramètre.

Ils permettent par exemple de spécifier quel type d'objets une collection peut contenir et ainsi éviter l'utilisation d'un cast pour obtenir un élément de la collection.

L'inconvénient majeur du cast est que celui-ci ne peut être vérifié qu'à l'exécution et qu'il peut échouer en levant une exception de type ClassCastException. Avec l'utilisation des génériques, le compilateur pourra réaliser cette vérification lors de la phase de compilation : la sécurité du code est ainsi renforcée.

Exemple ( code Java 1.4 ) :
import java.util.*;
 
public class SansGenerique {
 
  public static void main(String[] args) {
 
    List liste = new ArrayList();
    String valeur = null;
    for(int i = 0; i < 10; i++) {
      valeur = ""+i;
      liste.add(valeur);
    }
 
    for (Iterator iter = liste.iterator(); iter.hasNext(); ) {
      valeur = (String) iter.next();
      System.out.println(valeur.toUpperCase());
    }
  }
}

L'utilisation des génériques permet au compilateur de faire la vérification au moment de la compilation et ainsi de s'assurer d'une exécution correcte. Ce mécanisme permet de s'assurer que les objets contenus dans la collection seront homogènes.

La syntaxe pour mettre en oeuvre les génériques utilise les symboles < et > pour préciser le ou les types des objets à utiliser. Seuls des objets peuvent être utilisés avec les génériques : si un type primitif est utilisé dans les génériques, une erreur de type « unexpected type » est générée lors de la compilation.

Exemple ( code Java 5.0 ) :
import java.util.*;
 
public class AvecGenerique {
 
  public static void main(String[] args) {
 
    List<String> liste = new ArrayList();
    String valeur = null;
    for(int i = 0; i < 10; i++) {
      valeur = ""+i;
      liste.add(valeur);
    }
 
    for (Iterator<String> iter = liste.iterator(); iter.hasNext(); ) {
      System.out.println(iter.next().toUpperCase());
    }
  }
}

Si un objet de type différent de celui déclaré dans le générique est utilisé dans le code, le compilateur émet une erreur lors de la compilation.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
 
public class AvecGenerique {
 
  public static void main(String[] args) {
 
    List<String> liste = new ArrayList();
    String valeur = null;
    for (int i = 0; i < 10; i++) {
      liste.add(valeur);
      liste.add(new Date());
    }
 
    for (Iterator<String> iter = liste.iterator(); iter.hasNext();) {
      System.out.println(iter.next().toUpperCase());
    }
  }
}
Résultat :
C:\java>javac AvecGenerique.java
AvecGenerique.java:14: error: no suitable method found for add(Date)
      liste.add(new Date());
           ^
    method List.add(int,String) is not applicable
      (actual and formal argument lists differ in length)
    method List.add(String) is not applicable
      (actual argument Date cannot be converted to String by method invocation c
onversion)
Note: AvecGenerique.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.


1 error

Un concept important dans la mise en oeuvre des génériques en Java est l'effacement de type. Cela signifie que les informations relatives aux génériques ne sont pas incluses dans le bytecode généré par le compilateur. Le bytecode ne contient aucune information relative aux génériques pour des raisons de compatibilité.

Les génériques en Java sont un sucre syntaxique dans le code pour la sécurité de type car toutes ces informations de type ne sont pas incluses dans le bytecode à cause de la mise en oeuvre par le compilateur de l'effacement de type.

Donc un point important concernant les génériques : les génériques Java n'existent plus à l'exécution, c'est un concept utilisable uniquement dans le code source et exploité par le compilateur. Les génériques en Java ont été ajoutés pour fournir une vérification de type au moment de la compilation. Pour maintenir la compatibilité, ils n'ont aucune utilité au moment de l'exécution.

Depuis leur introduction dans le langage Java, les types du JDK utilisent de manière importante les génériques. Le JDK propose de nombreux types génériques, notamment dans l'API Collection mais aussi dans de nombreuses autres API. Il est aussi possible de définir ses propres types génériques.

Les génériques sont l'une des fonctionnalités les plus controversées et surement l'une des plus complexe du langage Java. Généralement les génériques sont considérés comme facile par les développeurs Java : c'est vrai lorsque l'on se place du point de vue de l'utilisation de classes génériques comme les classes de l'API Collections. Mais la mise en oeuvre des génériques pour définir des classes génériques et beaucoup plus compliqué et peut même devenir très complexes dans certains cas.

Ce chapitre contient plusieurs sections :

 

5.1. Le besoin des génériques

De nombreuses classes de l'API Collection possèdent des paramètres de type Object et renvoient les valeurs des méthodes sous forme d'Object. Sous cette forme, elles peuvent prendre n'importe quel type comme argument et retourner le même. Elles permettent donc de contenir des valeurs hétérogènes, c'est-à-dire qu'elles ne sont pas d'un type similaire particulier. Pour garantir que seul un type d'objet est ajouté dans la collection, il fallait tester le type avant d'invoquer les méthodes d'ajouts.

Exemple :
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
 
public class MaClasse {
 
  public static void main(String[] args) {
    List liste = new ArrayList();
    liste.add("test"); 
 
    Iterator it = liste.iterator();    
    while(it.hasNext()) {
      String valeur = it.next();
      System.out.println(valeur);
    }
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:13: error: incompatible types: Object cannot be converted to String
      String valeur = it.next();
                             ^
Note: MaClasse.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
1 error

Pour supporter tous les types d'objet, le type Object est utilisé pour stocker les valeurs dans la collection. Il faut donc obligatoirement utiliser un cast vers le type d'objet pour pouvoir le manipuler.

Exemple :
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
 
public class MaClasse {
 
  public static void main(String[] args) {
    List liste = new ArrayList();
    liste.add("test"); 
 
    Iterator it = liste.iterator();    
    while(it.hasNext()) {
      String valeur = (String) it.next();
      System.out.println(valeur);
    }
  }
}

Cela n'empêche cependant pas d'ajouter des objets d'un autre type et donc d'avoir une exception de type CastClassException à l'exécution.

Exemple :
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
 
public class MaClasse {
 
  public static void main(String[] args) {
    List liste = new ArrayList();
    liste.add("test"); 
    liste.add(new Integer(1));
 
    Iterator it = liste.iterator();    
    while(it.hasNext()) {
      String valeur = (String) it.next();
      System.out.println(valeur);
    }
  }
}
Résultat :
C:\java>javac MaClasse.java
Note: MaClasse.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
 
C:\java>java MaClasse
test
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be
 cast to java.lang.String
        at MaClasse.main(MaClasse.java:14)

 

5.1.1. L'apport des génériques

Il serait beaucoup plus pratique et sûr de pouvoir exprimer l'intention d'utiliser des types spécifiques et que le compilateur puisse garantir l'exactitude de ces types. C'est ce qu'il est possible de faire avec les génériques.

Les génériques sont des fonctionnalités du langage qui permettent de définir et d'utiliser des types et des méthodes génériques. Un type ou une méthode générique définit un paramètre de type qui sera précisé lors de la création d'une instance ou de l'invocation de la méthode.

Les génériques permettent au compilateur de faire des vérifications sur l'utilisation de types, dénommées sécurité de type (type safety) et améliore la robustesse du code en évitant l'utilisation de cast qui pouvaient lever des exceptions de type ClassCastException.

Le compilateur vérifie les affectations de types avec une conversion si elle est possible. Si les types sont incompatibles, il est nécessaire d'utiliser un cast pour éviter une erreur de compilation. Cela indique au compilateur que l'affectation est valide mais elle ne garantit pas qu'elle va irrémédiablement réussir à l'exécution. Si ce n'est pas le cas, une exception de type ClassCastException est levée à l'exécution.

Les génériques permettent de définir des classes, des interfaces, des records et des méthodes qui utilisent un type précisé à la création de l'instance ou à l'invocation de la méthode.

La définition et l'utilisation d'un type générique se fait en utilisant l'opérateur diamant <> dans lequel on précise le type à utiliser.

L'utilisation des génériques améliore significativement la robustesse du code et le rend plus lisible.

 

5.1.2. Les génériques et l'API Collection

Avant Java 5, il est possible d'ajouter des instances de différents types dans une collection car les méthodes acceptent et retournent des instances de type Object. Pour utiliser ses instances, il est alors nécessaire de faire un cast qui peut échouer à l'exécution si le type ne correspond pas.

Exemple :
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
 
public class MaClasse {
 
  public static void main(String[] args) {
    List liste = new ArrayList();
    liste.add("test"); 
    liste.add(new Integer(1));
 
    Iterator it = liste.iterator();    
    while(it.hasNext()) {
      String valeur = (String) it.next();
      System.out.println(valeur);
    }
  }
}
Résultat :
C:\java>javac MaClasse.java
Note: MaClasse.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
 
C:\java>java MaClasse
test
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be
 cast to java.lang.String
        at MaClasse.main(MaClasse.java:14)

En Java 5, l'API Collection a été revue pour utiliser les génériques : les types de l'API Collections sont génériques ce qui permet de préciser le type des objets pouvant être stockés et récupérés d'une collection. L'API Collections est probablement celle qui contient les classes et interfaces génériques les plus utilisées. C'est aussi un cas d'usage très simple et pratique, puisque ce sont des conteneurs d'objets généralement de même type.

Le compilateur va vérifier que seules des instances du type précisé seront ajoutées dans la collection, renforçant ainsi la fiabilité et la robustesse du code. Grâce à cette vérification, il n'est plus nécessaire de faire un cast lorsque l'on récupère une valeur de la collection.

Exemple :
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
 
public class MaClasse {
 
  public static void main(String[] args) {
    List<String> liste = new ArrayList<String>();
    liste.add("test"); 
 
    Iterator<String> it = liste.iterator();    
    while(it.hasNext()) {
      String valeur = it.next();
      System.out.println(valeur);
    }
  }
}
Résultat :
C:\java>javac MaClasse.java
 
C:\java>java MaClasse
test

Les génériques ne sont pas utiles que dans les types de la classe Collection qui encapsulent des objets.

 

5.2. La définition des concepts

L'utilisation des génériques met en oeuvre plusieurs concepts :

  • un type générique (generic type) est une classe, une interface ou un record qui est paramétrée avec au moins un paramètre de type, par exemple, ArrayList<T>
  • un paramètre de type (type parameter) ou variable de type est une variable définie dans une section entre <>, par exemple T dans ArrayList<T>
  • un type paramétré (parameterized type) est une classe où le paramètre de type est défini avec un type comme argument, par exemple, ArrayList<Long>
  • un argument de type (type argument) est le type précisé dans une section entre <> dans un type paramétré, par exemple Long dans ArrayList<Long>
  • un type brut (raw type) est un type générique qui n'est paramétré avec rien, comme new ArrayList()

 

5.3. L'utilisation de types génériques

L'utilisation des génériques permet de rendre le code plus lisible et plus sûr notamment car il n'est plus nécessaire d'utiliser un cast et de définir une variable intermédiaire.

Les génériques peuvent être utilisés avec :

  • des types (classes, interfaces, records)
  • des méthodes et des constructeurs

Les types génériques sont instanciés pour former des types paramétrés en fournissant des arguments de type réels qui remplacent les paramètres de type formels utilisés dans leur définition. Une classe comme ArrayList<E> est un type générique, qui possède un paramètre de type E. Les instanciations, telles que ArrayList<Integer> ou ArrayList<String>, sont appelées types paramétrés, et Integer et String sont les arguments de type réels respectifs.

Exemple ( code Java 5.0 ) :
public class MaClasseGenerique<T1, T2> {
  private T1 param1;
  private T2 param2;
  
  public MaClasseGenerique(T1 param1, T2 param2) {
    this.param1 = param1;
    this.param2 = param2;
  }
 
  public T1 getParam1() {
    return this.param1;
  }
 
  public T2 getParam2() {
    return this.param2;
  }     
}

Lors de l'utilisation de la classe, il faut définir les types des paramètres de types à utiliser.

Exemple ( code Java 5.0 ) :
import java.util.*;
 
public class TestClasseGenerique {
 
  public static void main(String[] args) {
    MaClasseGenerique<Integer, String> maClasse = 
          new MaClasseGenerique<Integer, String>(1, "valeur 1");
    Integer param1 = maClasse.getParam1();
    String  param2 = maClasse.getParam2();
  }
}

Le principe est identique avec les interfaces.

La syntaxe utilisant les caractères < et > se situe toujours après l'entité qu'elle concerne.

Exemple ( code Java 5.0 ) :
    MaClasseGenerique<Integer, String> maClasse = 
        new MaClasseGenerique<Integer, String>(1, "valeur 1"); 
    MaClasseGenerique<Integer, String>[] maClasses;

Le cast peut être utilisé avec les types génériques en utilisant le type paramétré dans le cast.

 

5.3.1. L'opérateur diamant

Avant Java 7, il était obligatoire, lors de l'instanciation d'une classe utilisant les génériques, de préciser l'argument de type dans la déclaration de la variable et dans l'invocation du constructeur.

Exemple ( code Java 5.0 ) :
    Map<Integer, String> maMap = new HashMap<Integer, String>();

Avec Java 7, il est possible de remplacer les types génériques utilisés lors de l'invocation du constructeur pour créer une instance par le simple opérateur <>, dit opérateur diamant (diamond operator), qui permet donc de demander une inférence de type par le compilateur.

Ceci est possible tant que le compilateur peut déterminer les arguments utilisés dans la déclaration du type paramétré à créer.

Exemple ( code Java 7 ) :
    Map<Integer, String> maMap = new HashMap<>();

L'utilisation de l'opérateur diamant n'est pas obligatoire. Si l'opérateur diamant est omis, le compilateur génère un warning de type unchecked conversion.

Exemple ( code Java 7 ) :
      Map<Integer, String> maMap = new HashMap();
      // unchecked conversion warning

La déclaration et l'instanciation d'un type qui utilise les génériques peuvent être verbeux. L'opérateur diamant est très pratique lorsque les types génériques utilisés sont complexes : le code est moins verbeux et donc plus simple à lire

Exemple ( code Java 5.0 ) :
    Map<Integer, Map<String, List<String>>> maCollection = new HashMap<Integer, 
      Map<String, List<String>>>();

L'inconvénient dans le code Java 5 ci-dessus est que le type générique utilisé doit être utilisé dans la déclaration et dans la création de l'instance : cette utilisation est redondante. Avec Java 7 et l'utilisation de l'opérateur diamant, le compilateur va automatiquement reporter le type utilisé dans la déclaration.

Exemple ( code Java 7 ) :
   Map<Integer,Map<String, List<String>>> maCollection = new HashMap<>();

Cette inférence de type réalisée avec l'opérateur diamant n'est utilisable qu'avec un constructeur.

L'utilisation de l'opérateur est conditionnée par le fait que le compilateur puisse déterminer le type. Dans le cas contraire, une erreur de compilation est émise.

Exemple ( code Java 7 ) :
package fr.jmdoudoux.dej;
      
import java.util.ArrayList;
import java.util.List;
 
public class TestOperateurDiamant {
  public static void main(String[] args) {
    List<String> liste = new ArrayList<>();
    liste.add("element1");
    liste.addAll(new ArrayList<>()); 
  }
}
Résultat :
C:\java>javac com\jmdoudoux\test\TestOperateurDiamant.java
com\jmdoudoux\test\TestOperateurDiamant.java:11: error: no suitable method found
 for addAll(ArrayList<Object>)
    liste.addAll(new ArrayList<>());
        ^
    method List.addAll(int,Collection<? extends String>) is not applicable
      (actual and formal argument lists differ in length)
    method List.addAll(Collection<? extends String>) is not applicable
      (actual argument ArrayList<Object> cannot be converted to Collection<? extends
String> by method invocation conversion)


1 error

La compilation de l'exemple ci-dessus échoue puisque la méthode addAll() attend en paramètre un objet de type Collection<String>.

L'exemple suivant compile car le compilateur peut explicitement déterminer le type à utiliser avec l'opérateur diamant.

Exemple ( code Java 7 ) :
package fr.jmdoudoux.dej;
      
import java.util.ArrayList;
import java.util.List;
 
public class TestOperateurDiamant {
  public static void main(String[] args) {
    List<String> liste = new ArrayList<>();
    liste.add("element1");
    
    List<? extends String> liste2 = new ArrayList<>();
    liste2.add("element2");
    liste.addAll(liste2);
  }
}

L'opérateur diamant peut aussi être utilisé lors de la création d'une nouvelle instance dans une instruction return : le compilateur peut déterminer le type à utiliser par rapport à la valeur de retour de la méthode.

Exemple ( code Java 7 ) :
  public Map<String, List<String>> getParametres(String contenu) {
    if (contenu == null) {
      return new HashMap<>();
    }
    // ...

  }

Avant Java 9, il n'est pas possible d'utiliser l'opérateur diamant lors de la définition d'une classe anonyme interne.

Exemple ( code Java 5.0 ) :
public interface Operation<T> {
 
  T ajouter(T a, T b);
}
Exemple ( code Java 9 ) :
public class MainOperation {
 
  public static void main(String[] args) {  
    Operation<Integer> op = new Operation<>() {  
      public Integer ajouter(Integer a, Integer b) {  
        return a+b;   
      }
    };    
  } 
}

Résultat :
C:\java>javac -version
javac 1.8.0_252
 
C:\java>javac MainOperation.java
MainOperation.java:4: error: cannot infer type arguments for Operation<T>
    Operation<Integer> op = new Operation<>() {
                                         ^
  reason: cannot use '<>' with anonymous inner classes
  where T is a type-variable:
    T extends Object declared in interface Operation
1 error

A partir de Java 9, il est possible d'utiliser l'opérateur diamant dans la déclaration d'un classe anonyme interne.

Résultat :
C:\java>javac -version
javac 9.0.1
 
C:\java>javac MainOperation.java
 
C:\java>

 

5.3.2. Le type brut (raw type)

Un type brut (raw type) est le nom donné à une classe ou une interface générique sans aucun argument de type précisé.

Pour des raisons de compatibilité ascendante, l'affectation d'un type paramétré à son type brut est autorisée. Lors de l'utilisation des types bruts, le comportement est similaire à celui pré-générique.

Si le type générique n'est pas fourni au moment de la création d'une instance, le compilateur émet un avertissement et le type générique devient Object. Le compilateur ne peut plus faire de vérification sur le type et il sera nécessaire de faire un cast.

Le rôle des types brutes est de conserver la rétrocompatibilité avec le code antérieure à Java 5 : il n'est donc pas recommandé de les utiliser dans un autre contexte.

L'affectation d'une instance d'une classe générique à une variable dont le type est brut (raw type) provoque l'émission d'un avertissement de type rawtype par le compilateur.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class UtilisationGeneriques {
  public static void main(String[] args) {
    List liste = new ArrayList<String>();
  }
}
Résultat :
C:\java>javac -Xlint UtilisationGeneriques.java
UtilisationGeneriques.java:6: warning: [rawtypes] found raw type: List
    List liste = new ArrayList<String>();
    ^
  missing type arguments for generic class List<E>
  where E is a type-variable:
    E extends Object declared in interface List
1 warning

Lorsque l'on mélange des types bruts et des types génériques, le compilateur émet des avertissements de type rawtype.

Lors de l'affectation à une variable d'un type générique d'une instance d'un type brut, le compilateur émet un avertissement de type unchecked.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class MaClasse {
  public static void main(String[] args) {
   List<String> liste = new ArrayList();
  }
}
Résultat :
C:\java>javac MaClasse.java
Note: MaClasse.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

Un warning de type "unchecked" signifie que le compilateur ne dispose pas de suffisamment d'informations sur les types pour effectuer toutes les vérifications de type nécessaires afin d'assurer la sécurité des types.

L'avertissement "unchecked" est désactivé par défaut, bien que le compilateur indique qu'un warning de ce type soit présent. Pour afficher tous les avertissements "unchecked", il faut utiliser l'option -Xlint:unchecked du compilateur.

Résultat :
C:\java>javac -Xlint:unchecked MaClasse.java
MaClasse.java:7: warning: [unchecked] unchecked conversion
   List<String> liste = new ArrayList();
                        ^
  required: List<String>
  found:    ArrayList
1 warning

Un avertissement de type "unchecked » est aussi émis par le compilateur lors de l'invocation d'une méthode avec un paramètre générique sur une instance d'un type brut.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class MaClasse {
 
 public static void main(String[] args) {
   List liste = new ArrayList();
   liste.add("test");
 }
}
Résultat :
C:\java>javac -Xlint:unchecked MaClasse.java
MaClasse.java:8: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
   liste.add("test");
            ^
  where E is a type-variable:
    E extends Object declared in interface List
1 warning

L'exemple ci-dessous génère plusieurs avertissements par le compilateur.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class Test {
 
  public static void main(String[] args) {
    List nombres = new ArrayList<Integer>();
    nombres.add(1);
    List chaines = new ArrayList<String>();
    chaines.add("1");
 
    List<String> liste;
    liste = chaines;
    String chaine = liste.get(0);    
    liste = nombres;
    chaine = liste.get(0);
  }
}
Résultat :
C:\java>javac -Xlint:unchecked MaClasse.java
MaClasse.java:8: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
    nombres.add(1);
               ^
  where E is a type-variable:
    E extends Object declared in interface List
MaClasse.java:10: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
    chaines.add("1");
               ^
  where E is a type-variable:
    E extends Object declared in interface List
MaClasse.java:12: warning: [unchecked] unchecked conversion
    liste = chaines;
            ^
  required: List<String>
  found:    List
MaClasse.java:15: warning: [unchecked] unchecked conversion
    liste = nombres;
            ^
  required: List<String>
  found:    List
4 warnings

Le compilateur émet quatre avertissements de type unchecked :

  • les deux premiers car le compilateur ne peut pas vérifier le type des données ajoutées puisque c'est le type brut qui est utilisé
  • le troisième car le compilateur ne peut pas vérifier le type des données lors de l'affectation d'une variable de type brut à une variable de type paramétré
  • le quatrième concerne la même raison que le précédent mais il engendre une pollution du heap liée à l'affection d'une List<Integer> à une List<String>. Le compilateur ne peut pas le vérifier de façon formelle puisque la List qui contient des entiers est défini avec le type brut

A l'exécution, si les données contenues dans les variables de type brut et de type paramétré sont de même type, tout se passe correctement. Si ce n'est pas le cas, une exception de type ClassCastException est levée.

Résultat :
C:\java>java MaClasse
Exception in thread "main" java.lang.ClassCastException:
java.lang.Integer cannot be cast to java.lang.String
        at MaClasse.main(MaClasse.java:16)

C'est la raison pour laquelle le compilateur n'émet que des avertissements et pas des erreurs : à l'exécution selon le type de données cela peut se passer correctement ou non.

Il n'y a aucun avantage à utiliser le type brut. Mixer des types bruts et des paramétrés doit être évité, si possible. Cela ne peut être évité lorsque du code legacy non générique est combiné avec du code générique. Dans les autres cas, mixer des types bruts et des paramétrés n'est pas une bonne pratique.

 

5.4. La définition de types génériques

Un type (classe, interface ou record) est générique s'il déclare au moins un paramètre de type.

Il est possible de définir ses propres types génériques permettant de définir un ou plusieurs types paramétrables. Un type générique est une classe, une interface ou un record qui est paramétré pour un ou plusieurs types.

La classe ci-dessous encapsule simplement une propriété de type Object.

Exemple :
public class Conteneur {
  private Object valeur;
 
  public void set(Object valeur) {
    this.valeur = valeur;
  }
 
  public Object get() {
    return valeur;
  }
}

A partir de Java 5, il est possible de rendre une classe générique.

La déclaration d'un type générique ressemble à une déclaration d'un type non générique, sauf que le nom de la classe est suivi d'une section de paramètres de type.

Pour définir une classe utilisant les génériques, il suffit de déclarer leur utilisation dans la signature de la classe à l'aide des caractères < et >. Ce type de déclaration est appelé type paramétré (parameterized type). Dans ce cas, les paramètres fournis dans la déclaration du générique sont des variables de type. Si la déclaration possède plusieurs variables de type alors il faut les séparer par un caractère virgule.

La section des paramètres de type d'un type générique peut comporter un ou plusieurs paramètres de type séparés par des virgules. Ces types sont appelés types paramétrés car ils acceptent un ou plusieurs types en paramètres.

Exemple ( code Java 5.0 ) :
public class Conteneur<T> {
  private T valeur;
 
  public void set(T valeur) {
    this.valeur = valeur;
  }
 
  public T get() {
    return valeur;
  }
}

 

5.4.1. La portée des paramètres de type

La portée d'un type paramétré dépend de l'endroit où il est défini :

la classe ou l'interface s'il s'agit d'un paramètre de type d'un type générique

la méthode ou le constructeur, s'il s'agit d'un paramètre de type d'une méthode ou d'un constructeur générique

Pour éviter toute confusion liée à l'utilisation d'un même nom, il est recommandé de nommer différemment les paramètres de type d'une méthode ou d'un constructeur et le paramètres de type du type dans lequel ils sont déclarés.

 

5.4.2. Les conventions de nommage sur les noms des types

Le nom d'un type générique doit être un identifiant valide en Java. Cependant par convention, on utilise une seule lettre majuscule.

Dans le JDK, les noms des paramètres de types couramment utilisés sont :

  • T : un type comme premier paramètre
  • E : un élément notamment dans les types de l'API Collection
  • N : un nombre
  • S, U V : un type comme paramètre supplémentaire
  • K : la clé d'une Map
  • V : la valeur d'une map

Le respect de cette convention permet de facilement identifier l'utilisation d'un type générique dans le code source.

 

5.4.3. L'utilisation de plusieurs paramètres de types

Il est possible de définir plusieurs paramètres de types en les séparant chacun par une virgule.

Exemple ( code Java 5.0 ) :
import java.util.List;
 
public class MaClasseGenerique<T, U> {
 
  private List<T> listeDeT;
  private List<U> listeDeU;
 
  public static void main(String[] args) {
    MaClasseGenerique<Integer, String> donnees = new MaClasseGenerique<>();
  }
}

 

5.4.4. Les interfaces génériques

Comme les classes, les interfaces peuvent être aussi génériques.

Exemple ( code Java 5.0 ) :
public MonInterface<T> {
  void traiter(T donnees);
}

Elles peuvent aussi avoir plusieurs paramètres de types.

Exemple ( code Java 5.0 ) :
public MonInterface<T, U> {
  void traiter(T conteneur, U donnees);
}

 

5.4.5. Les tableaux de génériques

Il est possible de définir un tableau d'un type générique.

Exemple ( code Java 5.0 ) :
import java.lang.reflect.Array;
 
public class MaClasse<T> {
 
  private T[] tableau;
  
  public MaClasse()  {
  }  
}

Les tableaux sont un ensemble d'éléments d'un même type. L'ajout d'un élément non compatible dans un tableau lève une exception de type ArrayStoreException car un tableau maintient son type dans le bytecode.

Exemple :
public class MaClasse {
 
  public static void main(String... args) {
    Object[] tab = new String[5];
    tab[0] = "test";
    tab[1] = 123;      
  }
}
Résultat :
C:\java>java MaClasse
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
        at MaClasse.main(MaClasse.java:6)
 
C:\java>

Cela va à l'encontre de l'effacement de type : ainsi il n'est pas possible de créer un tableau de génériques en utilisant l'opérateur new.

Une autre raison pour laquelle il n'est pas possible de créer un tableau de génériques avec l'opérateur new est que les tableaux sont co-variants, ce qui signifie qu'un tableau d'objets d'un type hérité est un super type d'un tableau d'objets de type fille. Autrement dit, Object[] est un supertype de String[] et on peut accéder à un tableau de chaînes de caractères par le biais d'une variable de type Object[].

Exemple :
public class MaClasse {
 
  public static void main(String[] args) {
    Object[] tab = new String[1];
    tab[0] = "test"; 
  }
}

Les paramètres de type ne sont de base pas covariants.

 

5.5. La pollution du heap (Heap Pollution)

Dans la documentation Java, la dénomination pollution du tas (heap pollution) désigne quelque chose dans le tas qui n'est pas du type attendu. C'est par exemple le cas d'un objet de type A alors que l'on pense avoir un objet de type B, celui précisé par un type générique. Dans ce cas, il est possible qu'une exception de type ClassCastException soit levée à l'exécution.

Une pollution du heap peut survenir dans plusieurs situations :

  • lorsque l'on mixe des types bruts avec des types génériques
  • lorsque l'on fait un cast
  • lorsque l'on compile les classes séparément

Dans les deux premiers cas, le compilateur émet un avertissement sur le risque potentiel de pollution du heap. La pollution du heap ne se produit pas nécessairement, même si le compilateur émet un avertissement de type unckecked.

L'implémentation en Java des génériques respose sur l'effacement de type : cela assure la compatibilité binaire avec du code qui a été créée avant l'ajout des génériques. C'est la raison pour laquelle c'est le choix historique de la solution d'implémentation des génériques en Java.

L'effacement de type implique qu'un type paramétré dans une classe générique est non réifiable. Un type non réifiable est un type qui n'est pas plus disponible au moment de l'exécution.

L'effacement de type permet l'interopérabilité entre du code générique et non générique mais peut induire la possibilité d'une pollution du tas. Cela survient lorsque le type attendu à la compilation n'est pas celui utilisé à l'exécution.

Une pollution du heap peut survenir lorsque des types bruts et des types paramétrés sont utilisés conjointement et une variable d'un type brut est assignée à une variable d'un type paramétré (une variable avec un type générique fait référence à un objet qui n'est pas de ce type générique).

Cette situation est détectée par le compilateur qui émet dans ce cas un avertissement de type unchecked.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class MaClasse {
 
  public static void main(String[] args) {
    List entiers = new ArrayList<Integer>();
    List<String> chaines = entiers;       // warning de type unchecked

    entiers.add(10);                      // warning de type unchecked

    String s = chaines.get(0);            // ClassCastException

  }
}
Résultat :
C:\java>javac -Xlint:unchecked MaClasse.java
MaClasse.java:8: warning: [unchecked] unchecked conversion
    List<String> chaines = entiers;       // unchecked warning
                           ^
  required: List<String>
  found:    List
MaClasse.java:9: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
    entiers.add(10);                      // unchecked warning
               ^
  where E is a type-variable:
    E extends Object declared in interface List
2 warnings

A la compilation, l'effacement de type (type erasure) est appliqué par le compilateur : par exemple List<String> est transformé en List.

La variable chaines est une List paramétrée avec le type générique String. Lorsque la variable entiers est affectée à la variable chaines, le compilateur émet un warning de type unchecked. Le compilateur indique que la JVM ne sera pas en mesure de savoir le type paramétré de la variable entiers. Dans ce cas, une pollution du heap survient.

Le compilateur émet un autre avertissement de type unchecked lors de l'invocation de la méthode add(). Le compilateur ne connait pas le type paramétré de la variable entiers. Une nouvelle fois, une pollution du heap survient.

Lors de l'invocation de la méthode get(), le compilateur n'émet pas de warning car pour lui le type paramétré de la variable chaines est String donc la valeur retournée par la méthode get() devrait être un objet de type String.

Malheureusement, une exception de type ClassCastException est levée à l'exécution car il n'est pas possible de convertir un objet de type Integer en un objet de type String.

Exemple ( code Java 5.0 ) :
C:\java>java MaClasse
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be
 cast to java.lang.String
        at MaClasse.main(MaClasse.java:10)

La pollution du heap survient lorsqu'une instance d'un type générique est affecté à une variable avec un type générique différent voir pas de type générique. Dans ce dernier cas, on parle de type brut.

Dans l'exemple précédent, cela survient lors qu'une instance de type List est affectée à une variable de type List<String>. Pour préserver la compatibilité ascendante, le compilateur accepte cette affectation. A cause de l'effacement de type à la compilation, List<Integer> et List<String> sont remplacé par le type brut List.

Une pollution du heap survient aussi lorsque la méthode add() est invoquée : à cause du type erasure le compilateur accepte l'invocation car pour lui la méthode add() attend un Object.

Dans les deux cas, le compilateur émet un avertissement de type unchecked indiquant qu'une pollution du heap peut survenir.

Celle-ci se matérialise à l'exécution si les types ne correspondent pas à une exception de type ClassCastException, ce qui est le cas lorsque l'on assigne le premier élément qui est de type Integer à une variable de type String. Ce cast est généré par le compilateur, car la List<Integer> est affecté à une List<String> : un cast vers le type String est ajouté avant l'affectation de la valeur dans le bytecode.

Une pollution du heap peut survenir lorsque du code non générique utilise des classes génériques, ou lorsque nous utilisons des casts non vérifiés ou des types bruts pour contenir une référence à une variable du mauvais type générique. Lorsque nous avons utilisé des casts non vérifiés ou des types bruts, le compilateur nous avertit qu'une pollution du heap peut survenir. Par exemple :

Exemple ( code Java 5.0 ) :
public class Conteneur<T> {
 
  private T valeur;
 
  public Conteneur(T valeur) {
      this.valeur = valeur;
  }
 
  public T get() {
      return valeur;
  }
 
  public static void main(String... args) {
    Conteneur<String> cs = new Conteneur<>("test");   // ok

    Conteneur<?> cq = cs;                      // ok

    Conteneur<Integer> ci = (Conteneur<Integer>) cq; // warning du compilateur

    Integer i = ci.get();                // ClassCastException 

  }
}
Résultat :
C:\java>javac -Xlint:unchecked Conteneur.java
Conteneur.java:16: warning: [unchecked] unchecked cast
    Conteneur<Integer> ci = (Conteneur<Integer>) cq; // warning du compilateur
                                                 ^
  required: Conteneur<Integer>
  found:    Conteneur<CAP#1>
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
1 warning
 
C:\java>java Conteneur
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be
 cast to java.lang.Integer
        at Conteneur.main(Conteneur.java:17)

 

5.6. La mise en oeuvre des génériques

Les classes génériques sont des classes qui sont typées au moment de la définition d'une variable ou de la création d'une instance.

Exemple : une classe qui possède un paramètre de type générique

Exemple ( code Java 5.0 ) :
public MaClasse<T> {
}

Ainsi cette déclaration se lit MaClasse de T.

Il est alors possible d'utiliser T comme un type dans le corps de la classe.

Exemple ( code Java 5.0 ) :
public MaClasse<T> {
  T monChamp;
}

Pour créer une instance de type MaClasse, il faut préciser le type réel qui sera utilisé : la classe est alors un type paramétré avec un argument de type.

Exemple ( code Java 5.0 ) :
  MaClasse<String> maClasse = new MaClasse<String>();

Les types génériques sont instanciés pour former des types paramétrés en fournissant des arguments de type réels qui remplacent les paramètres de type formels. Une classe comme ArrayList<E> est un type générique, qui possède un paramètre de type E. Les types d'une instance, telles que ArrayList<Integer> ou ArrayList<String>, sont appelées types paramétrés, et String et Integer sont les arguments de type réels qui sont utilisés.

Un type paramétré peut prendre deux formes :

  • utilisation d'un type concret, exemple : List<String>, ...
  • utilisation d'un wilcard, exemple : List<?>, List<? extends Number>, ...

Les types paramétrés avec un wilcard sont essentiellement utilisés pour définir le type d'une variable ou d'un paramètre.

A la compilation d'un type paramétré avec un wilcard, le compilateur effectue une capture pour conversion (capture conversion) pour remplacer le wilcard par un type concret qui est inconnu.

Par exemple : List<? extends Number> est le supertype de toute List<X> concrète où X est un sous-type de Number.

Une classe ou une interface générique peut être considérée comme un modèle de code : il faut remplacer ses paramètres de type par des types réels pour obtenir une classe ou une interface concrète.

interface List<T> {
  int   size();
  T     get(int);
  void  add(T);
  ...   
}
interface List<Integer> {
  int     size();
  Integer get(int);
  void    add(Integer);
  ...
}

Ces types concrets s'excluent mutuellement, ce qui signifie que List<A> et List<B> (avec A différent de B) sont des types différents qui ne partagent aucune relation même si une relation existe entre les types A et B. Par exemple, un objet ne peut pas être à la fois une instance de List<Number> et de List<Integer>. Chaque objet est une instance d'un type de classe concret (par exemple ArrayList<Number>), qui a des super-classes concrètes et des super-interfaces concrètes (par exemple List<Number>).

 

5.6.1. Les génériques et l'héritage

En application de l'héritage et du polymorphisme, il est possible d'assigner une instance d'une classe fille à une variable du type d'une de ses classes mères.

Exemple :
public class MaClasse {
 
  public static void main(String[] args) {
    Object objet = new Object();
    Integer entier = Integer.valueOf(10);
    objet = entier;
    traiter(entier);
    traiter(Double.valueOf(2.0)); 
  }
 
  public static void traiter(Number valeur) {
  }
}

De même, pour un type paramétré d'une collection, il est possible d'ajouter des instances du type de l'argument de type ou d'une de ses sous-classes.

Exemple ( code Java 5.0 ) :
import java.util.List;
import java.util.ArrayList;
 
public class MaClasse {
 
  public static void main(String[] args) {
    List<Number> liste1 = new ArrayList<Number>();
    liste1.add(Integer.valueOf(1));
    liste1.add(Double.valueOf(2.0));
 
    Number valeur = liste1.get(0);
    System.out.println(valeur);
  }
}
Résultat :
C:\java>javac MaClasse.java
 
C:\java>java MaClasse
1

Par contre, il n'y a pas de relation entre deux types paramétrés même si les arguments de types utilisés possèdent une relation. Ceci est dû au fait que les types paramétrés sont invariants.

Il existe une relation d'héritage entre les types Number et Integer : Number est la classe mère de la classe Integer.

Bien que ce lien existe entre ces deux types, il n'y a pas de relation entre List<Number> et List<Integer>.

Exemple ( code Java 5.0 ) :
import java.util.List;
import java.util.ArrayList;
 
public class MaClasse {
 
  public static void main(String[] args) {
    List<Number> nombres = new ArrayList<Number>();
    List<Integer> entiers = new ArrayList<Integer>();
    nombres = entiers;
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:9: error: incompatible types
    nombres = entiers;
              ^
  required: List<Number>
  found:    List<Integer>
1 error

 

5.6.2. Les classes génériques et le sous-typage

Il est possible de sous-typer une classe ou une interface générique en l'étendant ou en l'implémentant.

Une classe qui hérite d'une classe générique peut spécifier les arguments de type, peut conserver les paramètres de type ou ajouter des paramètres de type supplémentaire.

La relation entre les paramètres de type d'une classe ou d'une interface et les paramètres de type d'une autre est déterminée par les clauses extends et implements.

Par exemple, ArrayList<E> implémente List<E>, et List<E> étend Collection<E>. Ainsi, ArrayList<String> est un sous-type de List<String>, qui est un sous-type de Collection<String>. Tant que le type générique n'est pas modifié, la relation de sous-typage est préservée entre les types.

Exemple ( code Java 5.0 ) :
public interface MaList<E> extends List<E> {
}

Il est possible de rajouter d'autres paramètres de type tant que ceux hérités sont maintenus.

Par exemple, Il est possible de définir une interface générique qui implémente List<E> et de lui ajouter un paramètre de type supplémentaire tant que le paramètre de type de l'interface mère est conservé.

Exemple ( code Java 5.0 ) :
public interface MaList<E,T> extends List<E> {
}

Il n'est cependant pas possible de retirer un paramètre de type hérité.

Exemple ( code Java 5.0 ) :
public interface MaList extends List<E> {
}
Résultat :
C:\java>javac MaList.java
MaList.java:3: error: cannot find symbol
public interface MaList extends List<E> {
                                     ^
  symbol: class E
1 error

Il est possible d'hériter d'un type paramétré.

Exemple ( code Java 5.0 ) :
import java.util.Map.Entry;
 
public class IdNomEntry implements Entry<Integer, String> {
 
    private Integer id;
    private String nom;
  
    public IdNomEntry(Integer id, String nom) {
      this.id = id;
      this.nom = nom;
    }
 
    @Override
    public Integer getKey() {
      return id;
    }
 
    @Override
    public String getValue() {
      return nom;
    }
 
    @Override
    public String setValue(String value) {
      this.nom = value;
      return nom;
    }
}

Il est possible d'hériter d'un type générique mixant paramètre de type et argument de type.

Exemple ( code Java 5.0 ) :
package com.jmdoudoux.dej;
 
import java.util.Map.Entry;
 
public class IdValeurEntry<V> implements Entry<Integer, V> {
 
    private Integer id;
    private V valeur;
  
    public IdValeurEntry(Integer id, V valeur) {
      this.id = id;
      this.valeur = valeur;
    }
 
    @Override
    public Integer getKey() {
      return id;
    }
 
    @Override
    public V getValue() {
      return valeur;
    }
 
    @Override
    public V setValue(V value) {
      this.valeur = value;
      return valeur;
    }
}

 

5.6.3. Le transtypage des instances génériques

L'héritage permet d'affecter à une variable de type A toute instance d'une classe qui hérite de A.

Cependant ce comportement ne fonctionne pas avec les génériques.

Ainsi il n'est pas possible d'affecter une variable de type List<String> à une variable de type List<Object> car elles n'ont pas de relation.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class MaClasse<T> {
 
  public static void main(String... args) {
 
    List<String> chaines = new ArrayList<String>();
    List<Object> objets = chaines;
    objets.add(new Object());
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:9: error: incompatible types: List<String> cannot be converted to List<Object>
    List<Object> objets = chaines;
                                         ^
1 error

Il n'est pas possible non plus de caster l'affectation d'une variable d'un type générique même si c'est vers une variable d'un même type et d'un type générique qui soit un sous-type.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class MaClasse<T> {
 
  public static void main(String... args) {
 
    List<String> chaines = new ArrayList<String>();
    List<Object> objets = (List<Object>) chaines;
    objets.add(new Object());
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:9: error: incompatible types: List<String> cannot be converted to List<Object>
    List<Object> objets = (List<Object>) chaines;
                                         ^
1 error

Cela permet d'éviter des exceptions à l'exécution dû l'effacement de type utilisé dans la mise en oeuvre des génériques. Celui-ci impliquerait un cast en String de l'instance d'Object ajoutée dans la List.

 

5.7. Les méthodes et les constructeurs génériques

Parfois, la classe ne doit pas être générique mais seulement une méthode ou une méthode a besoin d'un type générique uniquement pour son usage propre.

Parfois, nous ne voulons pas que la classe entière soit paramétrée avec un générique mais uniquement une ou plusieurs méthodes. Dans ce cas, il est possible de définir une méthode générique.

Il est ainsi possible de définir des méthodes ou des constructeurs génériques.

Les méthodes génériques sont des méthodes qui sont écrites avec une seule déclaration et qui peuvent être invoquées avec des arguments de différents types. Le compilateur garantit l'exactitude du type utilisé.

Les méthodes génériques définissent un ou plusieurs types paramétrés qui leur sont propres.

Les méthodes génériques diffèrent des classes génériques dans la portée des types paramétrés définis : pour les méthodes génériques c'est uniquement dans la méthode alors que pour les classes génériques, c'est dans toute la classe.

Il est possible d'utiliser un type générique sur une méthode, que celle-ci soit dans une classe générique ou non.

La syntaxe de définition d'une méthode générique doit suivre plusieurs règles :

  • toutes les déclarations de méthodes génériques comportent une section de définition de type paramétré délimitée par une paire < et > qui précède le type de retour de la méthode
  • chaque section de type paramétré contient un ou plusieurs types paramétrés séparés par des virgules. Un type paramétré est un identifiant qui spécifie un nom de type générique. Un type paramétré ne peut pas être un type primitif
  • les types paramétrés peuvent être utilisés pour déclarer le type de retour et le type des paramètres
Exemple ( code Java 5.0 ) :
public class MaClasse {
 
  public static < E > void afficher( E[] donnees ) {
    for(E element : donnees) {
      System.out.print(element);
    }
    System.out.println();
  }
 
  public static void main(String args[]) {
      Integer[] entiers = { 1, 2, 3, 4, 5 };
      String[] chaines = { "a", "b", "c", "d", "e" };
 
      afficher(entiers);
      afficher(chaines);
   }
}
Résultat :
C:\java>java MaClasse
12345
abcde

Pour invoquer une méthode générique, ses paramètres de type doivent être remplacés par des types réels, soit par inférence soit explicitement.

En fonction des types d'arguments transmis à la méthode générique, le compilateur traite chaque appel de méthode de manière appropriée. Lors de l'invocation de la méthode, deux cas de figures peuvent se présenter :

  • le cas le plus courant, le compilateur est capable d'inférer le type générique et dans ce cas il suffit simplement d'invoquer la méthode comme une méthode sans générique
  • si compilateur ne peut pas faire l'inférence, il faut préciser le type générique entre un < et un > entre le point et le nom de la méthode générique à invoquer
Exemple ( code Java 5.0 ) :
public class MaClasse {
 
  public static <T extends Comparable<T>> T max(T x, T y) {
      T max = x;    
      if(y.compareTo(max) > 0) {
         max = y;
      }
      return max;
   }
 
  public static void main(String... args) {
    System.out.println(MaClasse.max(123, 26));
    System.out.println(MaClasse.max("abc", "xyz"));
  }
}
Résultat :
C:\java>javac MaClasse.java
 
C:\java>java MaClasse
123
xyz

Il est aussi possible et parfois nécessaire d'indiquer explicitement le type générique notamment si le compilateur ne peut pas l'inférer.

Exemple ( code Java 5.0 ) :
public class MaClasse {
 
  public static <T extends Comparable<T>> T max(T x, T y) {
    T max = x;    
    if(y.compareTo(max) > 0) {
      max = y;
    }
    return max;
  }
 
  public static void main(String... args) {
    System.out.println(MaClasse.max(123, 26));
    System.out.println(MaClasse.max("abc", "xyz"));
    System.out.println(MaClasse.<Integer>max(123, 26));
    System.out.println(MaClasse.<String>max("abc", "xyz"));
  }
}
Résultat :
C:\java>javac MaClasse.java
 
C:\java>java MaClasse
123
xyz
123
xyz

Comme les constructeurs sont une forme spéciale de méthode, il est possible de définir un constructeur générique.

Exemple ( code Java 5.0 ) :
public class MaClasse {
 
  public <T> void MaClasse (T donnees) {
    // ...

  }
}

 

5.8. Les paramètres de type bornés (bounded type parameters)

Il est parfois nécessaire de restreindre les types qui peuvent être utilisés comme arguments de type dans un type paramétré. Par exemple, une méthode qui opère sur des nombres ne peut accepter que des instances de type Number ou de ses sous-classes ou dans une méthode qui compare deux objets et que l'on souhaite s'assurer que les objets fournis implémentent l'interface Comparable.

Pour cela, il faut utiliser des paramètres de type bornés (bounded type parameters) qui permettent de définir des restrictions sur le type qui sera autorisé à être utilisé comme type paramétré.

Les paramètres de type bornés peuvent être utilisés dans la définition des types et des méthodes génériques. Ils permettent de restreindre les types utilisables avec une classe ou une méthode générique tout en ayant la flexibilité de travailler avec les différents types définis dans le générique.

Pour déclarer un paramètre de type borné, il faut indiquer le nom du paramètre de type, suivi du mot-clé extends, suivi du type qui représente sa borne supérieure.

Dans ce contexte, le mot clé extends indique que le type étend la borne supérieure dans le cas d'une classe ou implémente une borne supérieure dans le cas d'une interface. Exemple :

<T extends Number>

T pourra être le type Number ou une de ses classes filles lors de la déclaration d'une type paramétré.

Il est possible de préciser une relation entre une variable de type et une classe ou une interface : ainsi, avec le mot-clé extends dans la variable de type, il sera possible d'utiliser une instance du type paramétré avec n'importe quel objet qui hérite ou implémente la classe ou l'interface précisée.

Exemple ( code Java 5.0 ) :
import java.util.*;
 
public class MaClasseGenerique<T extends Collection> {
  private T param;
  
  public MaClasseGenerique2(T param) {
    this.param = param;
  }
 
  public T getParam() {
    return this.param;
  }
}

L'utilisation du type paramétré MaClasseGenerique peut être réalisée avec n'importe quelle classe qui hérite de l'interface java.util.Collection.

Exemple ( code Java 5.0 ) :
import java.util.*;
 
public class TestClasseGenerique {
 
  public static void main(String[] args) {
    MaClasseGenerique<ArrayList> maClasseA = 
          new MaClasseGenerique2<ArrayList>(new ArrayList());
    MaClasseGenerique<TreeSet> maClasseB = 
          new MaClasseGenerique2<TreeSet>(new TreeSet());
  }
}

Ce mécanisme permet une utilisation un peu moins stricte du typage dans les génériques.

L'utilisation d'une classe qui n'hérite pas de la classe ou n'implémente pas l'interface définie dans la variable de type, provoque une erreur à la compilation.

Exemple ( code Java 5.0 ) :
import java.util.*;
 
public class TestClasseGenerique {
 
  public static void main(String[] args) {
    MaClasseGenerique<ArrayList> maClasseA = 
          new MaClasseGenerique2<ArrayList>(new ArrayList());
    MaClasseGenerique<TreeSet> maClasseB = 
          new MaClasseGenerique2<TreeSet>(new TreeSet());
    MaClasseGenerique<String> maClasseC = 
          new MaClasseGenerique2<String>("test");
  }
}
Résultat :
C:\java>javac TestClasseGenerique.java
TestClasseGenerique.java:10: error: type argument String is not within bounds of type-variable T
    MaClasseGenerique2<String> maClasseC =
                     ^
  where T is a type-variable:
    T extends Collection declared in class MaClasseGenerique
TestClasseGenerique.java:11: error: type argument String is not within bounds of type-variable T
          new MaClasseGenerique<String>("test");
                              ^
  where T is a type-variable:
    T extends Collection declared in class MaClasseGenerique
2 errors

Un autre exemple : restreindre le type d'objets qui peut être utilisé dans le type paramétré dans une méthode qui compare deux objets pour assurer que les objets acceptés implémentent l'interface Comparable.

Exemple ( code Java 5.0 ) :
public class MaClasse<T> {
 
  public static <T extends Comparable<T>> int comparer(T t1, T t2){
    return t1.compareTo(t2);
  }
}

Si la méthode comparer() est invoquée avec un type générique qui n'implémente pas l'interface Comparable, alors le compilateur émet une erreur.

Au-delà d'une meilleure vérification du type générique utilisé, l'utilisation d'une borne supérieure permet dans le code d'invoquer les méthodes définies dans le type précisé.

 

5.9. Les paramètres de type avec wildcard

En raison de l'implémentation des types génériques reposant sur l'effacement des types, la JVM n'a aucun moyen de connaître à l'exécution les informations de type des paramètres de type. Par conséquent, il ne peut pas se protéger contre la pollution du tas au moment de l'exécution.

C'est pour cela que les types génériques sont invariants. Les paramètres de type doivent correspondre exactement, afin de se protéger contre la pollution du tas.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
 
public class UtilisationGeneriques {
  public static void main(String[] args) {
    ArrayList<Integer> listInteger = new ArrayList<>();
    ArrayList<Integer> autreListNumber = listInteger;
  }
}

Toute tentative d'utiliser un autre type même d'un sous-type provoque une erreur à la compilation.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
 
public class UtilisationGeneriques {
  public static void main(String[] args) {
    ArrayList<Integer> listInteger = new ArrayList<>();
    ArrayList<Number> listNumber = listInteger; // Erreur de compilation

  }
}
Résultat :
C:\java>javac UtilisationGeneriques.java
UtilisationGeneriques.java:6: error: incompatible types: ArrayList<Integer> cannot be converted
 to ArrayList<Number>
    ArrayList<Number> listNumber = listInteger; // Erreur de compilation
                                   ^
1 error

Pour permettre de contourner ces limitations et gagner en flexibilité en proposant un support de la covariance, contravariance et la bi-variance, les génériques propose les wildcards.

Un wildcard désigne un type quelconque : un wildcard n'est donc pas un type.

Un paramètre de type avec un wildcard (wilcard parameterized type) permet de représenter toute une famille de type en utilisant au moins un wildcard dans sa définition.

Les wildcards sont représentés, dans la déclaration d'un paramètre de type, par un point d'interrogation « ? » et sont utilisés pour faire référence à un type inconnu. Les wildcards sont particulièrement utiles lors de l'utilisation de génériques et peuvent être utilisés comme paramètre de type

Un wildcard peut être utilisé dans la définition d'un type de paramètre dans diverses situations : pour la définition d'un type pour un paramètre, un champ ou une variable locale ; parfois comme type de retour (même cela n'est pas recommandé car il est préférable d'être plus précis).

Un wildcard ne peut pas être utilisé dans des substitutions : cela n'a pas sens de créer une instance de type List<?> ni d'invoquer une méthode dont le type inféré serait un wildcard.

Les types paramétrés avec un wildcard imposent des restrictions par rapport aux types paramétrés avec un type concret :

  • il n'est pas possible de créer une instance, par exemple : new ArrayList<?>
  • il n'est pas possible d'hériter d'un type paramétré avec un wildcard, par exemple : interface MaList extends List<?>

Un paramètre de type avec un wildcard n'est pas un type concret qui pourrait apparaître avec l'utilisation d'un opérateur new. Il permet de préciser quels types sont valides dans des scénarios particuliers d'utilisation de génériques.

L'utilisation d'un wildcard dans un paramètre de type peut être non borné (unbounded wildcard) ou borné (bounded wildcard). Un type paramétré avec un wildcard peut avoir une borne inférieure ou une borne supérieure mais pas les deux.

 

5.9.1. Le besoin des wildcards avec les génériques

Les génériques sont invariants par nature. Même si la classe Object est le supertype de toutes les classes Java, une collection d'Object n'est pas le supertype d'une collection quelconque.

Ainsi, une List<Object> n'est pas le supertype de List<String> et l'affectation d'une variable de type List<Object> à une variable de type List<String> provoque une erreur de compilation. Ceci permet de prévenir d'éventuels conflits qui peuvent survenir si on ajoute des types hétérogènes à la même collection.

Comme les génériques sont invariants, il n'est possible d'affecter une référence d'un objet générique à une variable de la même classe mais avec un type générique différent même si ce type est un sous-type du générique. Ce comportement est dû à l'utilisation de l'effacement de type à la compilation.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class UtilisationGeneriques {
  public static void main(String[] args) {
    List<Object> liste = new ArrayList<String>();
  }
}
Résultat :
C:\java>javac UtilisationGeneriques.java
UtilisationGeneriques.java:6: error: incompatible types: ArrayList<String> cannot be converted
 to List<Object>
    List<Object> liste = new ArrayList<String>();
                         ^
1 error

Fréquemment cette contrainte est limitative, car il est parfois utile d'utiliser une instance d'un sous-type ou d'un super-type d'une classe. Dans ces cas, il faut utiliser les concepts de covariance et de contravariance. Exemple :

Exemple ( code Java 5.0 ) :
  public static void dessinerToutes(List<Forme> formes) {
    for(Forme f : forms) {
      f.dessiner();
    }
  }

Il n'est pas possible de passer en paramètre de cette méthode, une List<Carre> ou List<Triangle> même si Carre et Triangle sont des classes filles de Forme.

Pour permettre cela, il faut utiliser un wildcard avec une borne supérieure.

Exemple ( code Java 5.0 ) :
  public static void dessinerToutes(List<? extends Forme> formes) {
    for(Forme f : forms) {
      f.dessiner();
    }
  }

Cela permet de passer en paramètre une List<Forme> mais aussi une List dont l'argument de type est un sous-type de Forme.

 

5.9.2. L'utilisation de wildcards dans les types paramétrés

Un wildcard est représenté par un point d'interrogation et permet d'indiquer un type arbitraire dans un type paramétré. Un type paramétré avec un wildcard (wildcard parameterized type) utilise au moins un wildcard dans la définition d'un type paramétré.

Par exemple :

  • Set<?> : pour un ensemble qui peut contenir tout type d'objet. Cependant, à l'exécution, cet ensebmle devra être assignée à un type concret
  • List<? extends Number> : pour une liste qui peut contenir des instances de type Number ou sous-type de Number
  • Comparator<? super Integer> : pour une comparateur d'objets de type Integer ou d'un de ses super-types

Parfois au lieu de préciser un type, il faudrait préciser un type mais aussi ses sous-types. Cela se fait en utilisant le wildcard « ? » suivi du mot clé extends suivi du type. On dit alors que que le type générique est le wildcard avec comme borne inférieure le type précisé.

Par exemple, dans l'API Collections, la méthode addAll() de l'interface Collection permet d'ajouter tous les éléments de la Collection passée en paramètre et contenant des éléments du type de la collection ou d'un de ses sous-types. La signature de la méthode est de la forme :

boolean addAll(Collection<? extends E> c)

Le type paramétré <? extends E> indique qu'il ne sera possible que d'utiliser des instances de type E ou d'un sous-type de E.

Exemple ( code Java 5.0 ) :
import java.util.List;
import java.util.ArrayList;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<Number> nombres = new ArrayList<Number>();
    ArrayList<Integer> entiers = new ArrayList<Integer>();
    ArrayList<Long> entierlongs = new ArrayList<Long>(); 
    ArrayList<Float> flottants = new ArrayList<Float>();
    nombres.addAll(entiers);
    nombres.addAll(entierlongs);
    nombres.addAll(flottants);
  }
}

Parfois au lieu de préciser un type, il faudrait préciser un type mais aussi un de ses super-types. Cela se fait en utilisant le wildcard « ? » suivi du mot clé super suivi du type. On dit alors que le type générique est le wildcard avec comme borne supérieure le type précisé.

La mise en oeuvre des bornes inférieures et supérieures impose des contraintes pour garantir la sécurité de type.

 

5.9.2.1. Les types paramétrés avec wildcard non bornés (Unbounded wildcard parameterized type)

Les types génériques sont par nature invariants : parfois il est nécessaire d'utiliser des types qui supportent la bi-variance.

Un wilcard indique un type inconnu. Lors utilisation dans un paramètre d'une méthode permet d'exécuter des traitements sans connaître le type passé en paramètre.

Par exemple, pour définir une méthode générique qui affiche une liste d'objets quelconque, il pourrait être tentant d'écrire l'implémentation ci-dessous :

Exemple ( code Java 5.0 ) :
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
 
public class MaClasse {
 
  public static void afficher(List<Object> liste) {
    for (Object element : liste) {
      System.out.print(element + " ");
    }
    System.out.println();
  }
 
  public static void main(String... args) {
    List<Integer> entiers = Arrays.asList(1, 2, 3);
    List<String> chaines = Arrays.asList("A", "B", "C");
    afficher(entiers); 
    afficher(chaines);
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:17: error: incompatible types: List<Integer> cannot be converted to List<Object>
    afficher(entiers);
             ^
MaClasse.java:18: error: incompatible types: List<String> cannot be converted to List<Object>
    afficher(chaines);
             ^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
2 errors

Malheureusement un type paramétré est invariant. Cette méthode ne fonctionne que pour une List<Object> qui ne contient que des instances d'Object : elle ne fonctionne pour aucune autre List telle que List<Integer>, List<String>, ...

Un type paramétré par un Wildcard non borné représente toute version du type générique. Les wildcards non bornés sont utiles lorsque le type utilisé n'a pas d'importance.

Il existe deux scénarios dans lesquels un wildcard non borné est une approche utile :

  • si seules les méthodes de la classe Object sont utiles
  • lorsque le code utilise des méthodes de la classe générique qui ne dépendent pas du type paramétré. Par exemple, Class<?> est souvent utilisée parce que la plupart des méthodes de Class<T> ne dépendent pas de T.

Un type non borné est spécifié à l'aide d'un wilcard représenté par le caractère point d'interrogation « ? ». Par exemple, List<?> qui représente une List de type inconnu. Il est possible de considérer que List<?> exprime la bi-variance puisqu'il permet d'utiliser n'importe quel type.

Il faut affecter une instance d'une classe générique à une variable à un type générique composée d'un wildcard.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class UtilisationGeneriques {
  public static void main(String[] args) {
    List<?> liste = new ArrayList<String>();
  }
}

La déclaration d'une variable dont le type générique est un wildcard non borné permet de lui affecter une instance dont le type générique peut être quelconque.

Exemple ( code Java 5.0 ) :
import java.util.List;
import java.util.ArrayList;
 
public class MaClasse {
 
  public static void main(String[] args) {
    List<?> liste = new ArrayList<Integer>();
    liste = new ArrayList<String>();
  }
}

Ainsi pour définir une méthode qui puisse utiliser une liste de n'importe quel type, il faut utiliser un wildcard dans le type paramétré. Dans l'exemple suivant, la liste paramétrée par un wildcard permet d'accepter toute sorte de liste :

Exemple ( code Java 5.0 ) :
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
 
public class MaClasse {
 
  public static void afficher(List<?> liste) {
    for (Object element : liste) {
      System.out.print(element + " ");
    }
    System.out.println();
  }
 
  public static void main(String... args) {
    List<Integer> entiers = Arrays.asList(1, 2, 3);
    List<String> chaines = Arrays.asList("A", "B", "C");
    afficher(entiers); 
    afficher(chaines);
  }
}
Résultat :
C:\java>javac MaClasse.java
 
C:\java>java MaClasse
1 2 3
A B C

La déclaration d'une variable de type List<Object> est différente de List<?>.

Une collection de type Collection<Object> est paramétrée avec la classe mère directe ou indirecte de toutes les autres classes. Une telle collection peut donc contenir n'importe quel type d'objet : c'est donc un ensemble hétérogène d'objet.

Exemple ( code Java 5.0 ) :
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<Object> liste = new ArrayList<Object>();
    liste.add("test");
    liste.add(123);
    liste.add(new Date());
  }
}

Une collection de type Collection<?> devra au runtime posséder un type ou une limite de type qui précisera le type à utiliser. A l'exécution, une telle collection contiendra donc un ensemble homogène d'objets.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<Integer> entiers = Arrays.asList(1, 2, 3);
    List<String> chaines = Arrays.asList("A", "B", "C");
 
    List<?> listeEntiers = entiers;
    List<?> listeChaines = chaines;
 
    System.out.println(listeEntiers);
    System.out.println(listeChaines);
  }
}
Résultat :
C:\java>javac MaClasse.java
 
C:\java>java MaClasse
[1, 2, 3]
[A, B, C]

Le type List<Object> est donc différent du type List<?>

List<Object> liste

List<?> liste

Il est possible d'ajouter n'importe quel objet (une instance de type Object ou n'importe quel de ses sous-types) dans la liste avec la méthode add()

Il n'est possible que d'ajouter null à la liste avec la méthode add()

Invariant : il n'est possible que d'affecter une autre de type List<Object> à la variable liste

Bivariant : il est possible d'affecter n'importe quelle référence à une List peu importe le type générique


List<?> est équivalent à List<? extends Object)

Les types paramétrés avec wildcard non bornés sont utiles, mais présentent des limitations.

Dans une collection avec un tel type paramétré, il est possible d'obtenir des valeurs mais il n'est pas possible d'en ajouter d'autres, excepté la valeur null car il n'y a pas d'information sur l'objet.

Il n'est donc pas possible dans d'ajouter un nouvel élément dans la List même si cet élément est du type du type générique précisé lors de la création de l'instance.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class UtilisationGeneriques {
  public static void main(String[] args) {
    List<?> liste = new ArrayList<String>();
    liste.add("test");
  }
}
Résultat :
C:\java>javac UtilisationGeneriques.java
UtilisationGeneriques.java:7: error: incompatible types: String cannot be converted to CAP#1
    liste.add("test");
              ^
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error
 
C:\java>javac -Xdiags:verbose UtilisationGeneriques.java
UtilisationGeneriques.java:7: error: no suitable method found for add(String)
    liste.add("test");
         ^
    method List.add(CAP#1) is not applicable
      (argument mismatch; String cannot be converted to CAP#1)
    method List.add(int,CAP#1) is not applicable
      (actual and formal argument lists differ in length)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
1 error

Il peut paraître peu utile d'avoir une collection dans laquelle il n'est pas possible d'ajouter de valeurs mais c'est la contrainte pour pouvoir accepter n'importe quelle liste tout en garantissant la sécurité de type.

Certaines méthodes de l'API Collection attendent en paramètre une collection avec un type paramétré avec un wildcard Exemple : les méthodes containsAll() et removeAll() de l'interface java.util.List<E>

public boolean containsAll(Collection<?> c)
public boolean removeAll(Collection<?> c);

Ces deux méthodes ont uniquement besoin de lire les données passées en paramètres dans leur traitement. Ces traitements n'utilisent que des méthodes de la classe Object telles que equals() et hashCode().

Il n'est pas possible d'utiliser un wildcard dans un type générique avec l'opérateur new.

Exemple ( code Java 5.0 ) :
import java.util.Set;
import java.util.HashSet;
 
public class MaClasse {
 
  public static void main(String... args) {

    Set<?> valeurs = new HashSet<?>();
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:7: error: unexpected type
    Set<?> valeurs = new Set<?>();
                            ^
  required: class or interface without bounds
  found:    ?
1 error

Il faut utiliser un type concret avec l'opérateur new.

Exemple ( code Java 5.0 ) :
import java.util.Set;
import java.util.HashSet;
 
public class MaClasse {
 
  public static void main(String... args) {
    Set<?> valeurs = new HashSet<String>();
  }
}

 

5.9.2.2. Les types paramétrés avec wildcard borné (Bounded wildcard parameterized type)

Pour ajouter une restriction sur les types paramétrés, il est possible d'appliquer au wildcard une borne supérieure ou inférieure.

Cette restriction est appliquée à l'aide des mots-clés « extends »ou« super » qui permettent de définir des bornes inférieures et des bornes supérieures.

Les wildcards bornés imposent certaines restrictions sur les types possibles qu'il est possible d'utiliser pour créer une instance d'un type paramétré.

Il existe deux types de génériques avec wildcard bornés :

  • les génériques avec bornes inférieures (lower bounded generics) : permet de préciser une classe dont l'argument de type doit être une super classe
  • les génériques avec bornes supérieures (upper bounded generics) : permet de préciser un super type que doit hériter l'argument de type

La borne supérieure exprime la covariance, la borne inférieure exprime la contravariance.

Il est possible de définir une borne supérieure ou une borne inférieure mais pas les deux.

L'utilisation d'un wildcard avec une borne supérieure ou inférieure permet d'accroître la flexibilité d'une API.

 

5.9.2.3. Les wildcards avec bornes supérieures (Upper Bounded Wildcards)

Les wildcards avec borne supérieure sont utilisés pour assouplir la restriction sur le type d'un paramètre de type : ils permettent de leur assigner toute instance dont l'argument de type est le type de la borne ou un ses sous-types. Ce cas exprime la version covariante du type générique List<E>.

Par exemple, si une méthode doit pouvoir accepter en paramètre une List<Integer>, ou List<Double> ou List<Number>, il est possible d'utiliser un wildcard avec une borne supérieure dont le type de la borne sera Number.

Pour utiliser un wildcard avec une borne supérieure, il faut utiliser le caractère point d'interrogation suivi du mot clé extends et du type (classe ou interface) qui représente la borne.

Le mot clé extends peut s'utiliser aussi avec une interface plutôt qu'une classe.

Par exemple :

List<? extends Comparable>

Dans ce cas d'usage, extends signifie soit "étend" si la borne est une classe soit "implémente" si la borne est une interface.

Le type List<Number> est différent du type List<? Extends Number>

List<Number> liste

List<? extends Number> liste

Il est possible d'ajouter une instance de type Number ou n'importe quel de ses sous-types dans la liste avec la méthode add()

Il n'est possible que d'ajouter null à la liste avec la méthode add()

List<? extends Number> liste = ArrayList<Integer>();

liste.add(null);

Invariant : il n'est possible que d'affecter une autre instance de type List<Number> à la variable liste

Covariant : il est possible d'affecter n'importe quelle référence à une List dont le type générique est Number ou un sous-type de Number

List<Integer> entiers = new ArrayList<Integer>();

List<? extends Number> liste = entiers;


Il est possible d'utiliser toutes les méthodes de la classe utilisée comme borne supérieure, puisque tous les éléments de la collection héritent de cette classe.

L'exemple ci-dessous définit et utilise une méthode sommer() qui accepter une List d'objets de type de Number ou une liste d'un sous-type de Number. Elle permet donc de faire la somme de valeurs de type Integer ou Double.

Exemple ( code Java 5.0 ) :
import java.util.Arrays;
import java.util.List;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<Integer> entiers = Arrays.asList(1, 2, 3);
    System.out.println("Somme = " + sommer(entiers));
    List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
    System.out.println("Somme = " + sommer(doubles));
  }
 
  public static double sommer(List<? extends Number> valeurs) {
    double s = 0.0;
    for (Number n : valeurs)
        s += n.doubleValue();
    return s;
  }
}
Résultat :
C:\java>javac MaClasse.java
 
C:\java>java MaClasse
Somme = 6.0
Somme = 6.6

Cela est possible car par définition pour être valide les objets contenus dans la liste héritent de la classe Number. On a donc la garantie que toutes les instances possèdent au moins les méthodes de la classe Number.

Il est possible de lire un élément qui sera nécessairement compatible avec le type de la borne supérieure Number puisque les éléments contenus seront des sous-types de Number. Lorsque l'on accède à un élément d'une liste avec une borne supérieure, la référence obtenue peut être affecté de manière fiable à une référence de type borne supérieure. Cela est possible car il est garanti que tous les éléments soit du type de la borne supérieure ou l'une de ses classes-filles.

Exemple ( code Java 5.0 ) :
import java.util.List;
import java.util.ArrayList;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<Integer> entiers = new ArrayList<Integer>();
    entiers.add(1);
    List<? extends Number> liste = entiers;
    Number valeur = liste.get(0);
    System.out.println(valeur);
  }
}
Résultat :
C:\java>javac MaClasse.java
 
C:\java>java MaClasse
1

Autre exemple, l'interface Collection contient la méthode addAll() dont la signature est la suivante :

Exemple ( code Java 5.0 ) :
public interface Collection<E> {
  // ...

  public boolean addAll(Collection<? extends E> c); 
  // ... 

}

Elle permet d'ajouter tous les éléments d'une autre collection contenant des éléments compatibles à la collection.

La Collection attendue en paramètre est typée avec un wildcard avec une borne supérieure avec le paramètre de type E de la Collection. Ainsi, il est possible d'ajouter tous les éléments d'une Collection contenant des objets de type E ou d'un sous-type de E.
Exemple ( code Java 5.0 ) :
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<Number> nombres = new ArrayList<Number>();
    List<Integer> entiers = Arrays.asList(1, 2, 3);
    List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3 );
    nombres.addAll(entiers); 
    nombres.addAll(doubles);
    System.out.println(nombres);
  }
}
Résultat :
[1, 2, 3, 1.1, 2.2, 3.3]

Ce code est valide et fonctionne car la méthode addAll() attend en paramètre quelque chose de type Collection<? extends E>. Dans l'exemple ci-dessus, E correspond au type Number. Les deux List, respectivement typées avec Integer et Double passées en paramètre dans l'exemple sont compatibles avec ce type.

La mise en oeuvre d'une collection avec un type paramétré avec un wildcard avec borne supérieure ne permet pas d'ajouter des éléments dans la collection. La raison est que lorsque l'on récupère une valeur, le compilateur n'a aucune idée de son type, mais seulement qu'il hérite du type utilisé comme borne supérieure. Le compilateur ne pouvant assurer le type safety, puisqu'il est possible d'utiliser n'importe quel sous-type, l'ajout d'un élément dans une telle collection est interdit et génère une erreur par le compilateur.

Par exemple,

Collection<? extends Number>
permet d'utiliser n'importe quelle instance qui est un sous-type de Number. Il n'y a aucun moyen de savoir lequel de ses sous-types et donc de garantir la sécurité de type : il n'est donc pas possible d'ajouter l'élément dans la collection.

Exemple ( code Java 5.0 ) :
import java.util.List;
import java.util.ArrayList;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<Integer> entiers = new ArrayList<Integer>();
    List<? extends Number> liste = entiers;
    liste.add(1);
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:9: error: no suitable method found for add(int)
        liste.add(1);
             ^
    method Collection.add(CAP#1) is not applicable
      (argument mismatch; int cannot be converted to CAP#1)
    method List.add(CAP#1) is not applicable
      (argument mismatch; int cannot be converted to CAP#1)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Number from capture of ? extends Number
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error

Remarque : il n'est pas possible d'ajouter de nouveaux éléments dans une collection avec un type paramétré avec un wildcard avec une borne supérieure, hormis la valeur null.

Un type paramétré avec un wildcard ne peut pas être utilisé comme type générique avec l'opérateur new.

Exemple ( code Java 5.0 ) :
import java.util.List;
import java.util.ArrayList;
 
public class MaClasse {
 
  public static void main(String[] args) {
    List<? extends Number> list= new ArrayList<? extends Number>();
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:7: error: unexpected type
    List<? extends Number> list= new ArrayList<? extends Number>();
                                              ^
  required: class or interface without bounds
  found:    ? extends Number
1 error

Il faut obligatoirement utiliser un type dont le bytecode peut être chargé dans l'argument de type avec l'opérateur new.

Exemple ( code Java 5.0 ) :
import java.util.List;
import java.util.ArrayList;
 
public class MaClasse {
 
  public static void main(String[] args) {
    List<? extends Number> list= new ArrayList<Integer>();
  }
}

Il est possible d'utiliser de manière équivalente un paramètre de type borné à la place d'un wildcard avec borne supérieure.

Exemple ( code Java 5.0 ) :
  public static double sommer(List<? extends Number> valeurs) {
    double s = 0.0;
    for (Number n : valeurs)
        s += n.doubleValue();
    return s;
  }

L'exemple ci-dessus est équivalent à l'exemple ci-dessous

Exemple ( code Java 5.0 ) :
  public static <E extends Number> double sommer(List<E> valeurs) {
    double s = 0.0;
    for (Number n : valeurs)
        s += n.doubleValue();
    return s;
  }

 

5.9.2.4. Les wildcards avec bornes inférieures (Lower Bounded Wildcards)

Pour qu'un type générique accepte tous les types qui sont un super type d'un type particulier, il faut utiliser un wildcard avec une borne inférieure.

Ainsi dans une List avec un wildcard avec borne inférieure, il n'est possible de lui affecter qu'une List dont l'argument de type est le type de la borne ou un de ces super-types.

Une borne inférieure se définit en faisant suivre le wildcard par le mot clé super et le type de la borne inférieure : <? super T> indique un type qui peut être T ou un super-type de T.

Exemple avec List<? super Integer> indique qu'il sera possible d'utiliser List<Integer> ou List<Number> ou List<Object>.

Exemple ( code Java 5.0 ) :
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<? super Integer> valeurs = new ArrayList<Integer>();
    valeurs = new ArrayList<Number>();
    valeurs = new ArrayList<Object>();
  }
}

En fait, List<? super Integer> n'est pas un type défini : c'est un modèle décrivant un ensemble de types qui sont autorisés comme argument de type. Il est important de noter que List<Integer> n'est pas un sous-type de List<? super Integer>, mais un type qui correspond à ce modèle.

Une List<Integer> est plus restrictive que List<? super Integer> car la première correspond à une liste de type Integer uniquement, tandis que la seconde correspond à une liste de type Integer de tout type qui est un super-type d'Integer.

L'utilisation d'une borne inférieure est requise pour permettre l'ajout des éléments dans la collection.

Exemple : la classe Collections possède la méthode addAll() dont la signature est :

public static <T> boolean addAll(Collection<? super T> c, T... elements) ;

Elle permet d'ajouter tous les éléments du varargs de type T dans la Collection fournis en paramètre.

Le type paramétré <? super T> signifie que la liste de destination peut avoir des éléments de tout type qui est un super-type de T. La méthode accepte en entrée toute liste de type T ou d'un super type de T. Ce cas exprime la version contravariante du type générique List<T>.

Exemple ( code Java 5.0 ) :
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<Integer> entiers = Arrays.asList(1, 2, 3);
    List<? super Integer> valeurs = new ArrayList<Integer>();
    valeurs.addAll(entiers);
    valeurs.add(4);
    Object valeur = valeurs.get(0);
    Integer entier = (Integer) valeurs.get(0);
    System.out.println(valeurs);
  }
}
Résultat :
[1, 2, 3, 4]

L'intuition pourrait permettre de conclure qu'une List<? super Fille> est une List d'instance de type Fille ou d'un super type de Fille et que donc il est possible d'ajouter des instances d'un de ses super-types. Ce raisonnement est erroné.

La déclaration List<? super Fille> garantit que la liste sera d'un type permettant d'ajouter à la liste de tout ce qui est une instance de Fille. Lorsque l'on utilise des wildcards, ceux-ci s'appliquent au type de la List passée en argument à la méthode, et non au type de l'élément lors d'ajout d'un élément à la List. Les wildcards ne s'appliquent pas aux éléments eux-mêmes : les restrictions ne s'appliquent qu'au type générique qui les utilisent.

La List se comporte comme toujours lorsque l'on a une collection d'éléments. On ne pourra ajouter que des éléments de type de la borne inférieure ou d'un de ces sous-types. Par exemple avec List<? super Fille>, il sera possible d'ajouter que des instances de Fille ou des sous-types de Fille.

Il n'est pas possible d'ajouter une instance d'un super type mais il est possible d'ajouter une instance de type Fille, d'un de ses sous-types ou null. Comme Object n'est pas une sous-classe de classe Fille, puisqu'une de ses classes mères, le compilateur n'autorise pas l'ajout d'une instance d'une classe mère dans une telle List.

Exemple ( code Java 5.0 ) :
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<? super Integer> valeurs = new ArrayList<Integer>();
    valeurs.add(new Object());
    Object valeur = valeurs.get(0);
    System.out.println(valeurs);
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:9: error: no suitable method found for add(Object)
    valeurs.add(new Object());
           ^
    method Collection.add(CAP#1) is not applicable
      (argument mismatch; Object cannot be converted to CAP#1)
    method List.add(CAP#1) is not applicable
      (argument mismatch; Object cannot be converted to CAP#1)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object super: Integer from capture of ? super Integer
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error

Les raisons de ces contraintes sont simples : la sécurité de type. S'il était possible d'ajouter une Mere à une List<? super Fille> alors il serait aussi possible d'ajouter une AutreFille qui une classe fille de Mere. Mais une Fille n'est pas une AutreFille.

Afin d'appliquer l'effacement de type, le compilateur doit concrétiser un wildcard avec une borne en un type spécifique en utilisant l'inférence.

Dans l'exemple, le compilateur infère que c'est une List<Fille>.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<Object> objets = new ArrayList<Object>();
    List<Fille> filles = new ArrayList<Fille>();
    ajouter(objets);
    ajouter(filles);
    System.out.println(objets);
    System.out.println(filles);
  }
 
  public static void ajouter(List<? super Fille> liste){
    liste.add(new Fille());
    liste.add(new PetiteFille());
  }
}
 
class Mere {
}
 
class Fille extends Mere {
}
 
class PetiteFille extends Fille {
}
Résultat :
[Fille@cac736f, PetiteFille@5e265ba4]
[Fille@36aa7bc2, PetiteFille@76ccd017]

Le compilateur vérifie que la méthode add() de List a bien en paramètre une instance de Fille ou un sous-type de Fille.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<Object> objets = new ArrayList<Object>();
    List<Fille> filles = new ArrayList<Fille>();
    ajouter(objets);
    ajouter(filles);
    System.out.println(objets);
    System.out.println(filles);
  }
 
  public static void ajouter(List<? super Fille> liste){
    liste.add(new Fille());
    liste.add(new PetiteFille());
    liste.add(new Mere());
  }
}
 
class Mere {
}
 
class Fille extends Mere {
}
 
class PetiteFille extends Fille {
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:18: error: no suitable method found for add(Mere)
    liste.add(new Mere());
         ^
    method Collection.add(CAP#1) is not applicable
      (argument mismatch; Mere cannot be converted to CAP#1)
    method List.add(CAP#1) is not applicable
      (argument mismatch; Mere cannot be converted to CAP#1)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object super: Fille from capture of ? super Fille
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error

L'information indiquée par le compilateur dit que CAP#1 extends Object super: Fille from capture of ? super Fille. Il faut considérer 'Object super : Fille' comme un objet dont Fille est un supertype, ou plus simplement, un objet de type Fille. Une instance de Mere n'est pas une instance de Fille, et donc la compilation échoue.

Attention, lors de l'exploitation des données contenues dans la List : le type des objets est celui inféré et un cast vers le type de la borne peut échouer à l'exécution si le type inféré est un des super types de la borne.

Exemple ( code Java 5.0 ) :
import java.util.Arrays;
import java.util.List;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<Object> objets = Arrays.asList(new Object());
    List<Integer> valeurs = Arrays.asList(1, 2, 3);
    afficherPremier(valeurs);
    afficherPremier(objets);
  }
  
  public static void afficherPremier(List<? super Integer> liste ) {
    Object valeur = liste.get(0);
    try {
      Integer entier = (Integer) liste.get(0);
    } catch(Exception e) {
      e.printStackTrace();
    }
    System.out.println(liste);
  }
}
Résultat :
C:\java>javac MaClasse.java
 
C:\java>java MaClasse
[1, 2, 3]
java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.Integer
        at MaClasse.afficherPremier(MaClasse.java:16)
        at MaClasse.main(MaClasse.java:10)
[java.lang.Object@15db9742]

 

5.9.2.5. Comment choisir entre borne inférieure et supérieure

L'un des aspects qui peut être à l'origine de confusion lors de la mise en oeuvre des génériques est de déterminer quand il faut utiliser un wildcard avec borne inférieure et avec borne supérieure.

Les mêmes principes sont utilisés dans différents contextes avec des noms différents :

  • in/out : utilisé dans le tutorial Java SE
  • producer/consumer : utilisé par Joshua Block dans son livre de référence "Effective Java"
  • get/put

Quelques soient leurs noms, ces principes doivent être utilisés comme guide pour décider de l'utilisation d'un wildcard avec ou sans borne supérieure ou inférieure.

Le principe in/producer/get représente des données à lire. Il faut utiliser un wildcard avec borne supérieure et donc utiliser le mot clé extends

Le principe out/consumer/put représente des données à écrire. Il faut utiliser un wildcard avec borne inférieure et donc utiliser le mot clé super

Pour choisir d'utiliser une borne inférieure ou supérieure, il faut tenir compte de plusieurs contraintes :

  • L'utilisation d'une borne inférieure, avec le mot clé extends concernent des objets désignés par les termes Producer : elle permet uniquement d'ajouter des valeurs. Elle permet de mettre en oeuvre la covariance. Il faut utiliser <? extends T> s'il faut obtenir des objets de type T d'une collection. Par exemple, la méthode addAll(Collection<? extends E> c) de l'interface Collection permet d'ajouter tous les éléments de la collection passée en paramètre à l'instance de la collection : elle doit donc les lire et le type paramétré utilise extends.
  • L'utilisation d'une borne supérieure, avec le mot clé super concernent des objets désignés par les termes Consumer : elle permet uniquement de lire des valeurs. Elle permet de mettre en oeuvre la covariance. Il faut utiliser <? super T> s'il faut ajouter des éléments de type T dans la collection. Par exemple, la méthode static <T> boolean addAll(Collection<? super T> c, T... elements) de l'interface Collection permet d'ajouter tous les éléments du vargargs à la collection passée en premier paramètre : elle doit donc modifier la collection et le type paramétré utilise super.
  • Pour permettre de lire et d'ajouter d'une valeur, il faut utiliser un type explicite. Il ne faut donc pas utiliser de wildcard si des éléments doivent être lus et ajoutés dans la collection

L'utilisation d'une borne supérieure ne rend pas la collection totalement en lecture seule car il est tout de même possible d'ajouter null à la collection.

L'utilisation d'une borne inférieure ne rend pas la collection totalement en écriture seule car il est tout de même possible de lire des éléments en tant qu'Object.

Ces contraintes s'illustrent bien dans la méthode copy() de la classe Collections dont la signature est : copy(List<? super T> dest, List<? extends T> src). Elle permet de copier tous les éléments de la List src dans la List dest. Il est donc nécessaire de lire les éléments de la List src, raison pour laquelle son type paramétré utilise extends. Il est aussi nécessaire d'ajouter les éléments dans le List dest, raison pour laquelle son type paramétré utilise super.

Pour résumer :

  • une borne supérieure rend une collection en lecture seule
  • une borne inférieure rend une collection en écriture seule

Il est préférable d'utiliser les bornes inférieures et supérieures dans les paramètres d'entrée. Il n'est pas recommandé de les utiliser comme types de retour pour restreindre les manipulations de données potentielles.

L'utilisation d'un wildcard comme type de retour n'est pas recommandé car cela oblige les appelants qui invoquent la méthode à traiter avec les jokers.

 

5.9.2.6. Les wildcards et le sous-typage

Les types génériques ne proposent pas de relations entre-eux même s'il existe une relation entre les types utilisés comme paramètre dans les types génériques.

Cependant, il est possible d'utiliser un wildcard pour définir une relation entre des types génériques.

Soit une classe Mere et une classe Fille dont elle hérite. Grâce à l'héritage en Java, il est de créer une instance de type Fille et de l'affecter à une variable de type Mere car la classe Fille est une sous-classe de la classe Mere.

Exemple :
Fille fille = new Fille();
Mere mere = fille;

Cela ne s'applique pas aux types génériques

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class Mere {
 
  public static void main(String[] args) {
    Fille fille = new Fille();
    Mere mere = fille;
    
    List<Fille> filles = new ArrayList<Fille>();
    List<Mere> meres = filles;
  }
}
 
class Fille extends Mere { 
}
Résultat :
C:\java>javac Mere.java
Mere.java:11: error: incompatible types: List<Fille> cannot be converted to List<Mere>
    List<Mere> meres = filles;
                       ^
1 error

Bien que la classe Fille est un sous-type de la classe Mere, List<Fille> n'est pas un sous-type de List<Number>. Le type commun de List<Mere> et de List<Fille> est List<?>.

Diagramme de classe

Pour qu'il y ait une relation entre ces classes, il faut utiliser un wildcard avec une borne supérieure.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class Mere {
 
  public static void main(String[] args) {
    Fille fille = new Fille();
    Mere mere = fille;
    
    List<? extends Fille> filles = new ArrayList<Fille>();
    List<? extends Mere>  meres = filles;  
  }
}
 
class Fille extends Mere { 
}

L'exemple ci-dessus se compile sans erreur car List<? extends Fille> est un sous-type de List<? extends Mere>. La classe Fille héritant de la classe Mere, il y a une relation entre List<? extends Fille> et List<? extends Mere>.

Le diagramme ci-dessous illustre les relations entre List génériques qui utilisent des wildcards avec bornes supérieures et inférieures.

Diagramme de classes

 

5.9.2.7. La capture des wildcards et les méthodes helper

L'utilisation d'un type paramétré avec un wildcard empêche de savoir qu'elles sont les signatures des méthodes et donc quelles méthodes des objets pourront être invoquées.

Exemple avec List<?> : quel serait le type retourné par la méthode get() et quel serait le type du paramètre de la méthode add().

Pour permettre leur invocation, il faudra que l'objet ait un type concret. Lors de la compilation, ce type concret n'est pas connu.

Le compilateur utilise un mécanisme nommé capture lorsque le type paramétré utilise un wildcard : chaque wildcard est remplacé par une variable de type. Ainsi, le compilateur ne doit traiter que les objets de type concret.

La capture des wildcards dépend des bornes des wildcards et des bornes des paramètres de types.

Chaque paramètre de type à une borne supérieure qui limite les types avec lesquels il peut être substitué. La limite supérieure par défaut est Object. Exemple :

List<T> : la borne est Object

List<T extends Number> : la borne est Number

En Java, les paramètres de type n'ont pas de bornes inférieures.

Un wildcard possède soit une borne inférieure soit une borne supérieure mais par les deux. Par défaut, un wilcard possède une borne inférieure et supérieure sur Object.

Lors de la capture d'un wildcard avec borne supérieure, celui-ci est remplacé par une nouvelle variable de type, qui prend la limite supérieure du wildcard, et la limite supérieure du paramètre de type.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class ListUtils {
 
  public static void traiter(List<? extends Number> nombres) {
    Number nombre = nombres.get(0);
        System.out.println(nombre);
  }
  
  public static void main(String[] args) {
          List<Integer> liste = new ArrayList<Integer>();
          liste.add(1);
          traiter(liste);
  }
}

Dans l'exemple ci-dessus, le compilateur opère la capture de List<? extends Number> en List<XXX> ou XXX est un sous-type de Number. Au moment de la compilation le compilateur ne connait pas précisément le type XXX mais il sait que XXX doit être un sous-type de Number.

Comme XXX est un sous type Number, il est tout à fait légal d'assigner la valeur de retour de la méthode get() à une variable de type Number.

La méthode add(XXX) où XXX est un sous-type de Number ne peut cependant pas être utilisé avec un type Number car Number n'est pas sous-type de XXX.

Exemple ( code Java 5.0 ) :
import java.util.List;
 
public class ListUtils {
 
  public void ajouter(List<? extends Number> nombres, Number nombre) {
    nombres.add(nombre);
  }
}
Résultat :
C:\java>javac ListUtils.java
ListUtils.java:6: error: no suitable method found for add(Number)
    nombres.add(nombre);
           ^
    method Collection.add(CAP#1) is not applicable
      (argument mismatch; Number cannot be converted to CAP#1)
    method List.add(CAP#1) is not applicable
      (argument mismatch; Number cannot be converted to CAP#1)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Number from capture of ? extends Number
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error

Lors de la compilation, le compilateur utilise un nom qui lui est propre, par exemple de la forme CAP#n, pour les variables de type issues de la capture.

Le compilateur applique la capture sur chaque expression qui produit une valeur dans un objet défini avec un type paramétré utilisant un wildcard.

Exemple ( code Java 5.0 ) :
import java.util.List;
 
public class ListUtils {
 
  void ajouter(List<? extends Number> nombres) {
    nombres.add(nombres.get(0));
}
Résultat :
C:\java>javac ListUtils.java
ListUtils.java:6: error: no suitable method found for add(CAP#1)
    nombres.add(nombres.get(0));
           ^
    method Collection.add(CAP#2) is not applicable
      (argument mismatch; Number cannot be converted to CAP#2)
    method List.add(CAP#2) is not applicable
      (argument mismatch; Number cannot be converted to CAP#2)
  where CAP#1,CAP#2 are fresh type-variables:
    CAP#1 extends Number from capture of ? extends Number
    CAP#2 extends Number from capture of ? extends Number

Lors de la compilation de cet exemple, le compilateur réalise deux captures mais il ne peut pas s'assurer que les types concrets qui seront utilisés seront les mêmes.

Les variables de type introduites par les captures sont inutilisables dans le code source. Leurs noms sont donnés par le compilateur (exemple "CAP#1"). Elles n'ont pas de noms propres qu'il serait possible d'utiliser dans le code source.

Il est possible de gérer ces cas en définissant une méthode générique avec des variables de type nommées. Une telle méthode est désignée sous le nom capture helper.

Le but d'une méthode capture helper est d'attribuer un nom à la variable de type introduite par la capture conversion. Cette technique est utile pour permettre de faire référence dans le code au type d'un type paramétré avec un wildcard.

Exemple ( code Java 5.0 ) :
public class Paire<T> {
 
  private T val1;
  private T val2;
 
  public Paire(T val1, T val2) {
    this.val1 = val1;
    this.val2 = val2;
  }
 
  public T getVal1() {
    return val1;
  }
 
  public void setVal1(T val1) {
    this.val1 = val1;
  }
 
  public T getVal2() {
    return val2;
  }
 
  public void setVal2(T val2) {
    this.val2 = val2;
  }
 
  static <V> void intervertirValeurs(Paire<V> paire) {
    V getVal1 = paire.getVal1();
    paire.setVal1(paire.getVal2());
    paire.setVal2(getVal1);
  } 
 
  public static void main(String[] args) {
    Paire<Integer> p1 = new Paire<Integer>(10, 20);
    System.out.println("Avant : " + p1.getVal1() + ", " + p1.getVal2());
 
    Paire<?> p2 = p1;
    intervertirValeurs(p2);
 
    System.out.println("Apres : " + p2.getVal1() + ", " + p2.getVal2());
    System.out.println(p2.getVal1().getClass().getName());
  }
}
Résultat :
C:\java>javac Paire.java
 
C:\java>java Paire
Avant : 10, 20
Apres : 20, 10
java.lang.Integer

La définition d'une variable avec un type paramétré utilisant un wilcard n'a que peu d'intérêt hormis de montrer que dans ce cas le compilateur peut réaliser la capture.

Une autre possibilité serait d'utiliser un type paramétré avec un wildcard pour le paramètre de la méthode.

Exemple ( code Java 5.0 ) :
  static void intervertirValeurs(Paire<?> paire) {
 
    Object getVal1 = paire.getVal1();
    paire.setVal1(paire.getVal2());
    paire.setVal2(getVal1);
  } 
 
  public static void main(String[] args) {
    Paire<Integer> p1 = new Paire<Integer>(10, 20);
    System.out.println("Avant : " + p1.getVal1() + ", " + p1.getVal2());
 
    Paire<Integer> p2 = p1;
    intervertirValeurs(p2);
 
    System.out.println("Apres : " + p2.getVal1() + ", " + p2.getVal2());
    System.out.println(p2.getVal1().getClass().getName());
  }
Résultat :
C:\java>javac Paire.java
Paire.java:30: error: incompatible types: Object cannot be converted to CAP#1
    paire.setVal1(paire.getVal2());
                               ^
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
Paire.java:31: error: incompatible types: Object cannot be converted to CAP#1
    paire.setVal2(getVal1);
                  ^
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
2 errors

Le code ne compile pas car lors de l'invocation des méthodes setVal1() et setVal2() le type des paramètres passés n'est pas connu à cause de l'utilisation du wildcard.

Dans cet exemple, le compilateur traite le paramètre comme étant de type Paire d'Object. Lorsque la méthode invoque les setters, le compilateur n'est pas en mesure de confirmer le type d'objet qui est inséré dans la liste, et une erreur est produite. Lorsque ce type d'erreur se produit, cela signifie généralement que le compilateur pense que vous affectez le mauvais type à une variable qui va à l'encontre du but des génériques qui est de renforcer la sécurité des types à la compilation.

La solution pour contourner cette erreur de compilation est de définir une méthode helper dont le type paramétré utilise une variable nommée. Le compilateur peut alors faire la capture et on peut faire référence au type paramétré dans le code.

Exemple ( code Java 5.0 ) :
  static <V> void intervertirValeursHelper(Paire<V> paire) {
    V getVal1 = paire.getVal1();
    paire.setVal1(paire.getVal2());
    paire.setVal2(getVal1);
  }
 
  static <V> void intervertirValeurs(Paire<?> paire) {
    intervertirValeursHelper(paire);
  }
 
  public static void main(String[] args) {
    Paire<Integer> p1 = new Paire<Integer>(10, 20);
    System.out.println("Avant : " + p1.getVal1() + ", " + p1.getVal2());
 
    Paire<?> p2 = p1;
    intervertirValeurs(p2);
 
    System.out.println("Apres : " + p2.getVal1() + ", " + p2.getVal2());
    System.out.println(p2.getVal1().getClass().getName());
  }

Grâce à la méthode helper, le compilateur utilise l'inférence pour déterminer que T est CAP#1, la variable de capture, dans l'invocation. Le code se compile et s'exécute correctement

Résultat :
C:\java>javac Paire.java
 
C:\java>java Paire
Avant : 10, 20
Apres : 20, 10
java.lang.Integer

Par convention de nom de la méthode helper est le nom de la méthode suivi de « Helper ».

La problématique est identique lorsque l'on tente d'écrire une méthode qui intervertit les deux premiers éléments d'une liste. Une première version d'une telle méthode attend en paramètre une List<?> pour permettre une utilisation sur une liste contenant n'importe quel éléments.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class MaClasse {
 
  public static void intervertirDeuxPremiers(List<?> liste) {
    if (liste.size() > 1) {
      Object valeur = liste.get(0);
      liste.set(0, liste.get(1));
      liste.set(1, valeur);
    }
  }
 
  public static void main(String... args) {
    List<String> liste = new ArrayList<String>();
    liste.add("valeur1");
    liste.add("valeur2");
    intervertirDeuxPremiers(liste);
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:9: error: incompatible types: Object cannot be converted to CAP#1
      liste.set(0, liste.get(1));
                            ^
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
MaClasse.java:10: error: incompatible types: Object cannot be converted to CAP#1
      liste.set(1, valeur);
                   ^
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
2 errors

La solution est de nouveau d'utiliser une méthode helper qui va permettre d'utiliser le type générique défini dans la signature pour pouvoir l'utiliser dans le code source.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class MaClasse {
 
  public static void intervertirDeuxPremiers(List<?> liste) {
    intervertirDeuxPremiersHelper(liste);
  }
 
  private static <T> void intervertirDeuxPremiersHelper(List<T> liste) {
    if (liste.size() > 1) {
      T valeur = liste.get(0);
      liste.set(0, liste.get(1));
      liste.set(1, valeur);
    }
  }
  
  public static void main(String... args) {
    List<String> liste = new ArrayList<String>();
    liste.add("valeur1");
    liste.add("valeur2");
    intervertirDeuxPremiers(liste);
  }
}

Malheureusement, une méthode capture helper ne permet de résoudre tous les cas.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<String> l1 = new ArrayList<String>();
    List<String> l2 = new ArrayList<String>();
    l1.add("valeur1");
    l2.add("valeur2");
    intervertirPremier(l1,l2);
    System.out.println("liste1 = "+l1);
    System.out.println("liste2 = "+l2);
  }
 
  public static void intervertirPremier(List<?> liste1, List<?> liste2) {
    Object valeur = liste1.get(0);
    liste1.set(0, liste2.get(0));
    liste2.set(0, valeur);
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:18: error: incompatible types: Object cannot be converted to CAP#1
    liste1.set(0, liste2.get(0));
                            ^
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
MaClasse.java:19: error: incompatible types: Object cannot be converted to CAP#1
    liste2.set(0, valeur);
                  ^
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
2 errors

La mise en oeuvre d'une méthode capture helper ne permet de résoudre le problème ou plutôt en implique un autre.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<String> l1 = new ArrayList<String>();
    List<String> l2 = new ArrayList<String>();
    l1.add("valeur1");
    l2.add("valeur2");
    intervertirPremier(l1,l2);
    System.out.println("liste1 = "+l1);
    System.out.println("liste2 = "+l2);
  }
 
  public static void intervertirPremier(List<?> liste1, List<?> liste2) {
    intervertirPremierHelper(liste1, liste2);
  }
  
  public static <T> void intervertirPremierHelper(List<T> liste1, List<T> liste2) {
    T valeur = liste1.get(0);
    liste1.set(0, liste2.get(0));
    liste2.set(0, valeur);
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:17: error: method intervertirPremierHelper in class MaClasse cannot be applied
 to given types;
    intervertirPremierHelper(liste1, liste2);
    ^
  required: List<T>,List<T>

  found: List<CAP#1>,List<CAP#2>
  reason: inferred type does not conform to equality constraint(s)
    inferred: CAP#2
    equality constraints(s): CAP#2,CAP#1
  where T is a type-variable:
    T extends Object declared in method <T>intervertirPremierHelper(List<T>,List<T>)
  where CAP#1,CAP#2 are fresh type-variables:
    CAP#1 extends Object from capture of ?
    CAP#2 extends Object from capture of ?
1 error

Le compilateur ne peut pas garantir qu'à l'exécution les types des deux listes soient exactement le même à cause de l'utilisation des wildcards. La solution dans ce cas est de ne pas utiliser de wilcards.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<String> l1 = new ArrayList<String>();
    List<String> l2 = new ArrayList<String>();
    l1.add("valeur1");
    l2.add("valeur2");
    intervertirPremier(l1,l2);
    System.out.println("liste1 = "+l1);
    System.out.println("liste2 = "+l2);
  }
 
  public static <T> void intervertirPremier(List<T> liste1, List<T> liste2) {
    T valeur = liste1.get(0);
    liste1.set(0, liste2.get(0));
    liste2.set(0, valeur);
  }
}
Résultat :
C:\java>javac MaClasse.java
 
C:\java>java MaClasse
liste1 = [valeur2]
liste2 = [valeur1]

 

5.10. Les bornes multiples (Multiple Bounds) avec l'intersection de types

L'intersection de types est une des fonctionnalités avancées des génériques en Java.

Le type paramétré peut avoir plusieurs bornes en utilisant l'intersection de types. Cela permet à une variable de type ou à un wildcard d'avoir plusieurs bornes.

Une intersection de types est une forme de type anonyme créée en combinant au moins deux types différents.

Par exemple :

<T extends A> T est un sous type de A

< T extends A & B> T est un sous type de A et B

L'intersection de types définit un type anonyme qui n'établit aucune relation hiérarchique entre les types combinés.

Exemple : deux interfaces qui proposent chacune une fonctionnalité

Exemple :
package com.jmdoudoux.dej;
 
public interface Journalisable {
  void journaliser(String message);
}
Exemple :
package com.jmdoudoux.dej;
 
public interface Calculable {
  String calculer();
}

Le besoin est de pouvoir passer en paramètre d'une méthode, un objet qui implémente ces deux interfaces.

Il est possible de définir une classe qui implémente ces deux interfaces.

Exemple :
package com.jmdoudoux.dej;
 
public class MonTraitement implements Journalisable, Calculable {
 
  @Override
  public void journaliser(String message) {
    System.out.println(message);
  }
 
  @Override
  public String calculer() {
    return "MaClasse";
  }
}

Il est possible de définir et d'utiliser la méthode qui attend en paramètre la classe.

Exemple :
package com.jmdoudoux.dej;
 
public class MaClasse {
 
  static void traiter(MonTraitement traitement) {
    String valeur = traitement.calculer();
    traitement.journaliser(valeur);
  }
 
  public static void main(String[] args) {
    MonTraitement mc = new MonTraitement();
    traiter(mc);
  }
}

Cela fonctionne mais cela introduit un couplage avec ce type particulier, ce qui manque de souplesse.

Il est possible de définir une interface qui hérite des deux interfaces.

Exemple :
package com.jmdoudoux.dej;
 
public interface JournalisableCalculable extends Journalisable, Calculable {
}

Il suffit alors que les classes implémentent cette interface qui devient le type attendu en paramètre de la méthode traiter().

Exemple :
package com.jmdoudoux.dej;
 
public class MaClasse {
  static void traiter(JournalisableCalculable traitement) {
    String valeur = traitement.calculer();
    traitement.journaliser(valeur);
  }
 
  public static void main(String[] args) {
    MonTraitement mc = new MonTraitement();
    traiter(mc);
  }
}

Ce mécanisme est type-safe mais requiert plus de code et manque aussi de souplesse.

Une autre possibilité est d'utiliser l'intersection de type dans un générique.

Exemple ( code Java 5.0 ) :
package com.jmdoudoux.dej;
 
public class MaClasse {
  static <T extends Journalisable & Calculable> void traiter(T traitement) {
    String valeur = traitement.calculer();
    traitement.journaliser(valeur);
  }
 
  public static void main(String[] args) {
    MonTraitement mc = new MonTraitement();
    traiter(mc);
  }
}

Avec une telle implémentation, il est possible de passer en paramètre de la méthode toute instance qui implémente les deux interfaces.

Cette solution est type-safe ne requiert pas la création de types superflus.

Un paramètre de type paramétré peut avoir plusieurs bornes. Les bornes sont séparées par un caractère & dans la définition pour former une intersection de types. Exemple :

<T extends Runnable & AutoCloseable>
<T extends Comparable & Serializable>
<T extends Number & Comparable<? super T>>

Les bornes multiples peuvent rapidement devenir non triviale à lire et à comprendre. Exemple :

List<Number & Comparable<? extends Number & Comparable<?>>>

Le premier type peut être une classe ou une interface. Les bornes multiples suivent les mêmes contraintes que celles suivies par une classe : le type ne peut pas étendre deux classes et si une des bornes est une classe, elle doit être en premier suivie éventuellement par une ou plusieurs interfaces.

Les bornes peuvent donc toutes être des interfaces.

Par exemple, il est possible de définir une contrainte telle que le type doit être un CharSequence et qu'il implémente l'interface Comparable.

Exemple ( code Java 5.0 ) :
public class MaClasse {
 
  public static <T extends CharSequence & Comparable<T>> int comparer(T v1, T v2) {
    return v1.compareTo(v2);
  }
}

Cela permet de s'assurer que seules des instances de CharSequence qui implémente l'interface Comparable pourront être passées en paramètres.

Un seul des types bornés peut être une classe et elle doit être obligatoirement en première position. Les autres types doivent être des interfaces.

Exemple ( code Java 5.0 ) :
public class MaClasse {
 
  public static <T extends Number & Comparable<? super T>> int comparer(T v1, T v2){
    return v1.compareTo(v2);
  }
}

Dans le cas contraire, le compilateur émet une erreur. C'est notamment le cas, si les bornes sont des classes :

Exemple ( code Java 5.0 ) :
public class MaClasse {
 
  public static <T extends String & Number > void afficher(T v1) {
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:3: error: interface expected here
  public static <T extends String & Number > void afficher(T v1) {
                                    ^
1 error

C'est aussi le cas si la classe n'est pas en première position :

Exemple ( code Java 5.0 ) :
public class MaClasse {
 
  public static <T extends Comparable<? super T> & Number > int comparer(T v1, T v2) {
    return 0;
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:3: error: interface expected here
  public static <T extends Comparable<? super T> & Number > int comparer(T v1, T v2) {
                                                   ^
1 error

Il est parfois nécessaire d'utiliser l'intersection de type notamment pour garantir la rétro compatibilité du code pré générique.

Par exemple, la méthode max() de la classe Collections :

public static <T extends Object & Comparable<? super T>> T max​(Collection<? extends T> coll)

Le paramètre générique étend Object et Comparable pour des raisons de compatibilité.

La signature historique de la méthode max() est :

public static Object max(Collection)

A cause du type erasure, si la signature générique de la méthode avait été :

public static <T extends Comparable<? super T>> max(Collection<? extends T>)

Alors la signature de la méthode dans le byte-code serait équivalent à :

public static Comparable max(Collection)

Ce qui est différent de la signature historique. Pour maintenir la même signature, il faut utiliser un paramètre de type avec intersection de type :

public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T>)

 

5.11. L'effacement de type (type erasure)

Les génériques en Java ont été ajoutés au langage pour permettre une vérification de type au moment de la compilation mais à cause de leur implémentation, ils n'ont aucune utilité à l'exécution.

Pour s'assurer que le bytecode généré soit toujours compatible avec celui généré par les versions précédentes de Java, le compilateur applique un processus appelé effacement de type (type erasure) sur les génériques lors de la compilation.

Les génériques en Java ne sont donc une fonctionnalité qui n'est utilisable qu'au niveau du compilateur. Le compilateur Java effectue les vérifications mais il met en oeuvre l'effacement de type qui n'inclut aucune information relative aux génériques dans le bytecode. Il remplace les types génériques par Object ou le type d'une borne et ajoute des casts lorsque cela est nécessaire.

 

5.11.1. Le choix de l'effacement de type

L'un des défis des évolution sur un langage comme Java est qu'il doit supporter la rétrocompatibilité. Lorsque les génériques ont été ajoutés au langage, il a été décidé de ne pas les inclure dans le bytecode produit par le compilateur.

Pour maintenir la compatibilité ascendante et éviter des modifications majeures de l'environnement d'exécution de Java, les génériques sont mis en oeuvre en utilisant une technique appelée effacement de type (type erasure). Cela revient dans le bytecode à ne pas inclure les informations de type supplémentaires et à ajouter des casts lorsque cela est nécessaire.

L'effacement des types permet de pas créer de nouvelles classes pour chaque type paramétré utilisé comme c'est le cas dans d'autres langages qui n'utilisent pas l'effacement de type.

 

5.11.2. La mise en oeuvre de l'effacement de type

A la compilation, les génériques ne sont pas inclus dans le bytecode : toutes les références aux variables de type sont remplacées. L'effacement de type est mis en oeuvre par le compilateur, basiquement en appliquant plusieurs règles :

  • les paramètres de type non bornés sont remplacés par Object
  • les paramètres de type borné sont remplacés par le type de leur première borne
  • des casts sont insérés lorsque cela est nécessaire
  • des méthodes pont (bridge) sont générées au besoin pour préserver le polymorphisme

Ainsi, le bytecode généré par le compilateur ne contient que des classes, interfaces et méthodes normales, sans aucune information relative aux génériques.

Exemple avec une méthode générique :

Exemple ( code Java 5.0 ) :
public <T> List<T> methodeGenerique(List<T> liste) {
  // ...

}

Le bytecode généré par le compilateur est équivalent à :

Exemple ( code Java 5.0 ) :
public List<Object> methodeGenerique(List<Object> liste) {
  // ...

}

Mais comme le bytecode ne contient aucune information relative aux génériques, le bytecode généré est plutôt équivalent à :

Exemple ( code Java 5.0 ) :
public List methodeGenerique(List liste) {
  // ...

}

Pour un type générique, tous les types paramétrés partagent le même type à l'exécution : le type brut (Rawtype) qui correspond au type générique sans l'argument de type.

Si le type est borné, alors le type sera remplacé par la borne au moment de la compilation :

Exemple ( code Java 5.0 ) :
public <T extends MaCLasse> void methodeGenerique(T donnees) {
  // ...

}

Le bytecode généré par le compilateur est équivalent à :

Exemple ( code Java 5.0 ) :
public void methodeGenerique(MaClasse donnees) {
  // ...

}

 

5.11.2.1. L'effacement de type pour un type paramétré inconnu

Une classe ou une interface peut être déclarée avec un ou plusieurs types paramétrés. Le ou les types paramétrés doivent être fournis lors de la création d'une instance avec l'opérateur new.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<String> chaines = new ArrayList<String>();
    chaines.add("test");
  }
}

L'effacement de type (type erasure) opéré par le compilateur retire tous les types génériques dans le bytecode pour les remplacer par défaut par le type Object. A l'issue de la compilation, les informations de type sont effacées par le compilateur : ainsi le bytecode créé est similaire à celui par le compilateur du JDK 1.4.

Cela garantit une compatibilité du bytecode entre les différentes versions de Java. Ainsi, une List ou une List avec un générique sont toutes représentées par le même type dans le bytecode et donc à l'exécution : le type brut List.

Après avoir compilé la classe, il est possible d'utiliser l'outil javap du JDK pour vérifier l'effacement du type dans le bytecode créé dans le compilateur.

Résultat :
C:\java>javap -c MaClasse.class
Compiled from "MaClasse.java"
public class MaClasse {
  public MaClasse();
    Code:
       0: aload_0
       1: invokespecial #1            // Method java/lang/Object."<init>":()V
       4: return
 
  public static void main(java.lang.String...);
    Code:
       0: new           #2            // class java/util/ArrayList
       3: dup
       4: invokespecial #3            // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #4            // String test
      11: invokeinterface #5,  2      // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      16: pop
      17: return
}

Ceci s'applique aussi si le type paramétré est utilisé comme type d'un paramètre ou d'une valeur de retour d'une méthode. Exemple :

Exemple ( code Java 5.0 ) :
public class MonConteneur<T> {
 
    public T valeur;
 
    public MonConteneur(T valeur) {
        this.valeur = valeur;
    }
 
    public T get() {
        return valeur;
    }
}
Résultat :
C:\java>javap -s MonConteneur.class
Compiled from "MonConteneur.java"
public class MonConteneur<T> {
  public T valeur;
    descriptor: Ljava/lang/Object;
  public MonConteneur(T);
    descriptor: (Ljava/lang/Object;)V
 
  public T get();
    descriptor: ()Ljava/lang/Object;
}

Le paramètre de type est remplacé par Object. Dans le code qui utilise cette classe générique, un cast est ajouté pour obtenir le type correspondant au type générique utilisé.

Exemple ( code Java 5.0 ) :
public class MaClasse {
 
  public static void main() {
    MonConteneur<String> mc = new MonConteneur<>("test");
    String valeur = mc.get();  
  }
}

L'outil javap peut de nouveau être utilisé pour visualiser le bytecode généré suite à la compilation de la classe.

Résultat :
C:\java>javap -c MaClasse.class
Compiled from "MaClasse.java"
public class MaClasse {
  public MaClasse();
    Code:
       0: aload_0
       1: invokespecial #1           // Method java/lang/Object."<init>":()V
       4: return
 
  public static void main();
    Code:
       0: new           #2          // class MonConteneur
       3: dup
       4: ldc           #3          // String test
       6: invokespecial #4          // Method MonConteneur."<init>":(Ljava/lang/Object;)V
       9: astore_0
      10: aload_0
      11: invokevirtual #5          // Method MonConteneur.get:()Ljava/lang/Object;
      14: checkcast     #6          // class java/lang/String
      17: astore_1
      18: return
}

La méthode get() invoquée est celle qui retourne un objet de type Object et un cast vers le type java.lang.String est effectué dans la méthode main().

 

5.11.2.2. L'effacement de type pour un type paramétré avec borne supérieure

Si le type paramétré utilise une borne supérieure, alors le type utilisé est celui précisé dans la borne.

Exemple ( code Java 5.0 ) :
public class MonConteneur<T extends Comparable<T>> {
 
  public T valeur;
 
  public MonConteneur(T valeur) {
    this.valeur = valeur;
  }
 
  public T get() {
    return valeur;
  }
}

L'outil javap peut de nouveau être utilisé pour visualiser le bytecode généré suite à la compilation de la classe.

Résultat :
C:\java>javap -c MonConteneur.class
Compiled from "MonConteneur.java"
public class MonConteneur<T extends java.lang.Comparable<T>> {
  public T valeur;
 
  public MonConteneur(T);
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: aload_1
       6: putfield      #2                  // Field valeur:Ljava/lang/Comparable;
       9: return
 
  public T get();
    Code:
       0: aload_0
       1: getfield      #2                  // Field valeur:Ljava/lang/Comparable;
       4: areturn
}

Le type utilisé n'est plus Object mais Comparable, celui précisé dans la borne. Si le type paramétré utilise plusieurs types alors c'est le premier défini qui est utilisé comme type dans le bytecode.

Exemple ( code Java 5.0 ) :
import java.io.Serializable;
 
public class MonConteneur<T extends Serializable & Comparable<T>> {
 
    public T valeur;
 
    public MonConteneur(T valeur) {
        this.valeur = valeur;
    }
 
    public T get() {
        return valeur;
    }
}
Résultat :
C:\java>javap -c MonConteneur.class
Compiled from "MonConteneur.java"
public class MonConteneur<T extends java.io.Serializable & java.lang.Comparable<T>> {
  public T valeur;
 
  public MonConteneur(T);
    Code:
       0: aload_0
       1: invokespecial #1               // Method java/lang/Object."<init>":()V
       4: aload_0
       5: aload_1
       6: putfield      #2               // Field valeur:Ljava/io/Serializable;
       9: return
 
  public T get();
    Code:
       0: aload_0
       1: getfield      #2               // Field valeur:Ljava/io/Serializable;
       4: areturn
}

 

5.11.2.3. Les méthodes pont (bridge method)

L'effacement de type peut parfois amener à une situation où deux méthodes doivent avoir la même signature avec des types de retour différents. Cela n'est pas possible dans le code source Java puisque la valeur de retour ne fait pas partie de la signature de la méthode. Dans ce cas, le compilateur va ajouter des méthodes pont (bridge method) dans le bytecode.

Exemple ( code Java 5.0 ) :
public class MonConteneur<T> {
 
  public T valeur;
 
  public MonConteneur(T valeur) {
    this.valeur = valeur;
  }
 
  public T get() {
    return valeur;
  }
  
  public void set(T valeur) {
    this.valeur = valeur;
  }
}

Une classe fille hérite de la classe MonConteneur en la typant avec java.lang.String.

Exemple ( code Java 5.0 ) :
public class MonConteneurChaine<T> extends MonConteneur<String> {
 
  public MonConteneurChaine(String valeur) {
    super(valeur);
  }
 
  public String get() {
    return valeur;
  }
  
  public void set(String valeur) {
    this.valeur = valeur;
  }
}

La décompilation du bytecode affiche deux méthodes supplémentaires dans la classe fille : deux méthodes pont.

Résultat :
C:\java>javap -c -s -v MonConteneurChaine
Classfile /C:/java/MonConteneurChaine.class
  Last modified 2 mai 2022; size 656 bytes
  MD5 checksum 5253b513a9a143d48352dce0bf35dd6b
  Compiled from "MonConteneurChaine.java"
public class MonConteneurChaine<T extends java.lang.Object> 
  extends MonConteneur<java.lang.String>
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
...
{
  public MonConteneurChaine(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: invokespecial #1              // Method MonConteneur."<init>":(Ljava/lang/Object;)V
         5: return
      LineNumberTable:
        line 6: 0
        line 7: 5
 
  public java.lang.String get();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2              // Field valeur:Ljava/lang/Object;
         4: checkcast     #3              // class java/lang/String
         7: areturn
      LineNumberTable:
        line 10: 0
 
  public void set(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2              // Field valeur:Ljava/lang/Object;
         5: return
      LineNumberTable:
        line 14: 0
        line 15: 5
 
  public void set(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #3              // class java/lang/String
         5: invokevirtual #4              // Method set:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 3: 0
 
  public java.lang.Object get();
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #5              // Method get:()Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 3: 0
}
Signature: #18                      // <T:Ljava/lang/Object;>LMonConteneur<Ljava/lang/String;>;
SourceFile: "MonConteneurChaine.java"

A cause de l'effacement de type, il y a deux méthodes get() :

  • une qui retourne une instance d'Object, qui est une méthode pont
  • une qui retourne une instance de type String

La méthode set() présente aussi deux surcharges:

  • une qui attend en paramètre une instance d'Object, qui est une méthode pont
  • une qui attend en paramètre une instance de String

Remarque : la possibilité d'avoir deux surcharges identiques qui ne diffèrent que par leur type retour n'est pas permise dans le code source mais tout à fait possible dans le bytecode.

La classe fille ne peut pas être sûre que ses méthodes invoquées en utilisant la méthode héritée dont le type est effacé ou la méthode avec le type explicite. Les méthodes pont sont ajoutées pour assurer le bon traitement dans tous les cas notamment en effectuant un cast vers le type java.lang.String. Si ce cast échoue alors une exception de type ClassCastException est levée assurant ainsi la vérification que le type passé en paramètre est bien de type String.

Les méthodes pont sont aussi utilisées pour permettre à une classe étendant une classe générique ou implémentant une interface générique (avec un paramètre de type concret) d'être toujours utilisable comme un type brut.

Exemple ( code Java 5.0 ) :
import java.util.Comparator;
 
public class MonComparator implements Comparator<Integer> {
  public int compare(Integer a, Integer b) {
    return b - a;
  }
}

Sans la méthode pont ajoutée, il ne serait pas possible d'utiliser la classe sous sa forme brute (raw type) donc sans préciser le type générique.

Résultat :
C:\java>javap -c -s -v MonComparator
Classfile /C:/java/MonComparator.class
  Last modified 6 mai 2022; size 567 bytes
  MD5 checksum 9f635c783594dae3d1fc497b7141d062
  Compiled from "MonComparator.java"
public class MonComparator extends java.lang.Object 
  implements java.util.Comparator<java.lang.Integer>
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
...
{
  public MonComparator();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1          // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
 
  public int compare(java.lang.Integer, java.lang.Integer);
    descriptor: (Ljava/lang/Integer;Ljava/lang/Integer;)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: aload_2
         1: invokevirtual #2          // Method java/lang/Integer.intValue:()I
         4: aload_1
         5: invokevirtual #2          // Method java/lang/Integer.intValue:()I
         8: isub
         9: ireturn
      LineNumberTable:
        line 5: 0
 
  public int compare(java.lang.Object, java.lang.Object);
    descriptor: (Ljava/lang/Object;Ljava/lang/Object;)I
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=3, locals=3, args_size=3
         0: aload_0
         1: aload_1
         2: checkcast     #3         // class java/lang/Integer
         5: aload_2
         6: checkcast     #3         // class java/lang/Integer
         9: invokevirtual #4         // Method compare:(Ljava/lang/Integer;Ljava/lang/Integer;)I
        12: ireturn
      LineNumberTable:
        line 3: 0
}
Signature: #16                    // Ljava/lang/Object;Ljava/util/Comparator<Ljava/lang/Integer;>;
SourceFile: "MonComparator.java"

Le compilateur ajoute une méthode pont compare() qui attend en paramètre deux instances de type Object. L'implémentation de cette méthode pont effectue un cast des paramètres vers le type Integer et invoque la méthode définie dans le code source. Il est ainsi possible d'utiliser la classe MonComparator dans sa forme brute.

Exemple ( code Java 5.0 ) :
  public static void main(String[] args) {
    Object val1 = 1;
    Object val2 = 3;
 
    Comparator comp = new MonComparator();
    int resultat = comp.compare(val1, val2);  
  }  

Les méthodes pont permettent aussi la bonne mise en oeuvre de l'effacement de type.

Exemple ( code Java 5.0 ) :
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
 
public class TestComparator {
 
  public static <T> T min(List<T> liste, Comparator<T> comp) {
    T resultat = null;
    if (liste != null && liste.size() > 0) {
      resultat = liste.get(0);
      for (T element : liste) {
        if (comp.compare(element, resultat) > 0) {
          resultat = element;
        }
      }
    }
    return resultat;
  }
 
  public static void main(String[] args) {
    List<Integer> liste = Arrays.asList(1, 2, 3, 4, 5);
    Integer plusPetit = min(liste, new MonComparator());
    System.out.println(plusPetit);
  }
}

Avec l'effacement de type, le type T dans le bytecode est remplacé par Object et des cast selon le type du contexte d'invocation de la méthode. Dans ce cas aussi, c'est la méthode pont, celle attendant en paramètre deux instances de type Object qui est invoquée.

 

5.12. La différence entre l'utilisation d'un argument de type et un wildcard

L'utilisation d'un paramètre de type permet d'imposer d'avoir le même type de paramètres à une méthode. Par exemple pour une méthode dont le but est de copier les éléments d'une liste source dans une liste destination

Exemple ( code Java 5.0 ) :
public static <T extends Number> void copier(List<T> src, List<T> dest) {
  // ...

} 

L'intérêt est de s'assurer que les éléments des deux listes seront les mêmes, vérifié par le compilateur et donc que la copie peut se faire sans soucis. Cela n'est pas possible en utilisant des wildcards.

Exemple ( code Java 5.0 ) :
public static void copier(List<? extends Number> src, List<? extends Number> dest) {
  // ...

} 

Lors de l'invocation de la méthode, il est possible de fournir des instances de types différents tels que List<Integer> et List<Double>

Il n'est aussi pas possible d'utiliser des bornes multiples avec un wildcard.

Exemple ( code Java 5.0 ) :
import java.util.List;
import java.io.Serializable;
 
public class ListUtils {
 
  public static void afficher(List<? extends Number & Serializable> liste) {
    // ...

  }
} 
Résultat :
C:\java>javac ListUtils.java
ListUtils.java:6: error: > expected
  public static void afficher(List<? extends Number & Serializable> liste) {
                                                    ^
ListUtils.java:6: error: ')' expected
  public static void afficher(List<? extends Number & Serializable> liste) {
                                                     ^
ListUtils.java:6: error: ';' expected
  public static void afficher(List<? extends Number & Serializable> liste) {
                                                                  ^
ListUtils.java:6: error: <identifier> expected
  public static void afficher(List<? extends Number & Serializable> liste) {
                                                                         ^
4 errors 

Les wildcards peuvent avoir une borne supérieure ou inférieure.

Exemple ( code Java 5.0 ) :
  public static void afficher(List<? super Integer> list) {
    // ...

  }

Un paramètre de type peut avoir une borne supérieure mais pas une borne inférieure. Le code ci-dessous ne compile pas.

Exemple ( code Java 5.0 ) :
import java.util.List;  

public class Utils {  

  public static super Integer> void print(List list) {
    // ...

  }
}  
Résultat :
C:\java>javac Utils.java
Utils.java:5: error: > expected
  public static  void print(List list) {
                  ^
Utils.java:5: error: illegal start of type
  public static  void print(List list) {
                   ^
Utils.java:5: error: '(' expected
  public static  void print(List list) {
                                ^
3 errors

 

5.13. Les conséquences et les effets de bord de l'effacement de type

L'effacement de type implique certains effets de bords ou contraintes dont il faut tenir compte sous réserve pour la plupart d'avoir des erreurs à la compilation.

 

5.13.1. Les types génériques et les casts

L'utilisation d'un cast avec un type générique différent provoque une erreur de compilation.

Exemple ( code Java 5.0 ) :
import java.util.List;
import java.util.ArrayList;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<Integer> li = new ArrayList<Integer>();
    List<Number>  ln = (List<Number>) li;  //  erreur du compilateur

  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:9: error: incompatible types: List<Integer> cannot be converted to List<Number>
    List<Number>  ln = (List<Number>) li;  //  erreur du compilateur
                                      ^
1 error

Il existe une exception si le cast et la variable cible utilisent un wilcard non borné.

Exemple ( code Java 5.0 ) :
import java.util.List;
import java.util.ArrayList;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<Integer> li = new ArrayList<Integer>();
    List<?>  ln = (List<?>) li;  // ok

  }
}

Si le type générique est le même et que les classes sont compatibles alors le cast est aussi autorisé par le compilateur.

Exemple ( code Java 5.0 ) :
import java.util.List;
import java.util.ArrayList;
 
public class MaClasse {
 
  public static void main(String... args) {
    List<String> ls = new ArrayList<String>();
    ArrayList<String> als = (ArrayList<String>) ls;  // OK  

  }
}

 

5.13.2. Le type des instances génériques

Comme l'effacement de type empêche de conserver le type générique dans le byte-code, il n'est pas possible de distinguer le type de deux instances d'une même classe dont seul le type générique diffère dans le code source.

Cela a une conséquence, c'est que les classes de deux variables d'un même type avec des types génériques différents sont identiques et correspond au type brut.

Exemple ( code Java 5.0 ) :
public class Conteneur<T> {
 
  private T valeur;
 
  public Conteneur(T valeur) {
    this.valeur = valeur;
  }
 
  public T get() {
    return valeur;
  }
 
  public static void main(String... args) {
    Conteneur<String> cs = new Conteneur<String>("test");
    Conteneur<Integer> ci = new Conteneur<Integer>(123);
    System.out.println(cs.getClass()==ci.getClass());
  }
}
Résultat :
C:\java>javac Conteneur.java
 
C:\java>java Conteneur
true
Exemple ( code Java 5.0 ) :
import java.util.List;
import java.util.ArrayList;
 
public class MaClasse {
 
  public static void main(String[] args) {
    List<Integer> entiers = new ArrayList<Integer>();
    List<Double> doubles = new ArrayList<Double>();
    System.out.println(entiers.getClass() == doubles.getClass());
  }
}
Résultat :
true

 

5.13.3. Les génériques et les méthodes surchargées

Lors de l'invocation d'une méthode avec plusieurs surcharges à partir d'un type générique, le comportement peut paraitre surprenant comme dans l'exemple ci-dessous :

Exemple ( code Java 5.0 ) :
public class MaClasse<T> {
  
  public static void main(String[] args) {
    MaClasse<String> maClasse = new MaClasse<>();
    maClasse.traiter("test");
  }
 
  String obtenirDonnees(Object s) { 
    return "object";
  }
  
  String obtenirDonnees(String s) { 
    return "string";
  }
  
  public void traiter(T t) { 
    System.out.println(obtenirDonnees(t)); 
  } 
}
Résultat :
object

C'est un effet de bord de l'effacement de type : comme dans le bytecode, les types génériques sont remplacés par le type Object, c'est la surcharge de type Object qui est invoquée à l'exécution.

 

5.13.4. Les génériques et l'opérateur instanceof

Le type utilisé avec l'opérateur instanceof doit être réifiable : cela implique que le type doit être connu à l'exécution, ce qui n'est pas le cas avec les types génériques à cause de l'effacement de type.

Il est tout à fait légal d'utiliser une instance d'un type paramétré à la gauche de l'opérateur instanceof.

Exemple ( code Java 5.0 ) :
public class MaClasse<T> {
 
  public boolean tester(T o) {
    return o instanceof String;
  }
 
  public static void main(String... args) {
    MaClasse<String> mcString = new MaClasse<>();
    MaClasse<Integer> mcInteger = new MaClasse<>();
 
    System.out.println(mcString.tester("abc"));
    System.out.println(mcInteger.tester(123));
  }
}
Résultat :
C:\java>javac MaClasse.java
 
C:\java>java MaClasse
true
false

Il n'est pas possible d'utiliser un type générique comme type dans un opérateur instanceof puisque l'effacement de type ne permet pas au runtime de connaître le type paramétré.

Exemple ( code Java 5.0 ) :
public class MaClasse<T> {
 
  public boolean tester(Object o) {
    return o instanceof T;
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:4: error: illegal generic type for instanceof
    return o instanceof T;
                        ^
1 error

Il existe une exception qui concerne l'utilisation d'un wildcard non borné qui est considéré comme un type réifiable.

Exemple ( code Java 5.0 ) :
import java.util.List;
 
public class MaClasse<T> {
 
  public boolean tester(Object o) {
    return o instanceof List<?>;
  }
}

Dans l'exemple ci-dessus, cela permet de vérifier que l'objet passé en paramètre est une List de quelque chose.

Il est possible de fournir en plus la classe du type générique utilisé et de vérifier que le type de l'objet et assignable à la classe du type générique.

Exemple ( code Java 5.0 ) :
public class MaClasse<T> {
 
  private Class<T> t;
 
  public MaClasse(Class<T> t) {
    this.t = t;
  }
 
  public boolean tester(Object o) {
    return o != null && t.isAssignableFrom(o.getClass());
  }
 
  public static void main(String... args) {
    MaClasse<String> mcString = new MaClasse<>(String.class);
    MaClasse<Integer> mcInteger = new MaClasse<>(Integer.class);
 
    System.out.println(mcString.tester("abc"));
    System.out.println(mcInteger.tester(123));
  }
}
Résultat :
C:\java>javac MaClasse.java
 
C:\java>java MaClasse
true
true

L'inconvénient de cette solution est de devoir fournir le type deux fois : dans le paramètre de type et en paramètre.

 

5.13.5. Les surcharges avec un type générique et un type Object

Le compilateur émet une erreur lorsque qu'une méthode possède deux surcharges, l'un avec un paramètre de type et l'autre qui attend un paramètre de type Object.

Exemple ( code Java 5.0 ) :
public class MaClasseGenerique<T> {
  
  public void traiter(Object o) { 
  }
  
  public void traiter(T t) { 
  } 
}
Résultat :
C:\java>javac MaClasseGenerique.java
MaClasseGenerique.java:6: error: name clash: traiter(T) and traiter(Object) have the same
 erasure
  public void traiter(T t) {
              ^
  where T is a type-variable:
    T extends Object declared in class MaClasse
1 error

 

5.13.6. La création d'une instance de type générique

Il n'est pas possible de créer une instance d'un type générique en utilisant l'opérateur new : à cause de l'effacement de type, le type paramétré n'est pas inclus dans le bytecode.

Le compilateur émet donc une erreur si on tente de créer une instance d'un paramètre de type avec l'opérateur new.

Exemple ( code Java 5.0 ) :
public class MaClasseGenerique<T> {
 
  private T instance;
  
  public MaClasseGenerique() {
    this.instance = new T();
  }
}
Résultat :
C:\java>javac MaClasseGenerique.java
MaClasseGenerique.java:6: error: unexpected type
    this.instance = new T();
                        ^
  required: class
  found:    type parameter T
  where T is a type-variable:
    T extends Object declared in class MaClasse
1 error

Il n'est pas non plus possible de créer une instance en utilisant la méthode newInstance() sur la classe d'un type paramétré.

Exemple ( code Java 5.0 ) :
public class MaClasseGenerique<T> {
 
  private T instance;
  
  public MaClasseGenerique() {
    this.instance = T.class.newInstance();
  }
}
Résultat :
C:\java>javac MaClasseGenerique.java
MaClasseGenerique.java:6: error: cannot select from a type variable
    this.instance = T.class.newInstance();
                     ^
1 error

Cette instanciation n'est pas possible car le type T n'est pas connu dans la déclaration de la classe générique.

Pour créer une instance, il faut utiliser l'API Reflection qui impose d'avoir une instance de type Class du type générique utilisé passée en paramètre.

Exemple ( code Java 5.0 ) :
import java.util.Date;
 
public class MaClasseGenerique<T> {
 
  private T instance;
  
  public MaClasseGenerique(Class<T> classe) throws InstantiationException, 
    IllegalAccessException {
    this.instance = classe.newInstance();
    System.out.println(instance);
  }
  
  public static void main(String[] args) throws Exception {
    MaClasseGenerique<Date> maClasse = new MaClasseGenerique<Date>(Date.class);
  }
}

Dans l'exemple ci-dessus, le constructeur par défaut est invoqué dynamiquement pour obtenir une instance.

 

5.13.7. La création d'une instance d'un tableau de type générique

Il n'est pas possible de créer une instance d'un tableau d'un type générique avec l'opérateur new. A l'exécution le type n'est pas connu et cela pourrait conduire à des erreurs.

Exemple ( code Java 5.0 ) :
import java.lang.reflect.Array;
 
public class MaClasse<T> {
 
  private T[] tableau;
  
  public MaClasse()  {
    this.tableau = new T[10];
  }  
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:9: error: generic array creation
    this.tableau = new T[10];
                   ^
1 error

Comme la création d'une instance d'un type générique, la création d'un tableau de générique n'est pas possible car le type réel du générique ne sera connu qu'à l'exécution. Ainsi dans la déclaration de la classe, il n'est pas possible de créer une instance d'un type générique que le compilateur ne peut pas connaître.

Il est cependant possible de créer une instance dynamiquement en utilisant l'API Reflexion. Pour cela, il est nécessaire de passer en paramètre une instance de type Class du type générique pour créer une instance du tableau en invoquant la méthode newInstance() de la classe Array en lui passant en paramètre le type et le nombre d'éléments.

Exemple ( code Java 5.0 ) :
import java.lang.reflect.Array;
import java.util.Date;
 
public class MaClasseGenerique<T> {
 
  private T[] tableau;
  
  public MaClasseGenerique (Class<T> classe) throws InstantiationException, 
    IllegalAccessException  {
    this.tableau = (T[]) Array.newInstance(classe, 10);
    System.out.println(tableau);
  }
  
  public static void main(String[] args) throws Exception {
    MaClasseGenerique<Date> maClasse = new MaClasseGenerique<Date>(Date.class);
  }
}

Il est nécessaire de faire un cast car la méthode newInstance() renvoie un Object. Cette technique est utilisée dans la méthode toArray() des classes de l'API Collections.

 

5.13.8. Les varargs et les génériques

Le compilateur transforme un paramètre de type varargs en un tableau. Lorsque le varargs concerne un type paramétré, l'effacement de type remplace le type paramétré par Object. Il y a donc un risque de pollution de heap.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
 
public class MaClasse {
 
  public void traiter(List<String>... liste) {
    Object[] tableau = liste;
    tableau[0] = Arrays.asList(2, 4, 6);
    String s = liste[0].get(0);    // ClassCastException

  }
  
  public static void main(String[] args) {
    MaClasse instance = new MaClasse();
    instance.traiter(new ArrayList<String>());
  }
}
Résultat :
C:\java >javac -Xlint:unchecked MaClasseGenerique.java
MaClasseGenerique:7: warning: [unchecked] Possible heap pollution from parameterized
 vararg type List<String>
  public void traiter(List<String>... liste) {
                                      ^
MaClasseGenerique.java:15: warning: [unchecked] unchecked generic array creation for
 varargs parameter of type List<String>[]
    instance.traiter(new ArrayList<String>());
                    ^
2 warnings

Le compilateur affiche deux avertissements de type unchecked à la compilation sur un potentiel risque de pollution du heap lors de l'utilisation d'un tableau sur un type générique

Lors de la déclaration d'une méthode avec varargs qui a un ou des types paramétrés, et que l'on est sûr que le corps de la méthode ne lèvera pas de ClassCastException en raison d'une mauvaise gestion du paramètre de type varargs, il est possible de demander au compilateur de ne pas générer un avertissement que le compilateur génère pour ces types de méthodes avec varargs en utilisant l'une des options suivantes :

  • pour une méthode qui ne peut pas être redéfinie (constructeurs, méthodes final, static et private (depuis Java 9)) : il faut utiliser l'annotation @SafeVarargs
  • pour toute autre méthode : il faut utiliser l'annotation @SuppressWarnings({"unchecked", "varargs"}) sur la méthode
Exemple ( code Java 5.0 ) :
import java.util.ArrayList;  
import java.util.List;  
 
public class VarArgs<T>{  
  public final void afficher(T... messages) {
    for (T message : messages) {  
      System.out.println(message);  
    }  
  }  
 
  public static void main(String[] args) {  
    VarArgs<String> varargs = new VarArgs<String>();
    varargs.afficher("msg1","msg2","msg3");
  }     
}
Résultat :
C:\java>javac -Xlint:unchecked VarArgs.java
VarArgs.java:6: warning: [unchecked] Possible heap pollution from parameterized vararg type T
  public final void afficher(T... messages) {
                                  ^
  where T is a type-variable:
    T extends Object declared in class VarArgs
1 warning
 
C:\java>java VarArgs
msg1
msg2
msg3

Dans l'exemple ci-dessus, comme il n'y a pas de risque de pollution du heap, il est possible d'utiliser l'annotation @Safevarargs sur la méthode final pour demander au compilateur de ne pas afficher l'avertissement.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;  
import java.util.List;  
 
public class VarArgs<T>{  
  @SafeVarargs  
  public final void afficher(T... messages) {
    for (T message : messages) {  
      System.out.println(message);  
    }  
  }  
 
  public static void main(String[] args) {  
    VarArgs<String> varargs = new VarArgs<String>();
    varargs.afficher("msg1","msg2","msg3");
  }     
}
Résultat :
C:\java>javac VarArgs.java
 
C:\java>java VarArgs
msg1
msg2
msg3

 

5.13.9. Les collisions de méthodes liées à l'effacement de type

Il n'est pas possible de définir une surcharge d'une méthode qui attend en paramètre un générique alors qu'il existe déjà une autre surcharge qui corresponde à la méthode qui sera générée par le compilateur en application de l'effacement de type.

Ainsi, il n'est pas possible de définir deux surcharges d'une même méthode d'une classe générique T acceptant en paramètre pour la première un type T et pour la seconde un type Object.

Exemple ( code Java 5.0 ) :
public class MaClasse<T> {
 
  public void traiter(Object valeur) {
  }
 
  public void traiter(T valeur) {
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:6: error: name clash: traiter(T) and traiter(Object) have the same erasure
  public void traiter(T valeur) {
              ^
  where T is a type-variable:
    T extends Object declared in class MaClasse
1 error

Ce n'est pas possible non plus si une surcharge est déjà héritée.

Exemple ( code Java 5.0 ) :
public class MaClasse<T> {
 
  public boolean equals(T t) {
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:3:
error: name clash: equals(T) in MaClasse and equals(Object) in Object have the same erasure,
 yet neither overrides the other
  public  boolean equals(T t) {
                  ^
  where T is a type-variable:
    T extends Object declared in class MaClasse
1 error

Dans l'exemple ci-dessous la méthode equals(Object) est héritée de la classe Object et la même méthode est générée par le compilateur suite à l'application de l'effacement de type.

 

5.13.10. Le contournement lors de l'utilisation d'un type brut d'une classe générique

L'effacement de type peut parfois avoir des effets surprenants aux premiers abords. Exemple :

Exemple ( code Java 5.0 ) :
import java.util.Arrays;
import java.util.List;
 
public class MonConteneur<T> {
  private T valeur;
 
  public List<Integer> getEntiers() {
    return Arrays.asList(1, 2, 3);
  }
}
Exemple ( code Java 5.0 ) :
public class TestMonConteneur {
 
  public static void afficher(MonConteneur conteneur) {
    for (int entier : conteneur.getEntiers()) {
      System.out.println(entier);
    }
  }
 
  public static void main(String[] args) {
    MonConteneur mc = new MonConteneur();
    afficher(mc);
  } 
}

Cette classe ne se compile pas à cause de l'effacement de type et de l'utilisation du type brut comme paramètre de la méthode.

Résultat :
C:\java>javac TestMonConteneur.java
TestMonConteneur.java:4: error: incompatible types: Object cannot be converted to int
    for (int entier : conteneur.getEntiers()) {
                                          ^
1 error

A la compilation, l'effacement de type remplace List<Integer> par son type brut List. Le type but utilisé dans le paramètre de la méthode ne fournit aucune information au compilateur sur le fait que la classe soit générique.

Une solution pour contourner ce problème est d'utiliser un paramètre de type avec wildcard comme type en paramètre de la méthode.

Exemple ( code Java 5.0 ) :
public class TestMonConteneur {
 
  public static void afficher(MonConteneur<?> conteneur) {
    for (int entier : conteneur.getEntiers()) {
      System.out.println(entier);
    }
  }
 
  public static void main(String[] args) {
    MonConteneur mc = new MonConteneur();
    afficher(mc);
  } 
}

Cette version de la classe se compile et s'exécute correctement.

Résultat :
C:\java\workspace_2021-09\Test\src>javac TestMonConteneur.java
 
C:\java\workspace_2021-09\Test\src>java TestMonConteneur
1
2
3

L'ajout du paramètre de type avec un wildcard, implique que le compilateur va utiliser le type de plus précis possible selon la borne utilisée et ajouter un cast vers ce type. Dans l'exemple, au lieu d'utiliser Object du type brut, un cast est ajouté vers Integer puisque dans le code c'est une List d'Integer qui est utilisée.

Résultat :
C:\java\workspace_2021-09\Test\src>javap -c -s TestMonConteneur.class
Compiled from "TestMonConteneur.java"
public class TestMonConteneur {
  public TestMonConteneur();
    descriptor: ()V
    Code:
       0: aload_0
       1: invokespecial #1                // Method java/lang/Object."<init>":()V
       4: return
 
  public static void afficher(MonConteneur<?>);
    descriptor: (LMonConteneur;)V
    Code:
       0: aload_0
       1: invokevirtual #2                // Method MonConteneur.getEntiers:()Ljava/util/List;
       4: invokeinterface #3,  1          // InterfaceMethod java/util/List.iterator:()Ljava/
util/Iterator;
       9: astore_1
      10: aload_1
      11: invokeinterface #4,  1          // InterfaceMethod java/util/Iterator.hasNext:()Z
      16: ifeq          42
      19: aload_1
      20: invokeinterface #5,  1          // InterfaceMethod java/util/Iterator.next:()Ljava/
lang/Object;
      25: checkcast     #6                // class java/lang/Integer
      28: invokevirtual #7                // Method java/lang/Integer.intValue:()I
      31: istore_2
      32: getstatic     #8                // Field java/lang/System.out:Ljava/io/PrintStream;
      35: iload_2
      36: invokevirtual #9                // Method java/io/PrintStream.println:(I)V
      39: goto          10
      42: return
 
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    Code:
       0: new           #10               // class MonConteneur
       3: dup
       4: invokespecial #11               // Method MonConteneur."<init>":()V
       7: astore_1
       8: aload_1
       9: invokestatic  #12               // Method afficher:(LMonConteneur;)V
      12: return
}

 

5.14. Les restrictions dans l'utilisation des génériques

Plusieurs restrictions s'appliquent lors de la mise en oeuvre des génériques et sont vérifiées par le compilateur.

 

5.14.1. L'implémentation plusieurs fois de la même interface générique

Il n'est pas possible d'implémenter plusieurs fois la même interface avec des types génériques différents ou identiques.

Exemple ( code Java 5.0 ) :
public interface MonInterface<T> {
}
Exemple ( code Java 5.0 ) :
public class MaClasse implements MonInterface<String>, MonInterface<String>  {
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:1: error: repeated interface
public class MaClasse implements MonInterface<String>, MonInterface<String>  {
                                                                   ^
1 error

 

5.14.2. Les génériques ne supportent pas le sous-typage

Les génériques ne proposent pas de support pour les sous-types car cela pose des problèmes pour assurer la sécurité de type. Ainsi, List<T> n'est pas considérée comme un sous-type de List<S> où S est un super-type de T.

Par exemple, même si Integer hérite de Number, il n'est pas possible d'assigner une List<Integer> à une List<Number>.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class MaClasse {
 
  public MaClasse () {
 
    List<Number> valeurs = new ArrayList<Integer>();
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:8: error: incompatible types: ArrayList<Integer> cannot be converted 
 to List<Number>
    List<Number> valeurs = new ArrayList<Integer>();
                           ^
1 error

De la même manière, il n'est pas possible d'affecter deux variables d'un même type avec des types génériques différents.

Exemple ( code Java 5.0 ) :
import java.util.List;
 
public class MaClasse {
 
  public MaClasse() {
    List<Number> nombres;
    List<Integer> entiers;
    nombres = entiers;
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:8: error: incompatible types: List<Integer> cannot be converted to List<Number>
    nombres = entiers;
              ^
1 error

L'exemple ci-dessous permet de comprendre pourquoi ce n'est pas autorisé : si c'était autorisé cela permettrait d'ajouter des Double dans une List d'Integer.

Exemple ( code Java 5.0 ) :
import java.util.ArrayList;
import java.util.List;
 
public class MaClasse {
 
  public static void main(String[] args) {
    List<Integer> listeEntiers = new ArrayList<Integer>();
    listeEntiers.add(100);
    List<Number> listeNombres = listeEntiers; // erreur a la compilation

    listeNombres.add(3.14116);
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:9: error: incompatible types: List<Integer> cannot be converted to List<Number>
    List<Number> listeNombres = listeEntiers; // erreur a la compilation
                                ^
1 error

C'est pour éviter ce genre de soucis que le compilateur ne permet pas le support du sous-typage dans les génériques.

 

5.14.3. Les types génériques et les membres statiques

Le type générique est précisé à la création d'une instance. Cependant un membre static est lié à la classe et non à une instance. C'est la raison pour laquelle il n'est pas possible d'utiliser un type paramétré dans un membre statique.

Un champ static est géré au niveau classe car il est partagé par toutes les instances. Il n'est donc pas possible qu'il soit générique puisque le type générique est précisé à la création d'une instance et peut donc être différent.

Exemple ( code Java 5.0 ) :
public class MaClasse<T> {
 
  private static T donnees;
  
  public static void traiter(T donnees) {
  }
  
  public static T obtenir() {
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:3: error: non-static type variable T cannot be referenced from a static context
  private static T donnees;
                 ^
MaClasse.java:5: error: non-static type variable T cannot be referenced from a static context
  public static void traiter(T donnees) {
                             ^
MaClasse.java:8: error: non-static type variable T cannot be referenced from a static context
  public static T obtenir() {
                ^
3 errors

 

5.14.4. Les génériques et les types primitifs

Les génériques ne fonctionnent qu'avec des classes à cause de l'effacement de type. Il n'est donc pas possible d'utiliser des types primitifs comme type générique. Il est impératif d'utiliser les classes wrapper correspondantes. Les valeurs peuvent être fournies ou obtenues en utilisant l'autoboxing ou l'unboxing.

Exemple ( code Java 5.0 ) :
import java.util.List;
 
public class MaClasse {
 
  public MaClasse () {
    List<int> valeurs;
  }
}
Résultat :
C:\java>javac MaClasse.java
MaClasse.java:6: error: unexpected type
                List<int> valeurs;
                     ^
  required: reference
  found:    int
1 error

Il est impératif d'utiliser les classes wrapper correspondantes. Les valeurs peuvent être fournies ou obtenues en utilisant l'autoboxing ou l'unboxing.

Exemple ( code Java 5.0 ) :
import java.util.List;
 
public class MaClasse {
 
  public MaClasse () {
    List<Integer> valeurs;
  }
}

 

5.14.5. Une exception ne peut pas être générique

Le compilateur émet une erreur lors de la définition d'une exception, donc une classe qui hérite de Throwable, avec un type générique.

Exemple ( code Java 5.0 ) :
public class MonException<T> extends Throwable {
}
Résultat :
C:\java>javac MonException.java
MonException.java:2: error: a generic class may not extend java.lang.Throwable
public class MonException<T> extends Throwable {
                                     ^
1 error

Il n'est pas possible non plus d'utiliser un type paramétré dans une clause catch : dans ce cas, le compilateur émet une erreur.

Exemple ( code Java 5.0 ) :
public class MaClasseGenerique<T> {
 
  public void traiter() {
    try {
      // ...

   }  catch (T ex) {
   }
  }
}
Résultat :
C:\java>javac -Xlint:unchecked MaClasseGenerique.java
MaClasseGenerique.java:6: error: unexpected type
   }  catch (T ex) {
             ^
  required: class
  found:    type parameter T
  where T is a type-variable:
    T extends Object declared in class MaClasseGenerique
1 error

 

5.14.6. Une annotation ne peut pas être générique

La définition d'une interface d'annotation ne peut pas être générique.

Exemple ( code Java 5.0 ) :
public @interface MonAnnotation<T> {
}
Résultat :
C:\java>javac MonAnnotation.java
MonAnnotation.java:1: error: annotation type MonAnnotation cannot be generic
public @interface MonAnnotation<T> {
                                ^
1 error

 

5.14.7. Une énumération ne peut pas être générique

La définition d'une énumération ne peut pas être générique.

Exemple ( code Java 5.0 ) :
public enum MonEnumeration<T> {
}
Résultat :
C:\java>javac MonEnumeration.java
MonEnumeration.java:1: error: '{' expected
public enum MonEnumeration<T> {
                          ^
MonEnumeration.java:1: error: < expected
public enum MonEnumeration<T> {
                           ^
MonEnumeration.java:1: error: <identifier> expected
public enum MonEnumeration<T> {
                            ^
MonEnumeration.java:2: error: reached end of file while parsing
}
 ^
4 errors

 

 


[ Précédent ] [ Sommaire ] [ Suivant ] [Télécharger ]      [Accueil ]

78 commentaires Donner une note à l´article (5)

 

Copyright (C) 1999-2022 Jean-Michel DOUDOUX. Vous pouvez copier, redistribuer et/ou modifier ce document selon les termes de la Licence de Documentation Libre GNU, Version 1.1 ou toute autre version ultérieure publiée par la Free Software Foundation; les Sections Invariantes étant constitués du chapitre Préambule, aucun Texte de Première de Couverture, et aucun Texte de Quatrième de Couverture. Une copie de la licence est incluse dans la section GNU FreeDocumentation Licence. La version la plus récente de cette licence est disponible à l'adresse : GNU Free Documentation Licence.