This is due to Java Type Erasure. To answer this question, I need to explain Unbounded Wildcards, Bounded Wildcards, and Type Erasure. Feel free to skip any part if you are familiar with it.
The content of this post was compiled from the java documentation.
1. Unlimited wildcards
An infinite type of substitution is specified using a wildcard character ( ? ), For example, List<?> . This is called an unknown type list. There are two scenarios in which an unlimited wildcard is a useful approach:
If you are writing a method that can be implemented using the functions provided in the Object class.
When the code uses methods in a general class that are independent of the type parameter. For example, List.size or List.clear . In fact, Class<?> used so often because most methods from Class<T> are independent of T
2. Limited wildcards
Consider a simple drawing application that can draw shapes such as rectangles and circles. To represent these forms within a program, you can define a class hierarchy, such as:
public abstract class Shape { public abstract void draw(Canvas c); } public class Circle extends Shape { private int x, y, radius; public void draw(Canvas c) { ... } } public class Rectangle extends Shape { private int x, y, width, height; public void draw(Canvas c) { ... } }
These classes can be drawn on canvas:
public class Canvas { public void draw(Shape s) { s.draw(this); } }
Any drawing usually contains several figures. Assuming they are presented as a list, it would be convenient to have a method in Canvas that draws them all:
public void drawAll(List<Shape> shapes) { for (Shape s: shapes) { s.draw(this); } }
Now type rules say that drawAll() can only be called in lists exactly Shape: it cannot, for example, be called on List<Circle> . This is unfortunate, since the whole method does, reads the figures from the list, so it can just as easily be called on the List<Circle> . We really want the method to take a list of any form:
public void drawAll(List<? extends Shape> shapes) { ... }
Here is a small but very important difference: we have replaced the type of List<Shape> with List<? extends Shape> List<? extends Shape> . Now drawAll() will accept lists of any subclass of Shape , so we can now call it if we want.
List<? extends Shape> List<? extends Shape> is an example of a limited template. ? denotes an unknown type, but in this case we know that this unknown type is actually a subtype of Shape. (Note: this may be the form itself or some subclass, it should not literally expand the form.) We say that Shape is the upper boundary of the wildcard.
Similarly, the syntax ? super T ? super T , which is a limited wildcard, indicates an unknown type, which is a supertype of T. For example, ArrayedHeap280 includes ArrayedHeap280<Integer> , ArrayedHeap280<Number> and ArrayedHeap280<Object> . As you can see in the java documentation for the Integer class , Integer is a subclass of Number, which, in turn, is a subclass of Object.
Class Integer * java.lang.Object * java.lang.Number * java.lang.Integer
3. Type Erasure and ClassCastException
Generalizations have been introduced into the Java language to provide more stringent type checks at compile time and to support general programming. To implement generics, the Java compiler applies style erasure to:
- Replace all type parameters in generic types with your borders or object if the type parameters are not limited. Thus, the obtained bytecode contains only ordinary classes, interfaces, and methods.
- Insert the type if necessary to maintain type safety.
- Creating bridge methods for preserving polymorphism in extended generic types.
In the process of erasing a type, the Java compiler erases all the type parameters and replaces each of its first bindings if the type parameter is limited, or Object if the type parameter is unlimited.
Consider the following general class that represents a node in a separately linked list:
public class Node<T> { private T data; private Node<T> next; public Node(T data, Node<T> next) } this.data = data; this.next = next; } public T getData() { return data; }
In the following example, a generic node class uses a parameter of a restricted type:
public class Node<T extends Comparable<T>> { private T data; private Node<T> next; public Node(T data, Node<T> next) { this.data = data; this.next = next; } public T getData() { return data; }
The Java compiler replaces the parameter of a restricted type T with the first associated class, Comparable:
public class Node { private Comparable data; private Node next; public Node(Comparable data, Node next) { this.data = data; this.next = next; } public Comparable getData() { return data; }
After erasing the styles, the Node and MyNode become:
public class Node { public Object data; public Node(Object data) { this.data = data; } public void setData(Object data) { System.out.println("Node.setData"); this.data = data; } } public class MyNode extends Node { public MyNode(Integer data) { super(data); } public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } }
Consider the following code:
MyNode mn = new MyNode(5); Node n = mn;
After erasing the type, this code will look like this:
MyNode mn = new MyNode(5); Node n = (MyNode)mn;
Here's what happens when the code runs:
n.setData("Hello"); calls the setData(Object) method on an object of class MyNode . (The MyNode class inherited setData(Object) from Node .)- In the body of
setData(Object) ,
Field
data object referenced by n is assigned a String .
- The
data field of the same object referenced via mn may be accessible and expected to be an Integer (since mn is MyNode , which is a Node<Integer> . ClassCastException String to Integer raises a ClassCastException from the cast inserted into the assignment by the compiler Java