• Home
  • LLMs
  • Docker
  • Kubernetes
  • Java
  • All
  • About
Java | Generics
  1. Introduction
  2. Generic Classes
  3. Using the Generic Type
  4. Wildcard (?)
    1. Using the wildcard as a subtype of the generic type
    2. Using the wildcard as a supertype of the generic type
    3. Using the wildcard to represent any type
  5. Generic Methods
  6. Generic Constructors
  7. Throwing Generic Exceptions

  1. Introduction
    Generics provide the ability to specify parameterized types for variables (including return types of methods, method parameters, and constructor parameters).

    The generic type information is used at compile time; the compiler uses it to ensure type safety when instantiating classes and invoking methods.

    Generic type information is erased at runtime through a process called type erasure.
  2. Generic Classes
    To define a generic type for a class, you must follow its name with a type parameter placed between the characters "<" and ">".

    It is possible to specify multiple generic type parameters; the names of the type parameters should be listed side by side, separated by the character ",": <T,R,P>.

    By convention, generic type parameter names should be single uppercase letters: T (Type), E (Element), K (Key), V (Value), N (Number), etc.

    The generic type parameter can be used as:
    • the type of instance variables and local variables;

    • the return type of methods;

    • the type of method parameters;

    • or the type of constructor parameters.

    public class MyGenericClass<T> {
        private T attr1;
    
        public MyGenericClass() {
        }
    
        public MyGenericClass(T attr1) {
            this.attr1 = attr1;
        }
    
        public T getAttr1() {
            return attr1;
        }
    
        public void setAttr1(T attr1) {
            this.attr1 = attr1;
        }
    }
    Due to type erasure, generic type information is removed during compilation and replaced with raw types. The previous code is effectively compiled as if it had been written as follows:
    public class MyGenericClass {
        private Object attr1;
    
        public MyGenericClass() {
        }
    
        public MyGenericClass(Object attr1) {
            this.attr1 = attr1;
        }
    
        public Object getAttr1() {
            return attr1;
        }
    
        public void setAttr1(Object attr1) {
            this.attr1 = attr1;
        }
    }
    The compiler replaces unbounded type parameters with Object, while bounded type parameters are replaced with their upper bound.
  3. Using the Generic Type
    At the level of generic class definitions, the generic type may not seem very impressive, especially knowing that this information exists only in the source code and is lost once the bytecode is generated (due to type erasure).

    However, this impression changes when we start using the generic type to declare/instantiate a generic class and invoke its generic methods; the compiler uses it to ensure and verify that the type specified during instantiation is used correctly throughout the code where the class's generic methods are called.
    public class MainClass {
        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(); // Explicit cast not needed due to generics
    
            System.out.println(v1);
        }
    }
    The type you want to use must be specified when declaring the variables that reference the generic class. The declaration is done the same way as the generic class definition: the type must be placed between the "<" and ">" characters.

    Again, the generic type exists only in the source code and is lost in the bytecode due to type erasure:
    public class MainClass {
        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);
        }
    }
    The previous code is treated, during bytecode generation, as if it had been written like this (due to type erasure):
    public class MainClass {
        private static MyGenericClass foo(MyGenericClass myGenericClass) {
            if(myGenericClass == null)
                return null;
    
            Number v1 = (Number) myGenericClass.getAttr1(); // Cast inserted by compiler
    
            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);
        }
    }
    The compiler uses the generic type to add automatic type casts when assigning a generic reference to a variable of the specified type during class declaration.

    When bytecode is generated, the following code Number v1 = myGenericClass.getAttr1(); is replaced by Number v1 = (Number) myGenericClass.getAttr1();.

    Notes:
    • The type specified when instantiating a class must be exactly the same as the type used in the declaration of the variable that references that class. The compiler will throw an error if the type is different (generics are invariant - a subtype is considered different):
      MyGenericClass<Number> myGenericClass;
      
      // Compiler error: Type mismatch: cannot convert from MyGenericClass<Integer> to MyGenericClass<Number>
      // This is because MyGenericClass<Integer> is NOT a subtype of MyGenericClass<Number>
      myGenericClass = new MyGenericClass<Integer>(Integer.valueOf(0));
      This restriction also applies to method return values and arguments:
      public class MainClass {
          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 MainClass is not applicable for the arguments (MyGenericClass<Integer>)
              foo(myGenericClass);
      
              // Compiler error: The method foo(MyGenericClass<Number>) in the type MainClass 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>());
          }
      }
      The general rule is: the compiler will always disallow assigning one reference to another if their generic types do not match exactly (invariance principle).
      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;
    • The previous rule applies only when classes are instantiated and variables or parameters are declared using an explicit generic type.

      If class instantiation and variable or parameter declarations do not use the generic type (raw types), the compiler will not perform any checks and will not generate an error. However, it will display a warning message:
      // 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 - assigning parameterized type to raw type is allowed
      myGenericClass = myGenericClass_Number;
    • The type specified when invoking the class's generic methods can be the same or a subtype of the type specified during the class instantiation (covariance for method arguments):
      MyGenericClass<Number> myGenericClass = new MyGenericClass<Number>();
      
      myGenericClass.setAttr1(Float.valueOf(10.0f)); // OK: Float is a subclass of Number
      
      myGenericClass.setAttr1(Integer.valueOf(10));  // OK: Integer is a subclass of Number
      The compiler will display an error if the type used when invoking the class's generic methods is not compatible with the type specified during instantiation:
      // 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 (?)
    The wildcard (the "?" character) is used to specify that the generic type can be a subtype or a supertype of a certain type.
    It can also be used to specify that the generic type can be any type!

    Note that the wildcard can only be used in variable declarations (including method parameters and return types):
    public class MainClass {
        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. Using the wildcard as a subtype of the generic type
      In the following example, we can assign to the variable myGenericClass any reference whose generic type is the same as or a subtype of the Number class.
      public class MainClass {
          public static void main(String[] args) {
              MyGenericClass<? extends Number> myGenericClass;
      
              myGenericClass = new MyGenericClass<Integer>(); // OK
      
              myGenericClass = new MyGenericClass<Float>(); // OK
          }
      }
      However, there are some restrictions when using this syntax:
      • You cannot use the reference created with this syntax to modify attributes whose type is generic (typically by directly accessing these attributes or by invoking methods that modify them).
        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));
      • You can only use the reference created with this syntax to read attributes whose type is generic (typically by directly accessing these attributes or invoking methods that return them), but you must add an explicit cast to do so:
        // Compiler error: Type mismatch: cannot convert from capture#3-of ? extends Number to Integer
        Integer intVar1 = myGenericClass.getAttr1();
        
        Integer intVar2 = (Integer) myGenericClass.getAttr1(); // OK
        Note that the compiler only checks that the cast type is a subtype of the generic type declared. This means that you may get a runtime error if the cast is not valid:
        package com.mtitek.generics;
        
        public class MainClass {
            public static void main(String[] args) {
                MyGenericClass<? extends Number> myGenericClass;
        
                myGenericClass = new MyGenericClass<Float>(Float.valueOf(10.0f)); // OK
        
                Integer intVar1 = (Integer) myGenericClass.getAttr1(); // Runtime error: ClassCastException
            }
        }
        Output:
        Exception in thread "main" java.lang.ClassCastException: java.lang.Float cannot be cast to java.lang.Integer
            at com.mtitek.generics.MainClass.main(MainClass.java:9)
    2. Using the wildcard as a supertype of the generic type
      You can assign to the variable any reference whose generic type is the same as or a supertype of the Integer class.
      public class MainClass {
          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>();
          }
      }
      You can use the reference created with this syntax to read and modify attributes whose type is generic (typically by directly accessing these attributes or invoking methods that return or modify them), but you must add an explicit cast to do so:
      public class MainClass {
          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: explicit cast required
      
              // Compiler error: Type mismatch: cannot convert from capture#4-of ? super Integer to Integer
              Integer intVar1 = myGenericClass.getAttr1();
      
              Integer intVar2 = (Integer) myGenericClass.getAttr1(); // OK: explicit cast required
          }
      }
      Note that the compiler only checks that the type used in the cast is a subtype of the generic type declared. This means you may get a runtime error if the cast is not valid:
      package com.mtitek.generics;
      
      public class MainClass {
          public static void main(String[] args) {
              MyGenericClass<? super Integer> myGenericClass;
      
              myGenericClass = new MyGenericClass<Integer>(); // OK
      
              Object objVar1 = Float.valueOf(10.0f);
      
              myGenericClass.setAttr1((Integer) objVar1); // Runtime error: ClassCastException
          }
      }
      Output:
      Exception in thread "main" java.lang.ClassCastException: java.lang.Float cannot be cast to java.lang.Integer
          at com.mtitek.generics.MainClass.main(MainClass.java:11)
    3. Using the wildcard to represent any type
      In the following example, we can assign to the variable myGenericClass any reference whose generic type can be any type.
      public class MainClass {
          public static void main(String[] args) {
              MyGenericClass<?> myGenericClass;
      
              myGenericClass = new MyGenericClass<Integer>(); // OK
      
              myGenericClass = new MyGenericClass<Object>(); // OK
          }
      }
      In fact, the syntax <?> is equivalent to <? extends Object>.
      This means it has the same restrictions (see above) as using the wildcard as a subtype of the generic type:
      • You cannot use the reference created with this syntax to modify attributes whose type is generic (typically by directly accessing these attributes or by invoking methods that modify them).
        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));
      • You can only use the reference created with this syntax to read attributes whose type is generic (typically by directly accessing these attributes or invoking methods that return them), but you must add an explicit cast to do so:
        // Compiler error: Type mismatch: cannot convert from capture#2-of ? to Integer
        Integer intVar1 = myGenericClass.getAttr1();
        
        Integer intVar2 = (Integer) myGenericClass.getAttr1();
        Note that the compiler only checks that the cast type is valid for the assignment. This means you may get a runtime error if the cast is not valid:
        package com.mtitek.generics;
        
        public class MainClass {
            public static void main(String[] args) {
                MyGenericClass<?> myGenericClass;
        
                myGenericClass = new MyGenericClass<Float>(Float.valueOf(10.0f)); // OK
        
                Integer intVar1 = (Integer) myGenericClass.getAttr1(); // Runtime error: ClassCastException
            }
        }
        Output:
        Exception in thread "main" java.lang.ClassCastException: java.lang.Float cannot be cast to java.lang.Integer
            at com.mtitek.generics.MainClass.main(MainClass.java:9)
  5. Generic Methods
    It is possible to define generic types at the method level.
    The syntax for defining and using the generic type is similar to what we have seen above.
    The only particularity is that the generic type definition must appear just before the method's return type.

    The generic type can be specified for the method's return type, its parameters, and its local variables.
    public class MainClass {
        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: MainClass.<Integer>
            Integer result1 = MainClass.<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) MainClass.<Integer>multiply(10, 5);
            System.out.println(result3);
        }
    }
  6. Generic Constructors
    Constructors can also define generic types; the syntax is the same as that used for methods.
    public class MainClass {
        Number field1;
    
        <R extends Number> MainClass(R arg1) {
            field1 = arg1;
        }
    
        public static void main(String[] args) {
            MainClass mainClass_Integer = new MainClass(Integer.valueOf(10));
            System.out.println(mainClass_Integer.field1);
    
            MainClass mainClass_Float = new MainClass(Float.valueOf(10.0f));
            System.out.println(mainClass_Float.field1);
        }
    }
  7. Throwing Generic Exceptions
    To throw generic exceptions, we use the same syntax to define generic types for methods.
    We use the throws keyword to throw the exception specified by the generic type.
    The definition of the generic type must specify that the type is a subtype of Throwable, Error, Exception, or any class that inherits from them.
    package com.mtitek.generics;
    
    import java.util.function.Supplier;
    
    public class MainClass {
        private static <T extends RuntimeException> void foo(Object arg1, Supplier<T> exceptionSupplier) throws T {
            if (arg1 == null || !(arg1 instanceof Number)) {
                throw exceptionSupplier.get();
            }
        }
    
        public static void main(String[] args) {
            try {
                foo(null, (() -> new IllegalArgumentException()));
            } catch (RuntimeException e) {
                e.printStackTrace();
            }
    
            try {
                foo("NOT A NUMBER", (() -> new NumberFormatException()));
            } catch (RuntimeException e) {
                e.printStackTrace();
            }
        }
    }
    Output:
    java.lang.IllegalArgumentException
        at com.mtitek.generics.MainClass.foo(MainClass.java:20)
        at com.mtitek.generics.MainClass.main(MainClass.java:6)
    java.lang.NumberFormatException
        at com.mtitek.generics.MainClass.foo(MainClass.java:22)
        at com.mtitek.generics.MainClass.main(MainClass.java:12)
© 2025  mtitek