JDK 5 introduced Java Generics with the aim of reducing bugs and adding an extra layer of abstraction over types.
What You Need
- About 9 minutes
- A favorite text editor or IDE
- Java 8 or later
4. Deep understanding of generics
4.1 Generics and the Collection API
Before Java 5, it is possible to add instances of different types into a collection because methods accept and return instances of type Object.
It is then necessary to make a cast which can fail at runtime if the type does not match.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class GenericsTest16 {
public static void main(String[] args) {
List elements = new ArrayList();
elements.add("1");
Iterator it = elements.iterator();
while (it.hasNext()) {
Object element = it.next();
System.out.println(element);
}
elements.add(2);
for (int i = 0; i < elements.size(); i++) {
Integer element = (Integer) elements.get(i);
System.out.println(element);
}
}
}
The output of above code snippet is below :
1
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
at GenericsTest16.main(GenericsTest16.java:22)
In Java 5, the Collection API has been revised to use generics : the types of the Collections API are generic, which allows to specify the type of objects that can be stored and retrieved from a collection.
The compiler will verify that only instances of the specified type will be added to the collection, thus reinforcing the reliability and robustness of the code.
Thanks to this check, it is no longer necessary to make a cast when retrieving a value from the collection.
import java.util.ArrayList;
import java.util.List;
public class GenericsTest4 {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
intList.add(1);
intList.forEach(System.out::println);
List<String> stringList = new ArrayList<>();
stringList.add("1");
stringList.forEach(System.out::println);
List<Long> longList = new ArrayList<>();
longList.add(1l);
longList.forEach(System.out::println);
List<Float> floatList = new ArrayList<>();
floatList.add(1f);
floatList.forEach(System.out::println);
List<Double> doubleList = new ArrayList<>();
doubleList.add(1d);
doubleList.forEach(System.out::println);
}
}
Below is the output of the above code snippet :
1
1
1
1.0
1.0
In order to maintain backward compatibility with code prior to Java 5, a raw type is allowed even after introducing generics.
A raw type is a generic class or interface without any specified type argument.
It is not recommended to use raw type in any other context.
Assigning an instance of a generic class to a variable with a raw type causes the compiler to issue a rawtype warning.
To display this kind of warnings, we can use the -Xlint compiler option.
import java.util.ArrayList;
import java.util.List;
public class GenericsTest17 {
public static void main(String[] args) {
List elements = new ArrayList();
}
}
Below is the output of using javac -Xlint GenericsTest17.java :
java.generics/GenericsTest17.java:6: warning: [rawtypes] found raw type: List
List elements = new ArrayList();
^
missing type arguments for generic class List<E>
where E is a type-variable:
E extends Object declared in interface List
java.generics/GenericsTest17.java:6: warning: [rawtypes] found raw type: ArrayList
List elements = new ArrayList();
^
missing type arguments for generic class ArrayList<E>
where E is a type-variable:
E extends Object declared in class ArrayList
2 warnings
4.2 Type Erasure
One of the challenges of evolving a language like Java is that it must support backwards compatibility.
When generics were added to the language, it was decided not to include them in the byte code produced by the compiler.
To ensure that the generated byte code is still compatible with that generated by previous versions of Java, the compiler applies a process called type erasure on generics during compilation.
Generics in Java are therefore a feature that can only be used at the compiler level.
The Java compiler performs the checks but implements type erasure which does not include any generic information in the byte code.
At compilation, all references to type variables are replaced.
The compiler is in charge of implementing type erasure by applying several rules :
- Unbounded type parameters are replaced by Object;
- Bounded type parameters are replaced by the type of their first bound;
- Casts are inserted when necessary;
- Bridge methods are generated as needed to preserve polymorphism.
When there are no restrictions on the type parameters in the class definition, they are directly replaced by Object during type erasure.
For example, type parameters in the form of <T> and <?> are replaced by Object.
public <T> List<T> myMethod(List<T> list) {
// ...
}
The byte code generated by the compiler is equivalent to :
public List<Object> myMethod(List<Object> list) {
// ...
}
But as the byte code does not contain any information relating to generics, the generated byte code is rather equivalent to :
public List myMethod(List list) {
// ...
}
When there are restrictions (upper and lower bounds) on the type parameters in the class definition, they are replaced by the upper or lower bounds of the type parameters during type erasure.
For example, type parameters in the form of <T extends Number> and <? extends Number> are replaced by Number.
public <T extends Number> void myMethod(T data) {
// ...
}
The byte code generated by the compiler is equivalent to :
public void myMethod(Number data) {
// ...
}
For example, type parameters in the form of <? super Number> is replaced by Object.
public void myMethod(<? super Number> data) {
// ...
}
The bytecode generated by the compiler is equivalent to :
public void myMethod(Object data) {
// ...
}
4.2.1 How to prove type erasure
In below code snippet, we have defined two ArrayList, one is of the ArrayList<String> generic type which can only store strings, the other is of the ArrayList<Integer> generic type which can only store integers.
Finally, we use getClass() method of list1 and list2 to obtain information about their classes, after comparing, it can be seen that the result is found to be true.
It shows that the generic types String and Integer have been erased, leaving only the original type.
import java.util.ArrayList;
import java.util.List;
public class GenericsTest18 {
public static void main(String[] args) {
List<Integer> l1 = new ArrayList<>();
List<String> l2 = new ArrayList<>();
System.out.println(l1.getClass() == l2.getClass());
}
}
Below is the output of above code snippet :
true
Let us have a look at another example.
In below code snippet, an ArrayList generic type is defined and instantiated as an Integer object.
If the add() method is called directly, only integer data can be stored.
However, when we use reflection to call the add() method, strings can be stored.
This shows that the Integer generic instance is erased after compilation, leaving only the original type.
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
public class GenericsTest19 {
public static void main(String[] args) throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException, NoSuchMethodException, SecurityException {
List<Integer> list = new ArrayList<>();
list.add(1);
list.getClass().getMethod("add", Object.class).invoke(list, "hello");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
The output of above code snippet is below :
1
hello
Another way to observe the type erasure is to look directly into the byte code.
After compiling below class, it is possible to use javap tool to check for type erasure in the byte code.
import java.util.ArrayList;
import java.util.List;
public class GenericsTest20 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
}
}
Below is the output of executing javap -c GenericsTest20.class :
Compiled from "GenericsTest20.java"
public class GenericsTest20 {
public GenericsTest20();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #16 // class java/util/ArrayList
3: dup
4: invokespecial #18 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: iconst_1
10: invokestatic #19 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
13: invokeinterface #25, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
18: pop
19: return
}
As we have seen in above byte code, at code line 13, it indicates that List.add method accept java.lang.Object but not only java.lang.Integer even if we declared an array list with generics type of integer.
4.2.2 Bridge Method
Type erasure will cause polymorphic conflicts, and JVM’s solution is the bridge method.
In below code snippet, StringContainer extends Container<String>, and it overrides two methods : setValue and getValue.
public class GenericsTest21 {
public static void main(String[] args) {
StringContainer sc = new StringContainer();
sc.setValue("hello");
sc.getValue();
}
}
class Container<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
class StringContainer extends Container<String> {
@Override
public String getValue() {
System.out.println("getValue : ");
String value = super.getValue();
System.out.println(value);
return value;
}
@Override
public void setValue(String value) {
System.out.println("setValue : ");
System.out.println(value);
super.setValue(value);
}
}
The output of above code snippet is below :
setValue :
hello
getValue :
hello
We have already known that with type erasure, the setValue and getValue methods of Container<String> will finally be associated with Object instead of String.
So why in StringContainer class, we can still override those two methods with association of String ? Shouldn’t it be Object ?
Let us have a look at the bytecode of StirngContainer (javap -c StringContainer.class):
Compiled from "GenericsTest21.java"
class StringContainer extends Container<java.lang.String> {
StringContainer();
Code:
0: aload_0
1: invokespecial #8 // Method Container."<init>":()V
4: return
public java.lang.String getValue();
Code:
0: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #22 // String getValue :
5: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aload_0
9: invokespecial #30 // Method Container.getValue:()Ljava/lang/Object;
12: checkcast #33 // class java/lang/String
15: astore_1
16: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
19: aload_1
20: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
23: aload_1
24: areturn
public void setValue(java.lang.String);
Code:
0: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #38 // String setValue :
5: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_1
12: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: aload_0
16: aload_1
17: invokespecial #40 // Method Container.setValue:(Ljava/lang/Object;)V
20: return
public void setValue(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #33 // class java/lang/String
5: invokevirtual #43 // Method setValue:(Ljava/lang/String;)V
8: return
public java.lang.Object getValue();
Code:
0: aload_0
1: invokevirtual #45 // Method getValue:()Ljava/lang/String;
4: areturn
}
We can remark that there are actually 2 setValue methods and 2 getValue methods.
The last two methods are the bridge methods generated by the compiler itself.
The parameter types of the bridge method are all Object. In other words, the two bridge methods that we cannot see in the subclass actually cover the two methods of the parent class.
The @Oveerride on our own defined setValue and getValue methods is just an illusion.
The internal implementation of the bridge method is just to call the two methods we have rewritten.
Therefore, the virtual machine cleverly uses the bridge method to solve the conflict between type erasure and polymorphism.