Home > Software engineering >  Experimenting with java generics
Experimenting with java generics

Time:01-08

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)

Ideone.com demo

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.

  • Related