• Home
  • LLMs
  • Python
  • Docker
  • Kubernetes
  • Java
  • Maven
  • All
  • About
Java | Généricité (generics)
  1. Introduction
  2. Classes génériques
  3. Utiliser le type générique
  4. Wildcard (?)
    1. Utiliser le wildcard comme sous-type du type générique
    2. Utiliser le wildcard comme super-type du type générique
    3. Utiliser le wildcard pour représenter n'importe quel type
  5. Méthodes génériques
  6. Constructeurs génériques
  7. Lancer des exceptions génériques

  1. Introduction
    La généricité offre la capacité de spécifier des types génériques pour les variables (incluant le type de retour des méthodes et les paramètres des méthodes et des constructeurs).

    Le type générique est utilisé uniquement à la compilation ; le compilateur s'en sert pour vérifier que le type est correct lors de l'instanciation des classes et l'invocation des méthodes.

    L'information sur le type générique est complètement perdue dans le bytecode.
  2. Classes génériques
    Pour définir un type générique pour une classe il faut suivre son nom par un identifiant qui doit etre placé entre les caractères "<" et ">".

    Il est possible de spécifier plusieurs types génériques ; les noms des types génériques doivent être placés l'un à coté de l'autre séparés par le caractère "," : <T,R,P>.

    Typiquement les noms des types génériques doivent être : T, E, R, P, …

    Le nom du type générique peut être utilisé comme :
    • type des attributs et des variables ;

    • type de retour des méthodes ;

    • type des paramètres des méthodes ;

    • ou type des paramètres des constructeurs.

    public class MyGenericClass<T> {
        T attr1;
    
        MyGenericClass() {
        }
    
        MyGenericClass(T attr1) {
            this.attr1 = attr1;
        }
    
        public T getAttr1() {
            return attr1;
        }
    
        public void setAttr1(T attr1) {
            this.attr1 = attr1;
        }
    }
    Comme mentionné précédemment le type générique est présent uniquement dans le code source et il est complètement perdu dans le bytecode. En fait, le code précédent est considéré, lors de la génération du bytecode, comme s'il a été écrit comme suit :

    class MyGenericClass {
        Object attr1;
    
        MyGenericClass() {
        }
    
        MyGenericClass(Object attr1) {
            this.attr1 = attr1;
        }
    
        public Object getAttr1() {
            return attr1;
        }
    
        public void setAttr1(Object attr1) {
            this.attr1 = attr1;
        }
    }
    Le compilateur remplace le type générique par le type de la classe Object.
  3. Utiliser le type générique
    Au niveau de la définition des classes génériques, le type générique n'est pas très impressionnant, surtout en connaissant que cette information est présente uniquement dans le code source et elle est perdue une fois le bytecode est généré.

    Mais, cette impression va changer en commençant à utiliser le type générique pour déclarer/instancier une classe générique et invoquer ses méthodes génériques ; le compilateur s'en sert pour contrôler et vérifier que le type spécifié lors de l'instanciation est correct partout dans le code où les méthodes génériques de cette classe sont invoquées.
    public class MyGenericClassTest {
        public static void main(String[] args) {
            MyGenericClass<Number> myGenericClass;
    
            myGenericClass = new MyGenericClass<Number>(Integer.valueOf(0));
    
            myGenericClass.setAttr1(Integer.valueOf(10));
    
            Number v1 = myGenericClass.getAttr1(); // ~ Number v1 = (Number) myGenericClass.getAttr1();
    
            System.out.println(v1);
        }
    }
    Le type que l'on souhaite utilisé doit être spécifié lors de la déclaration des variables qui référencent la classe générique.
    La déclaration est faite de la même façon que la définition de la classe générique : le type doit être placé entre les caractères "<" et ">".

    Encore une fois, le type générique est présent uniquement dans le code source et il est perdu dans le bytecode :
    public class MyGenericClassTest {
        private static MyGenericClass<Number> foo(MyGenericClass<Number> myGenericClass) {
            if(myGenericClass == null)
                return null;
    
            Number v1 = myGenericClass.getAttr1();
    
            if(v1 == null)
                return new MyGenericClass<Number>(Integer.valueOf(0));
    
            if(v1 instanceof Byte)
                myGenericClass.setAttr1(v1.byteValue() * (byte) 2);
            else if(v1 instanceof Short)
                myGenericClass.setAttr1(v1.shortValue() * (short) 2);
            else if(v1 instanceof Integer)
                myGenericClass.setAttr1(v1.intValue() * 2);
            else if(v1 instanceof Long)
                myGenericClass.setAttr1(v1.longValue() * 2L);
            else if(v1 instanceof Float)
                myGenericClass.setAttr1(v1.floatValue() * 2.0f);
            else if(v1 instanceof Double)
                myGenericClass.setAttr1(v1.doubleValue() * 2.0d);
    
            return myGenericClass;
        }
    
        public static void main(String[] args) {
            MyGenericClass<Number> myGenericClass = new MyGenericClass<Number>(Integer.valueOf(10));
            myGenericClass = foo(myGenericClass);
        }
    }
    Le code précédent est considéré, lors de la génération du bytecode, comme s'il a été écrit comme suit :
    public class MyGenericClassTest {
        private static MyGenericClass foo(MyGenericClass myGenericClass) {
            if(myGenericClass == null)
                return null;
    
            Number v1 = (Number) myGenericClass.getAttr1();
    
            if(v1 == null)
                return new MyGenericClass(Integer.valueOf(0));
    
            if(v1 instanceof Byte)
                myGenericClass.setAttr1(v1.byteValue() * (byte) 2);
            else if(v1 instanceof Short)
                myGenericClass.setAttr1(v1.shortValue() * (short) 2);
            else if(v1 instanceof Integer)
                myGenericClass.setAttr1(v1.intValue() * 2);
            else if(v1 instanceof Long)
                myGenericClass.setAttr1(v1.longValue() * 2L);
            else if(v1 instanceof Float)
                myGenericClass.setAttr1(v1.floatValue() * 2.0f);
            else if(v1 instanceof Double)
                myGenericClass.setAttr1(v1.doubleValue() * 2.0d);
    
            return myGenericClass;
        }
    
        public static void main(String[] args) {
            MyGenericClass myGenericClass = new MyGenericClass(Integer.valueOf(10));
            myGenericClass = foo(myGenericClass);
        }
    }
    Le compilateur utilise le type générique pour ajouter un cast lors de l'affectation d'une référence de type générique à une variable du type spécifié lors de la déclaration de la classe.

    Lors de la génération du bytecode, le code suivant Number v1 = myGenericClass.getAttr1(); sera remplacé par Number v1 = (Number) myGenericClass.getAttr1();.

    Notes :
    • Le type spécifié lors de l'instanciation d'une classe doit être exactement le même type utilisé pour la déclaration de la variable qui référence cette classe. Le compilateur générera une erreur si le type est différent (un sous type est considéré différent) :
      MyGenericClass<Number> myGenericClass;
      
      // compiler error: Type mismatch: cannot convert from MyGenericClass<Integer> to MyGenericClass<Number>
      myGenericClass = new MyGenericClass<Integer>(Integer.valueOf(0));
      Cette restriction s'applique aussi aux valeurs de retour et arguments des méthodes :
      public class MyGenericClassTest {
          private static MyGenericClass<Number> foo(MyGenericClass<Number> myGenericClass) {
              return null;
          }
      
          public static void main(String[] args) {
              MyGenericClass<Integer> myGenericClass = null;
      
              // compiler error: The method foo(MyGenericClass<Number>) in the type MyGenericClassTest is not applicable for the arguments (MyGenericClass<Integer>)
              foo(myGenericClass);
      
              // compiler error: The method foo(MyGenericClass<Number>) in the type MyGenericClassTest is not applicable for the arguments (MyGenericClass<Float>)
              foo(new MyGenericClass<Float>());
      
              // compiler error: Type mismatch: cannot convert from MyGenericClass<Number> to MyGenericClass<Integer>
              myGenericClass = foo(new MyGenericClass<Number>());
          }
      }
      La règle générale est la suivante : le compilateur va toujours interdire l'affectation d'une référence à une autre si les types génériques des deux références ne correspondent pas exactement.
      MyGenericClass<Integer> myGenericClass_Integer = null;
      MyGenericClass<Float> myGenericClass_Float = null;
      
      // compiler error: Type mismatch: cannot convert from MyGenericClass<Float> to MyGenericClass<Integer>
      myGenericClass_Integer = myGenericClass_Float;
    • La règle précédente s'applique uniquement pour l'instanciation des classes et la déclaration des variables et des paramètres qui utilisent explicitement le type générique.

      Si l'instanciation des classes et la déclaration des variables et des paramètres n'utilisent pas le type générique, alors le compilateur ne ferra aucune vérification et ne générera aucune erreur. Cependant, il affichera un message d'avertissement :
      // Warning: Type safety: The expression of type MyGenericClass needs unchecked conversion to conform to MyGenericClass<Number>
      MyGenericClass<Number> myGenericClass_Number = new MyGenericClass(new String("10"));
      
      // Warning: MyGenericClass is a raw type. References to generic type MyGenericClass<T> should be parameterized
      MyGenericClass myGenericClass = null;
      
      // Warning: Type safety: The constructor MyGenericClass(Object) belongs to the raw type MyGenericClass.
      // References to generic type MyGenericClass<T> should be parameterized
      myGenericClass = new MyGenericClass(new String("10"));
      
      // Warning: Type safety: The expression of type MyGenericClass needs unchecked conversion to conform to MyGenericClass<Number>
      myGenericClass_Number = myGenericClass;
      
      // OK
      myGenericClass = myGenericClass_Number;
    • Le type spécifié lors de l'invocation des méthodes génériques de la classe peut être le même type ou un sous-type du type spécifié lors de l'instanciation de cette classe :
      myGenericClass.setAttr1(new Float(10.0f)); // OK : Float est une sous-classe de Number
      
      myGenericClass.setAttr1(Integer.valueOf(10));  // OK : Integer est une sous-classe de Number
      Le compilateur affichera une erreur si le type spécifié lors de l'invocation des méthodes génériques de la classe est différent du type spécifié lors de l'instanciation de cette classe :
      // compiler error: The method setAttr1(Number) in the type MyGenericClass<Number> is not applicable for the arguments (String)
      myGenericClass.setAttr1(new String("10"));
  4. Wildcard (?)
    Le wildcard (le caractère "?") est utilisé pour préciser que le type générique peut être un sous-type ou un super-type d'un certain type.
    Il peut être aussi utilisé pour préciser que le type générique peut être n'importe quel type !

    Attention, le wildcard peut être utilisé uniquement à la déclaration des variables (incluant les paramètres des méthodes et les types de retour des méthodes) :
    public class MyGenericClassTest {
        public static void main(String[] args) {
            MyGenericClass<? extends Number> myGenericClass_1; // OK
            MyGenericClass<? super Integer> myGenericClass_2; // OK
            MyGenericClass<?> myGenericClass_3; // OK
    
            // compiler error: Cannot instantiate the type MyGenericClass<? extends Number>
            myGenericClass_1 = new MyGenericClass<? extends Number>();
    
            // compiler error: Cannot instantiate the type MyGenericClass<? super Integer>
            myGenericClass_2 = new MyGenericClass<? super Integer>();
    
            // compiler error: Cannot instantiate the type MyGenericClass<?>
            myGenericClass_3 = new MyGenericClass<?>();
    
            myGenericClass_1 = new MyGenericClass<Float>(); // OK : Float extends Number
            myGenericClass_2 = new MyGenericClass<Object>(); // OK : Integer extends Object
            myGenericClass_3 = new MyGenericClass<String>(); // OK : ? means any type
        }
    
        public static MyGenericClass<? extends Number> foo(MyGenericClass<? extends Number> myGenericClass_1) {
            return null;
        }
    }
    1. Utiliser le wildcard comme sous-type du type générique
      Dans l'exemple suivant, on peut assigner à la variable myGenericClass n'importe quelle référence dont le type générique est le même type ou un sous-type de la classe Number.

      public class MyGenericClassTest {
          public static void main(String[] args) {
              MyGenericClass<? extends Number> myGenericClass;
      
              myGenericClass = new MyGenericClass<Integer>(); // OK
      
              myGenericClass = new MyGenericClass<Float>(); // OK
          }
      }
      Il y a cependant quelques restrictions avec l'utilisation de cette syntaxe :
      • Vous ne pouvez pas utiliser la référence créée avec cette syntaxe pour modifier les attributs dont le type est générique (typiquement en accédant directement à ces attributs ou en invoquant des méthodes qui modifient ces attributs).
        MyGenericClass<? extends Number> myGenericClass;
        
        myGenericClass = new MyGenericClass<Integer>(Integer.valueOf(10)); // OK
        
        // compiler error: The method setAttr1(capture#2-of ? extends Number) in the type MyGenericClass<capture#2-of ? extends Number> is not applicable for the arguments (Integer)
        myGenericClass.setAttr1(Integer.valueOf(10));
      • Vous pouvez utiliser la référence créée avec cette syntaxe, seulement, pour lire les attributs dont le type est générique (typiquement en accédant directement à ces attributs ou en invoquant des méthodes qui renvoient ces attributs), mais il faut ajouter un cast explicite pour pouvoir le faire :
        // compiler error: Type mismatch: cannot convert from capture#3-of ? extends Number to Integer
        Integer intVar1 = myGenericClass.getAttr1();
        
        Integer intVar2 = (Integer) myGenericClass.getAttr1();
        Attention, le compilateur vérifie uniquement que le type utilisé pour le cast est un sous-type du type générique défini à la déclaration de la variable. Ce qui veut dire que vous pouvez avoir une erreur à l'exécution si le cast n'est pas possible :
        package com.mtitek.generics;
        
        public class MyGenericClassTest {
            public static void main(String[] args) {
                MyGenericClass<? extends Number> myGenericClass;
        
                myGenericClass = new MyGenericClass<Float>(new Float(10.0)); // OK
        
                Integer intVar1 = (Integer) myGenericClass.getAttr1(); // Runtime error: ClassCastException
            }
        }
        Résultat d'exécution du code :
        Exception in thread "main" java.lang.ClassCastException: java.lang.Float cannot be cast to java.lang.Integer
            at com.mtitek.generics.MyGenericClassTest.main(MyGenericClassTest.java:9)
    2. Utiliser le wildcard comme super-type du type générique
      On peut assigner à la variable n'importe quelle référence dont le type générique est le même type ou un super-type de la classe Number.

      public class MyGenericClassTest {
          public static void main(String[] args) {
              MyGenericClass<? super Integer> myGenericClass;
      
              myGenericClass = new MyGenericClass<Integer>(); // OK
      
              // compiler error: Type mismatch: cannot convert from MyGenericClass<Float> to MyGenericClass<? super Integer>
              myGenericClass = new MyGenericClass<Float>();
          }
      }
      Vous pouvez utiliser la référence créée avec cette syntaxe pour lire et modifier les attributs dont le type est générique (typiquement en accédant directement à ces attributs ou en invoquant des méthodes qui renvoient ou modifient ces attributs), mais il faut ajouter un cast explicite pour pouvoir le faire :
      public class MyGenericClassTest {
          public static void main(String[] args) {
              MyGenericClass<? super Integer> myGenericClass;
      
              myGenericClass = new MyGenericClass<Integer>(); // OK
      
              Object objVar1 = Integer.valueOf(10);
      
              // compiler error: The method setAttr1(capture#2-of ? super Integer) in the type MyGenericClass<capture#2-of ? super Integer> is not applicable for the arguments (Object)
              myGenericClass.setAttr1(objVar1);
      
              myGenericClass.setAttr1((Integer) objVar1); // OK : il faut ajouter un cast explicite
      
              // compiler error: Type mismatch: cannot convert from capture#4-of ? super Integer to Integer
              Integer intVar1 = myGenericClass.getAttr1();
      
              Integer intVar2 = (Integer) myGenericClass.getAttr1(); // OK : il faut ajouter un cast explicite
          }
      }
      Attention, le compilateur vérifie uniquement que le type utilisé pour le cast est un sous-type du type générique défini à la déclaration de la variable. Ce qui veut dire que vous pouvez avoir une erreur à l'exécution si le cast n'est pas possible :
      package com.mtitek.generics;
      
      public class MyGenericClassTest {
          public static void main(String[] args) {
              MyGenericClass<? super Integer> myGenericClass;
      
              myGenericClass = new MyGenericClass<Integer>(); // OK
      
              Object objVar1 = new Float(10.0);
      
              myGenericClass.setAttr1((Integer) objVar1); // Runtime error: ClassCastException
          }
      }
      Résultat d'exécution du code :
      Exception in thread "main" java.lang.ClassCastException: java.lang.Float cannot be cast to java.lang.Integer
          at com.mtitek.generics.MyGenericClassTest.main(MyGenericClassTest.java:11)
    3. Utiliser le wildcard pour représenter n'importe quel type
      Dans l'exemple suivant, on peut assigner à la variable myGenericClass n'importe quellle référence dont le type générique peut être n'importe quel type.

      public class MyGenericClassTest {
          public static void main(String[] args) {
              MyGenericClass<?> myGenericClass;
      
              myGenericClass = new MyGenericClass<Integer>(); // OK
      
              myGenericClass = new MyGenericClass<Object>(); // OK
          }
      }
      En fait, la syntaxe <?> est équivalente à <? extends Object>.
      Ce qui veut dire que cette syntaxe a les mêmes restrictions (voir ci-dessus) de l'utilisation du wildcard comme sous-type du type générique:

      • Vous ne pouvez pas utiliser la référence créée avec cette syntaxe pour modifier les attributs dont le type est générique (typiquement en accédant directement à ces attributs ou en invoquant des méthodes qui modifient ces attributs).
        MyGenericClass<?> myGenericClass;
        
        myGenericClass = new MyGenericClass<Integer>(Integer.valueOf(10)); // OK
        
        // compiler error: The method setAttr1(capture#2-of ?) in the type MyGenericClass<capture#2-of ?> is not applicable for the arguments (Integer)
        myGenericClass.setAttr1(Integer.valueOf(10));
      • Vous pouvez utiliser la référence créée avec cette syntaxe, seulement, pour lire les attributs dont le type est générique (typiquement en accédant directement à ces attributs ou en invoquant des méthodes qui renvoient ces attributs), mais il faut ajouter un cast explicite pour pouvoir le faire :
        // compiler error: Type mismatch: cannot convert from capture#2-of ? to Integer
        Integer intVar1 = myGenericClass.getAttr1();
        
        Integer intVar2 = (Integer) myGenericClass.getAttr1();
        Attention, le compilateur vérifie uniquement que le type utilisé pour le cast est correct pour l'instruction d'affectation.
        Ce qui veut dire que vous pouvez avoir une erreur à l'exécution si le cast n'est pas possible :
        package com.mtitek.generics;
        
        public class MyGenericClassTest {
            public static void main(String[] args) {
                MyGenericClass<?> myGenericClass;
        
                myGenericClass = new MyGenericClass<Float>(new Float(10.0)); // OK
        
                Integer intVar1 = (Integer) myGenericClass.getAttr1(); // Runtime error: ClassCastException
            }
        }
        Résultat d'exécution du code :
        Exception in thread "main" java.lang.ClassCastException: java.lang.Float cannot be cast to java.lang.Integer
            at com.mtitek.generics.MyGenericClassTest.main(MyGenericClassTest.java:9)
  5. Méthodes génériques
    Il est possible de définir des types génériques au niveau des méthodes.
    La syntaxe de la définition ainsi que l'utilisation du type générique restent semblables à ce que nous avons vu ci-dessus.
    La seule particularité est que la définition du type générique doit être faite juste avant le type du retour de la méthode.

    Le type générique peut être spécifié à la fois pour le type de retour de la méthode, ses paramètres, et ses variables locales.

    public class MyGenericMethodTest {
        private static <T> T getValue(T value, T defaultValue) {
            return value != null ? value : defaultValue;
        }
    
        private static <R extends Number> Number multiply(R arg1, R arg2) {
            R var1 = arg1;
            R var2 = arg2;
            Number var3 = null;
    
            if (var1 == null || var2 == null)
                return null;
    
            if (var1 instanceof Byte)
                var3 = var1.byteValue() * var2.byteValue();
            else if (var1 instanceof Short)
                var3 = var1.shortValue() * var2.shortValue();
            else if (var1 instanceof Integer)
                var3 = var1.intValue() * var2.intValue();
            else if (var1 instanceof Long)
                var3 = var1.longValue() * var2.longValue();
            else if (var1 instanceof Float)
                var3 = var1.floatValue() * var2.floatValue();
            else if (var1 instanceof Double)
                var3 = var1.doubleValue() * var2.doubleValue();
    
            return var3;
        }
    
        public static void main(String[] args) {
            // explicit syntax: MyGenericMethodTest.<Integer>
            Integer result1 = MyGenericMethodTest.<Integer>getValue(1, 2);
            System.out.println(result1);
    
            // implicit syntax: : you can omit the <Integer> type
            String result2 = getValue(null, "default");
            System.out.println(result2);
    
            // another test: you still need an explicit cast otherwise you get this error "Type mismatch: cannot convert from Number to Integer"
            Integer result3 = (Integer) MyGenericMethodTest.<Integer>multiply(10, 5);
            System.out.println(result3);
        }
    }
  6. Constructeurs génériques
    Les constructeurs peuvent aussi définir des types génériques ; la syntaxe est la même que celle utilisée pour les méthodes.
    public class MyGenericConstructorTest {
        Number field1;
    
        <R extends Number> MyGenericConstructorTest(R arg1) {
            field1 = arg1;
        }
    
        public static void main(String[] args) {
            MyGenericConstructorTest myGenericConstructorTest_Integer = new MyGenericConstructorTest(Integer.valueOf(10));
            System.out.println(myGenericConstructorTest_Integer.field1);
    
            MyGenericConstructorTest myGenericConstructorTest_Float = new MyGenericConstructorTest(new Float(10.0f));
            System.out.println(myGenericConstructorTest_Float.field1);
        }
    }
  7. Lancer des exceptions génériques
    Pour lancer des exceptions génériques, on utilise la même syntaxe pour définir des types génériques pour les méthodes.
    On utilise le mot clé throws pour lancer l'exception spécifiée par le type générique.
    Il faut que la définition du type générique précise que le type est un sous-type des classes Throwable, Error, Exception, ou n'importe quelle classe qui hérite de ces classes.
    package com.mtitek.generics;
    
    public class MyGenericExceptionTest {
        private static <T extends RuntimeException, R> void foo(R arg1) throws T {
            if(arg1 == null)
                throw new IllegalArgumentException();
            else if(! (arg1 instanceof Number))
                throw new NumberFormatException();
        }
    
        public static void main(String[] args) {
            try {
                foo(null);
            } catch (RuntimeException e) {
                e.printStackTrace();
            }
    
            try {
                foo("NOT A NUMBER");
            } catch (RuntimeException e) {
                e.printStackTrace();
            }
        }
    }
    Résultat d'exécution du code :
    java.lang.IllegalArgumentException
        at com.mtitek.generics.MyGenericExceptionTest.foo(MyGenericExceptionTest.java:20)
        at com.mtitek.generics.MyGenericExceptionTest.main(MyGenericExceptionTest.java:6)
    java.lang.NumberFormatException
        at com.mtitek.generics.MyGenericExceptionTest.foo(MyGenericExceptionTest.java:22)
        at com.mtitek.generics.MyGenericExceptionTest.main(MyGenericExceptionTest.java:12)
© 2025  mtitek