Java Generics: Type Parameters, Wildcards, Erasure
Put the string "42" into a list without generics, then pull it out as a number: Integer n = (Integer) list.get(0);. The compiler says nothing, and the program crashes at runtime with a ClassCastException. Generics move this error to the cheapest place to find it: compile time.
Generics in Java are a mechanism for parameterizing types: a class, interface or method is declared with a type parameter (usually T), and a concrete type is substituted when you use it, for example List<String> or Matrix<Integer>. The same code works with different data types, and the compiler checks the types and inserts the casts for you. Generics were introduced in Java 5.
Classes, interfaces and methods that have type parameters are called generic or parameterized: a generic class, a generic method. A type with a type argument substituted, such as Matrix<Integer>, is called a parameterized type.
Why use generics
Generics provide type safety. Without them, a collection stores Object, you cast elements by hand, and a bad cast shows up only at runtime. With generics the compiler knows the element type, rejects unsuitable values and removes the need for explicit casts.
import java.util.ArrayList;
import java.util.List;
public class WhyGenerics {
public static void main(String[] args) {
// Without generics: a raw type
List rawList = new ArrayList();
rawList.add("42");
Integer n = (Integer) rawList.get(0); // compiles, but fails at runtime
// With generics
List<Integer> numbers = new ArrayList<>();
// numbers.add("42"); // compile error: incompatible types: String cannot be converted to Integer
numbers.add(42);
Integer m = numbers.get(0); // no cast needed
}
}
// Output:
// Exception in thread "main" java.lang.ClassCastException:
// class java.lang.String cannot be cast to class java.lang.Integer ... A type without angle-bracket arguments (List instead of List<Integer>) is called a raw type. It exists only for compatibility with code written before Java 5. Do not use it in new code.
What does T mean in Java: the type parameter
T in Java is the name of a type parameter, a placeholder that is replaced by a concrete reference type when the class or method is used. In the declaration class Matrix<T> the letter T is the type parameter, and in Matrix<Integer> the type Integer is the type argument.
A type parameter is declared in angle brackets after the class name, or before the return type of a method. Inside the class, T is used like any other type: for fields, parameters, return values and local variables. The name can be any identifier, but by convention a single uppercase letter is used:
| Name | Stands for | Typical use | Example from the JDK |
|---|---|---|---|
T | Type — any type | Generic classes and methods | Optional<T>, Comparable<T> |
E | Element | Collections | List<E>, Set<E> |
K, V | Key, Value | Maps | Map<K, V> |
N | Number | Numeric parameters | <N extends Number> |
R | Result | Functional interfaces | Function<T, R> |
S, U | Second and third type | Several type parameters | BiFunction<T, U, R> |
A type argument must be a reference type. Use wrapper classes instead of primitives: Integer instead of int, Double instead of double. Values are converted automatically thanks to autoboxing. Since Java 7 you do not have to repeat the type arguments on the right-hand side of a declaration. The diamond operator <> is enough: Matrix<Integer> m = new Matrix<>(...).
Generic classes (parameterized classes)
A generic (parameterized) class is a class declared with one or more type parameters in angle brackets: class Matrix<T>. Its code is written once, and a concrete type is specified when you create an object: Matrix<Double>, Matrix<Integer>.
Below is a basic Java generic class example: a class that stores the elements of a matrix in an array.
public class Matrix<T> {
private final T[] array;
public Matrix(T[] array) {
this.array = array.clone();
}
public T get(int index) {
return array[index];
}
public int size() {
return array.length;
}
public static void main(String[] args) {
Matrix<Double> doubleMatrix = new Matrix<>(new Double[]{1.5, 2.5});
Matrix<Integer> integerMatrix = new Matrix<>(new Integer[]{1, 2, 3, 4});
Integer first = integerMatrix.get(0); // no cast needed
System.out.println(first + " " + doubleMatrix.size());
}
}
// Output: 1 2 In the declaration Matrix<Integer> integerMatrix, the type Integer is the type argument. Java does not create a separate version of the class for each argument. At runtime there is only one Matrix class (the reason is explained in the Type erasure section).
Generics work only with objects, so a primitive type as an argument will not compile:
Matrix<int> intMatrix = new Matrix<>(new int[3]);
// Compile error: unexpected type; required: reference, found: int Parameterized types with different arguments are incompatible with each other. doubleMatrix and integerMatrix are both instances of Matrix, but to the compiler they are different types:
doubleMatrix = integerMatrix;
// Compile error: incompatible types: Matrix<Integer> cannot be converted to Matrix<Double> A generic class can have any number of type parameters, separated by commas:
public class TwoGen<T, V> {
private final T obT;
private final V obV;
public TwoGen(T obT, V obV) {
this.obT = obT;
this.obV = obV;
}
public void showTypes() {
System.out.println("Type of T: " + obT.getClass().getName());
System.out.println("Type of V: " + obV.getClass().getName());
}
public T getObT() {
return obT;
}
public V getObV() {
return obV;
}
}
public class SimpleGen {
public static void main(String[] args) {
TwoGen<Integer, String> twoGen = new TwoGen<>(88, "Generics");
twoGen.showTypes();
System.out.println("Value of T: " + twoGen.getObT());
System.out.println("Value of V: " + twoGen.getObV());
}
}
// Output:
// Type of T: java.lang.Integer
// Type of V: java.lang.String
// Value of T: 88
// Value of V: Generics Bounded type parameters: <T extends Number>
A bounded type parameter has an upper bound: the declaration <T extends Number> allows only Number and its subclasses to be substituted for T. In return, inside the class you can call the methods of the bound, such as doubleValue(), which a plain Object does not have.
The general form of the declaration is a type parameter, the extends keyword and a superclass as the bound:
class Gen<T extends Superclass> Example: a class that calculates the average of an array of any numbers.
public class Average<T extends Number> {
private final T[] array;
public Average(T[] array) {
this.array = array;
}
public double average() {
double sum = 0.0;
for (T value : array) {
sum += value.doubleValue(); // Number's method is available thanks to the bound
}
return sum / array.length;
}
}
public class AverageExample {
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
Average<Integer> integerAverage = new Average<>(intArray);
System.out.println("Average for Integer: " + integerAverage.average());
Double[] doubleArray = {1.1, 2.2, 3.3, 4.4, 5.5};
Average<Double> doubleAverage = new Average<>(doubleArray);
System.out.println("Average for Double: " + doubleAverage.average());
// Will not compile because String is not a subclass of Number:
// String[] strArray = {"1", "2", "3"};
// Average<String> strAverage = new Average<>(strArray);
// error: type argument String is not within bounds of type-variable T
}
}
// Output:
// Average for Integer: 3.0
// Average for Double: 3.3 The bound can be an interface as well as a class. For an interface you also write extends, not implements:
public class MyClass<T extends Serializable> A bound can include a class and one or more interfaces joined with the & sign. The class must come first:
class Gen<T extends MyClass & MyInterface1 & MyInterface2> Here the type argument must be a subclass of MyClass that implements both interfaces, MyInterface1 and MyInterface2.
Wildcard arguments in Java (?)
A wildcard argument is the ? sign in place of a type argument. It stands for an unknown type. You need it when a method must accept a parameterized type with any argument: Average<?> accepts both Average<Integer> and Average<Double>.
Suppose you want to add a sameAvg() method to the Average class from the Bounded type parameters section. It compares the averages of two arrays, and the array types may differ. The first idea is to use the parameter T:
boolean sameAvg(Average<T> ob) {
return average() == ob.average();
} This method accepts only an object with the same type argument: an Average<Integer> can be compared with an Average<Integer>, but a call with an Average<Double> will not compile. The solution is a wildcard argument:
boolean sameAvg(Average<?> ob) {
return average() == ob.average();
} The wildcard does not affect which Average objects you can create. That is still decided by the extends Number bound in the class declaration. It simply matches any valid Average object:
public class AverageExample2 {
public static void main(String[] args) {
Average<Integer> iob = new Average<>(new Integer[]{1, 2, 3, 4, 5});
Average<Double> dob = new Average<>(new Double[]{1.1, 2.2, 3.3, 4.4, 5.5});
Average<Float> fob = new Average<>(new Float[]{1.0F, 2.0F, 3.0F, 4.0F, 5.0F});
System.out.println("iob and dob: " + (iob.sameAvg(dob) ? "same" : "different"));
System.out.println("iob and fob: " + (iob.sameAvg(fob) ? "same" : "different"));
}
}
// Output:
// iob and dob: different
// iob and fob: same A wildcard can have an upper or a lower bound. This matters most when a method works with a class hierarchy: List<Integer> is not a subtype of List<Number>, even though Integer is a subclass of Number, but List<? extends Number> accepts both.
| Notation | What it accepts | Reading elements | Adding elements |
|---|---|---|---|
List<?> | A list with any type argument | As Object | Not allowed (except null) |
List<? extends Number> | List<Number>, List<Integer>, List<Double>... | As Number | Not allowed (except null) |
List<? super Integer> | List<Integer>, List<Number>, List<Object> | Only as Object | You can add Integer |
import java.util.ArrayList;
import java.util.List;
public class WildcardExample {
// ? extends Number: you can read elements of the list as Number
static double sum(List<? extends Number> list) {
double sum = 0;
for (Number n : list) {
sum += n.doubleValue();
}
// list.add(1); // compile error: the exact element type is unknown
return sum;
}
// ? super Integer: you can add Integer to the list
static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 3; i++) {
list.add(i);
}
}
public static void main(String[] args) {
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5);
System.out.println(sum(ints));
System.out.println(sum(doubles));
List<Number> numbers = new ArrayList<>();
addNumbers(numbers);
System.out.println(numbers);
// List<Number> wrong = ints;
// compile error: incompatible types: List<Integer> cannot be converted to List<Number>
}
}
// Output:
// 6.0
// 4.0
// [1, 2, 3] Generic methods and constructors
A generic (parameterized) method is a method that declares its own type parameters in angle brackets before the return type: public static <T> boolean isIn(T x, T[] array). You can declare such a method in an ordinary, non-generic class. The compiler usually infers the type argument from the arguments of the call.
The methods of a generic class that use the class parameter T also work with a type parameter, but they do not declare their own. A generic method is independent of its class:
public class GenMethodExample {
/**
* Checks whether the object x is an element of the array.
*
* @param x the object to look for
* @param array the array to search in
* @param <T> the type of the searched object and of the array elements
* @return true if x equals one of the elements
*/
public static <T> boolean isIn(T x, T[] array) {
for (T element : array) {
if (x.equals(element)) {
return true;
}
}
return false;
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
if (isIn(2, intArray)) {
System.out.println("2 is in intArray");
}
if (!isIn(7, intArray)) {
System.out.println("7 is not in intArray");
}
String[] strArray = {"one", "two", "three", "four", "five"};
if (isIn("two", strArray)) {
System.out.println("two is in strArray");
}
if (!isIn("seven", strArray)) {
System.out.println("seven is not in strArray");
}
}
}
// Output:
// 2 is in intArray
// 7 is not in intArray
// two is in strArray
// seven is not in strArray Easy to get wrong
The call isIn("2", intArray) compiles and returns false: the compiler infers T as a common supertype of String and Integer (an intersection of Serializable & Comparable<...>). To catch this mistake at compile time, pass the type argument explicitly: GenMethodExample.<Integer>isIn("2", intArray). Now the compiler reports incompatible types: String cannot be converted to Integer.
Constructors can be generic too, even if their class is not:
public class GenConstructor {
private final double value;
public <T extends Number> GenConstructor(T arg) {
value = arg.doubleValue();
}
public void showValue() {
System.out.println("value: " + value);
}
}
public class GenConstructorExample {
public static void main(String[] args) {
GenConstructor genConstructor1 = new GenConstructor(100);
GenConstructor genConstructor2 = new GenConstructor(123.5F);
genConstructor1.showValue();
genConstructor2.showValue();
}
}
// Output:
// value: 100.0
// value: 123.5 Generic interfaces
A generic (parameterized) interface is declared the same way as a generic class: interface MyInterface<T>. In the JDK, Comparable<T>, Comparator<T> and List<E> are built like this. A class that implements such an interface either becomes generic itself and passes its parameter to the interface, or substitutes a concrete type.
public interface MyInterface<T> {
T someMethod(T t);
}
// 1. A generic class passes its type parameter T to the interface
public class Echo<T> implements MyInterface<T> {
@Override
public T someMethod(T t) {
return t;
}
}
// 2. An ordinary class fixes the type argument
public class Upper implements MyInterface<String> {
@Override
public String someMethod(String s) {
return s.toUpperCase();
}
}
public class InterfaceExample {
public static void main(String[] args) {
MyInterface<String> echo = new Echo<>();
MyInterface<String> upper = new Upper();
System.out.println(echo.someMethod("some string"));
System.out.println(upper.someMethod("some string"));
}
}
// Output:
// some string
// SOME STRING If a class implements a generic interface but is declared without a parameter and does not specify an argument (class Echo implements MyInterface<T>), the compiler reports cannot find symbol: class T.
Generic class hierarchies
Generic classes take part in inheritance just like ordinary ones: a generic class can be a superclass or a subclass. The main rule is that a subclass must pass up the hierarchy every type argument the generic superclass requires, either with its own parameter or with a concrete type.
public class GenericSuper<T> {
private final T ob;
public GenericSuper(T ob) {
this.ob = ob;
}
public T getOb() {
return ob;
}
}
// 1. The subclass passes its own parameter T to the superclass
public class GenericSub<T> extends GenericSuper<T> {
public GenericSub(T ob) {
super(ob);
}
}
// 2. A non-generic subclass fixes the type argument
public class IntegerHolder extends GenericSuper<Integer> {
public IntegerHolder(int value) {
super(value);
}
}
// 3. The subclass adds its own type parameter
public class LabeledHolder<T, V> extends GenericSuper<T> {
private final V label;
public LabeledHolder(T ob, V label) {
super(ob);
this.label = label;
}
public V getLabel() {
return label;
}
}
public class HierarchyExample {
public static void main(String[] args) {
LabeledHolder<Double, String> price = new LabeledHolder<>(9.99, "price");
System.out.println(price.getLabel() + ": " + price.getOb());
int value = new IntegerHolder(5).getOb(); // no cast needed
System.out.println(value);
}
}
// Output:
// price: 9.99
// 5 A subclass of a generic superclass does not have to be generic (option 2), and it can declare its own type parameters when needed (option 3). The reverse is also allowed: the superclass of a generic class can be an ordinary non-generic class.
The instanceof operator and generics
The instanceof operator checks the class of an object at runtime, and by then the type arguments have been erased. So a check such as instanceof GenericSub<?> always works, while a check with a concrete argument, such as instanceof GenericSub<Integer>, is allowed only when the compiler can guarantee it is correct from the static type of the variable.
public class InstanceofExample {
public static void main(String[] args) {
GenericSuper<Integer> object = new GenericSub<>(88);
System.out.println(object instanceof GenericSuper<?>);
System.out.println(object instanceof GenericSub<?>);
// Java 16+: compiles, the cast GenericSuper<Integer> -> GenericSub<Integer> is checked
System.out.println(object instanceof GenericSub<Integer>);
Object any = object;
// Compile error in every Java version:
// System.out.println(any instanceof GenericSub<Integer>);
// Java 16+: Object cannot be safely cast to GenericSub<Integer>
// before Java 16: illegal generic type for instanceof
}
}
// Output:
// true
// true
// true What changed in Java 16
Before Java 16, any instanceof check with a concrete type argument was a compile error, and older tutorials and tests still say so. With pattern matching for instanceof (JEP 394) the rule was relaxed: the check is allowed if the cast to the target type is not unchecked. It works nicely with a pattern: if (object instanceof GenericSub<Integer> sub) { int x = sub.getOb(); }.
Type erasure in Java
Type erasure is the way generics are implemented in Java: the compiler checks the type arguments and then removes them from the bytecode. The parameter T is replaced by its bound (Object if there is no bound, or Number for <T extends Number>), and casts are inserted where the type is used. At runtime the JVM does not know what T was.
Here is a generic class before compilation and (roughly) after erasure:
// Source code
public class Box<T extends Number> {
private T value;
public T get() { return value; }
}
Box<Integer> box = new Box<>();
Integer i = box.get();
// What remains after erasure
public class Box {
private Number value;
public Number get() { return value; }
}
Box box = new Box();
Integer i = (Integer) box.get(); // the cast was inserted by the compiler That is why parameterized types with different arguments cannot be told apart at runtime. They are the same class:
List<String> strings = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
System.out.println(strings.getClass() == numbers.getClass());
System.out.println(strings.getClass().getName());
// Output:
// true
// java.util.ArrayList Erasure also explains why you cannot write new T(): in the bytecode this would be new Object(), and the JVM would not know which class to instantiate. If you need the type at runtime, pass it explicitly as a Class<T> object (this technique is called a type token):
public class Factory<T> {
private final Class<T> type;
public Factory(Class<T> type) {
this.type = type;
}
public T create() throws ReflectiveOperationException {
return type.getDeclaredConstructor().newInstance(); // instead of the illegal new T()
}
public static void main(String[] args) throws ReflectiveOperationException {
Factory<StringBuilder> factory = new Factory<>(StringBuilder.class);
StringBuilder sb = factory.create();
System.out.println(sb.append("created via Class<T>"));
}
}
// Output: created via Class<T> Where type information survives: getGenericSuperclass()
Type arguments are erased from objects, but not from the declarations of classes, fields and methods. If a class extends a parameterized superclass with a concrete argument, that argument is recorded in the class file, and you can read it by reflection with the Class.getGenericSuperclass() method:
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
public class SuperclassTypeExample {
public static void main(String[] args) {
// IntegerHolder extends GenericSuper<Integer> (from the section on hierarchies)
Type type = IntegerHolder.class.getGenericSuperclass();
System.out.println(type);
Type argument = ((ParameterizedType) type).getActualTypeArguments()[0];
System.out.println(argument);
}
}
// Output:
// GenericSuper<java.lang.Integer>
// class java.lang.Integer The super type token technique is built on this: you create an anonymous subclass of an abstract generic class, for example new TypeReference<List<String>>() {}, and call getClass().getGenericSuperclass() in its constructor. This is how Jackson (TypeReference) and Spring (ParameterizedTypeReference) learn the full type List<String>, which cannot be passed through List.class.
Restrictions on generics
Almost all restrictions on generics in Java are a consequence of type erasure: anything that needs to know T at runtime (creating an object or an array, checking a type) is forbidden, and the class's type parameter does not apply to static members at all. A summary table with the exact javac messages:
| What you cannot do | Example | Compiler message | Workaround |
|---|---|---|---|
| Use a primitive as a type argument | Matrix<int> | unexpected type; required: reference, found: int | Use a wrapper class: Matrix<Integer> |
| Create an object from a type parameter | new T() | unexpected type; required: class, found: type parameter T | Pass a Class<T> or a Supplier<T> |
| Create an array from a type parameter | new T[10] | generic array creation | Receive a ready-made array in the constructor, or use List<T> |
| Create an array of a parameterized type | new GenericSub<Integer>[10] | generic array creation | GenericSub<?>[] or List<GenericSub<Integer>> |
Use the class's T in a static field or method | static T ob; | non-static type variable T cannot be referenced from a static context | A static method with its own parameter: static <V> void m(V v) |
| Overload that differs only by type argument | print(List<String>) and print(List<Integer>) | name clash: ... have the same erasure | Give the methods different names |
| Generic exception class | class MyEx<T> extends Exception | a generic class may not extend java.lang.Throwable | Store the data in an ordinary field of the exception |
The same restrictions in code:
public class GenRestriction<T> {
private T ob;
private T[] array;
// Wrong: a static field cannot have type T
// public static T staticOb;
public GenRestriction(T ob, T[] array) {
// Not allowed:
// this.ob = new T();
// this.array = new T[10];
this.ob = ob;
this.array = array; // the array is created by the calling code, where the type is known
}
// Wrong: a static method cannot use the class's T
// public static T getStaticOb() { return null; }
// But you can declare a static generic method with its own type parameter
public static <V> void print(V v) {
System.out.println(v);
}
public static void main(String[] args) {
// GenericSub<Integer>[] gens = new GenericSub<Integer>[10]; // generic array creation
GenericSub<?>[] gens = new GenericSub<?>[10];
gens[0] = new GenericSub<>(34);
print(gens[0].getOb());
}
}
// Output: 34 Frequently Asked Questions
How do Java generics differ from C++ templates and C# generics?
C++ templates generate separate code for each type argument, and in C# type arguments are kept at runtime (reified generics), so List<int> and new T() are allowed there. In Java there is one class for all arguments, and the arguments themselves are erased after the compiler checks them. See the Type erasure section for details.
Why can't you pass a List<Integer> to a method that takes a List<Number>?
Parameterized types are invariant. If List<Integer> were a subtype of List<Number>, the method could add a Double to it and break the list of integers. To let a method accept lists of any numbers, declare the parameter as List<? extends Number>. See the Wildcard arguments section.
What is the PECS rule?
PECS stands for Producer Extends, Consumer Super. If a parameter only supplies elements to the method (a producer), use ? extends T. If it only receives them (a consumer), use ? super T. The classic JDK example is Collections.copy(List<? super T> dest, List<? extends T> src).
What happens if you write var list = new ArrayList<>()?
The variable's type becomes ArrayList<Object>: with var there is no type on the left from which the diamond operator could infer the argument. You can put anything into such a list, and type checking is lost. Write var list = new ArrayList<String>() or declare the type explicitly.
Why does the compiler say "uses unchecked or unsafe operations"?
It is a warning about unchecked operations: the code uses a raw type (for example, List without angle brackets) or a cast to a parameterized type that cannot be verified at runtime. Compile with the -Xlint:unchecked flag to see the details. The fix is to specify type arguments. Use @SuppressWarnings("unchecked") only where you have proved safety by hand.
Comments