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
Class
carrying 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
Number
type fromList<? extends Number>
because its elements are of typeNumber
or its subclasses. - You cannot retrieve
Integer
type fromList<? extends Number>
because its stored elements could beDouble
. - You cannot retrieve
Double
type fromList<? extends Number>
because its stored elements could beInteger
.
Writing#
- You cannot add
Number
toList<? extends Number>
because its stored elements could beInteger
orDouble
. - You cannot add
Integer
toList<? extends Number>
because its stored elements could beDouble
. - You cannot add
Number
toList<? 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
Integer
type data from aList<? super Integer>
type object because it might be of typeList<Number>
. - We cannot guarantee that we can read
Number
type 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
Object
from aList<? super Integer>
type object.
Writing#
For the List<? super Integer>
array object in the above example:
- We can add
Integer
objects toarray
, and we can also add objects of subclasses ofInteger
toarray
, becauseInteger
and 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 Integer
limits the generic parameter. Letl1
's generic parameter beT
, thenT
isInteger
or a superclass ofInteger
, so objects ofInteger
or subclasses ofInteger
can be added tol1
. - Incorrect understanding:
? super Integer
limits the type of elements inserted, so any object that isInteger
or a superclass ofInteger
can be inserted intol1
.
For List<? extends Integer> l2
:#
- Correct understanding:
? extends Integer
limits the generic parameter. Letl2
's generic parameter beT
, thenT
isInteger
or a subclass ofInteger
, thus we cannot find a classX
such thatX
is a subclass of the generic parameterT
, so we cannot add elements tol2
. However, since we know that the generic parameterT
isInteger
or a subclass ofInteger
, we can read elements froml2
(the retrieved element type isInteger
or a subclass ofInteger
) and store them inInteger
. - Incorrect understanding:
? extends Integer
limits the type of elements inserted, so any object that isInteger
or a subclass ofInteger
can be inserted intol2
.
Application: PECS#
PECS Principle: Producer Extends, Consumer Super
- Producer extends: If we need a
List
to provide data of typeT
(i.e., we want to read data of typeT
from 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
List
to consume data of typeT
(i.e., we want to write data of typeT
into theList
), then we need to use? super T
, for example,List<? super Integer>
. However, thisList
cannot 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));
}
}
}