I was playing little bit with java generics, I came across this piece of code, which I am confused why it is happening so.
I am passing my second parameter K
as Integer
and inside generic method I was casting float
to my K
type, and in main()
I am receiving it as Integer
,
In my Code inspector I was seeing the Float
number completely sitting in my list (not chopped after casting to Integer
) which is of Integer
type, but when I try to pick element to save it in Integer
variable it gives ClassCastException
.
Can someone explain what is going wrong with generics so it is not saving us from casting exception.
Note : I reach to this scenario when I removed my second parameter K
from signature so there will be nothing defining type of K
, in that case I think Java make it Object
and then possibly we get cast exception but why in this case when I am passing K
type as well.
import java.util.ArrayList;
import java.util.List;
public class IntegerPrinter {
Integer item;
public void print() {
System.out.println(item);
}
public <T,K> List<K> anyPrint(List<T> num,K lo) {
List<K> mylist = new ArrayList<>();
mylist.add( (K) new Float(2.99f));
return mylist;
}
public IntegerPrinter(Integer item) {
this.item = item;
}
}
import java.util.ArrayList;
import java.util.List;
public class GenericsInAction {
public static void main(String[] args) {
IntegerPrinter oldPrinter = new IntegerPrinter(188);
oldPrinter.print();
List<Integer> dates = oldPrinter.anyPrint(new ArrayList<Integer>(),7);
Integer x = dates.get(0);
}
}
CodePudding user response:
I condensed the code down to the essential parts and modified it slightly to highlight the behaviour that is important:
class Ideone {
public static void main(String[] args) {
List<Integer> dates = new IntegerPrinter().anyPrint(7);
System.out.println(dates.get(0)); // succeeds
Integer x = dates.get(0); // Line 8, throws
}
}
class IntegerPrinter {
public <K> List<K> anyPrint(K lo) {
List<K> mylist = new ArrayList<>();
mylist.add((K) Float.valueOf(2.99f));
return mylist;
}
}
When executed, this program will result in the following output:
2.99
Exception in thread "main" java.lang.ClassCastException: class java.lang.Float cannot be cast to class java.lang.Integer (java.lang.Float and java.lang.Integer are in module java.base of loader 'bootstrap')
at Ideone.main(Main.java:8)
Now, let us step through the code and try to understand what is going on.
This line:
mylist.add((K) new Float(2.99f));
basically tells the compiler "do not care for the type, we (as programmers) guarantee that it is a K
, tread it as a K
".
Then, if we dig deeper, we see that ArrayList
uses an Object[]
as backing data structure. So there is on problem here, the backing Object[] elementData
can store everything.
Things get weird when we start retrieving elements. The JLS is somewhat vague about the type assertions in those cases (I think they are covered under §5.1.5 and §5.1.6.3, but I am not entirely sure). It basically says "the compiler has to assert the types, but only when necessary".
So if we retrieve an element from our List<Integer>
, that clearly is not an Integer
, but is passed along to a method that can deal with Object
, no type-assertion is necessary. This is exactly the case here:
System.out.println(dates.get(0));
The closest signature matching in System.out
is the println(Object)
method. This is the situation in JLS, §5.1.5: a widening conversion, it will never throw.
On the other hand, if we now try to retrieve an Integer
and try to store it in an Integer
:
Integer x = dates.get(0);
Now, a type check is in place. And indeed, if we check the output of the program, we see that the System.out.println(...)
took place, but the assignment to an int
-variable was the statement that triggered the ClassCastException
. This is the situation described in JLS, §5.1.6.3: a narrowing conversion at run time (that comes from ArrayList
's elementData(int)
method).
Footnote
Generics are most certainly one of the most, if not the most, complex and confusing parts in the JLS. I made a best-effort attempt to cite the JLS on its relevant parts, this might be miss-cited. I also know that this question was asked before, but I am unable to find the duplicate. If:
- a citation of the JLS is wrong, and another part should be cited instead, please ping me via comments or edit the post
- you find the (a) duplicate, please ping me, and I will close the question as duplicate (and delete my answer, if possible)
CodePudding user response:
Since ArrayList
is a generic type whose type erasure is java.lang.Object
, that is what is stored in the list. You can think of the type erasure as being the run-time type, the "real" type, as opposed to the compile-time type that the compiler knows about. Any type can be stored in the ArrayList
when the program runs.
It just so happens the the type erasure of K in anyPrint
is also java.lang.Object
, because you have no bounds on the type K. The method is compiled once for all usages, and it must be able to accept any type for K. So when the code for anyPrint
is compiled, the cast to K in the line mylist.add( (K) new Float(2.99f));
is ignored, since the type erasure of K is java.lang.Object
. Casting to java.lang.Object
is useless and pointless. It compiles as mylist.add(new Float(2.99f));
and the code inserts an object of type java.lang.Float
into a list of type java.lang.Object
.
Also, a cast in Java on an object type simply ensures the object has the correct type, it does not change the values of the object, like a cast on a primitive type. So there is no reason for you to believe the value 2.99f could change.
GenericsInAction
is compiled separately.
The parametrized type of K is java.lang.Integer
in the main
method of GenericsInAction
, since you pass in a 7
which is converted to java.lang.Integer
via auto-boxing, to be compatible with the type erasure of java.lang.Object
in anyPrint
. So, when that main
method is compiled, the compiler inserts a run-time check, a checkcast, right after the call to dates.get
, a check that ensures that the call to dates.get(0);
dates returns an object of type java.lang.Integer
, since the type of K must be java.lang.Integer
inside main
.
Since you inserted a java.lang.Float
into the list, that run-time check fails and throws ClassCastException
.