Type Erasure#
The Compiler Treats Type <T> as Object#
public class Pair<T> {
private final T first;
private final T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
Generics in Java are handled at compile time. The compiler applies type erasure to the code above, and the code seen by the JVM is as follows:
public class Pair {
private final Object first;
private final Object last;
public Pair(Object first, Object last) {
this.first = first;
this.last = last;
}
public Object getFirst() {
return first;
}
public Object getLast() {
return last;
}
}
The Compiler Implements Safe Casting Based on <T>#
Pair<String> p = new Pair<>("Hello", "world");
String first = p.getFirst();
String last = p.getLast();
The compiler safely casts based on the type of T:
Pair p = new Pair("Hello", "world");
String first = (String) p.getFirst();
String last = (String) p.getLast();
Limitations of Type Erasure#
<T>cannot be a primitive type because type erasure replaces it withObject.- It is impossible to obtain the
Classcarrying the generic type because generics are handled at compile time, so what you get isPair<Object>.
For example:
@Test
void testGetClass() {
Pair<String> p1 = new Pair<>("Hello", "World");
Pair<Integer> p2 = new Pair<>(114, 514);
Class c1 = p1.getClass();
Class c2 = p2.getClass();
assertEquals(c1, c2);
assertEquals(c1, Pair.class);
}
- Improperly overridden methods
class Pair<T> {
public boolean equals(T t) {}
}
This is because the defined equals(T t) method will actually be erased to equals(Object t), and this method is inherited from Object, so the compiler will prevent the definition of a generic method that would actually become an override.
Generic Inheritance#
A class can inherit a generic class. In the case of inheriting a generic type, the subclass can access the generic type of the parent class. For example, IntPair can access the generic type Integer of the parent class.
public class IntPair extends Pair<Integer> {
public IntPair(Integer first, Integer last) {
super(first, last);
}
}
class IntPairTest {
@Test
void getSuperParameterizedType() {
Class<IntPair> clazz = IntPair.class;
Type t = clazz.getGenericSuperclass();
if (t instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) t;
Type[] types = pt.getActualTypeArguments(); // may have many types
Type firstType = types[0];
Class<?> typeClass = (Class<?>) firstType;
assertEquals(typeClass, Integer.class);
}
}
}
extends Wildcard#
public class PairHelper {
static int add(Pair<Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
}

As shown, the add function accepts a parameter type of Pair<Number>, so passing Pair<Number> is fine; however, passing Pair<Integer> will result in a compilation error because although Integer is a subclass of Number, Pair<Integer> is not a subclass of Pair<Number>.
To solve this problem, you can use Pair<? extends Number>, which allows you to accept Pair types with generic types that are subclasses of Number:
static int add(Pair<? extends Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
@Test
void addNumberBased() {
assertEquals(579, add(new Pair<Integer>(123, 456)));
}
Reading#
- You can retrieve
Numbertype fromList<? extends Number>because its elements are of typeNumberor its subclasses. - You cannot retrieve
Integertype fromList<? extends Number>because its stored elements could beDouble. - You cannot retrieve
Doubletype fromList<? extends Number>because its stored elements could beInteger.
Writing#
- You cannot add
NumbertoList<? extends Number>because its stored elements could beIntegerorDouble. - You cannot add
IntegertoList<? extends Number>because its stored elements could beDouble. - You cannot add
NumbertoList<? extends Number>because its stored elements could beInteger.
public class ExtendsTest {
List<? extends Number> nums1 = new ArrayList<Number>();
List<? extends Number> nums2 = new ArrayList<Integer>();
List<? extends Number> nums3 = new ArrayList<Double>();
void get() {
Number x = nums1.get(0);
// Integer y = nums2.get(0); // compile error
// Double z = nums3.get(0); // compile error
}
void set() {
/*
nums1.add(new Number() {
@Override
public int intValue() {
return 0;
}
@Override
public long longValue() {
return 0;
}
@Override
public float floatValue() {
return 0;
}
@Override
public double doubleValue() {
return 0;
}
});
*/ // compile error
// nums2.add(new Integer(1)); // compile error
// nums3.add(new Double(1.2)); // compile error
}
}
super Wildcard#
<? super T> describes the lower bound of the wildcard, meaning the specific generic parameter must satisfy the condition: the generic parameter must be of type T or its superclass.
Reading#
- We cannot guarantee that we can read
Integertype data from aList<? super Integer>type object because it might be of typeList<Number>. - We cannot guarantee that we can read
Numbertype data from aList<? super Integer>type object because it might be of typeList<Object>. - The only guarantee is that we can retrieve an instance of
Objectfrom aList<? super Integer>type object.
Writing#
For the List<? super Integer> array object in the above example:
- We can add
Integerobjects toarray, and we can also add objects of subclasses ofIntegertoarray, becauseIntegerand its superclasses can accept subclasses ofInteger. - We cannot add objects that are not subclasses of
Integer, such asDouble/Number/Object, toarray.

extends vs super#
For List<? super Integer> l1:#
- Correct understanding:
? super Integerlimits the generic parameter. Letl1's generic parameter beT, thenTisIntegeror a superclass ofInteger, so objects ofIntegeror subclasses ofIntegercan be added tol1. - Incorrect understanding:
? super Integerlimits the type of elements inserted, so any object that isIntegeror a superclass ofIntegercan be inserted intol1.
For List<? extends Integer> l2:#
- Correct understanding:
? extends Integerlimits the generic parameter. Letl2's generic parameter beT, thenTisIntegeror a subclass ofInteger, thus we cannot find a classXsuch thatXis a subclass of the generic parameterT, so we cannot add elements tol2. However, since we know that the generic parameterTisIntegeror a subclass ofInteger, we can read elements froml2(the retrieved element type isIntegeror a subclass ofInteger) and store them inInteger. - Incorrect understanding:
? extends Integerlimits the type of elements inserted, so any object that isIntegeror a subclass ofIntegercan be inserted intol2.
Application: PECS#
PECS Principle: Producer Extends, Consumer Super
- Producer extends: If we need a
Listto provide data of typeT(i.e., we want to read data of typeTfrom theList), then we need to use? extends T, for example,List<? extends Integer>. However, we cannot add data to thisList. - Consumer Super: If we need a
Listto consume data of typeT(i.e., we want to write data of typeTinto theList), then we need to use? super T, for example,List<? super Integer>. However, thisListcannot guarantee the type of data read from it. - If we want to read and write, we must explicitly declare the type of the generic parameter, for example,
List<Integer>.
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i));
}
}
}