Java generics: why is this output possible?

2.9k views Asked by At

I have this class:

class MyClass<N extends Number> {
    N n = (N) (new Integer(8));
}

And I want to get these outputs:

System.out.println(new MyClass<Long>().n);
System.out.println(new MyClass<Long>().n.getClass());
  1. Output of first System.out.println() statement:

    8
    
  2. Output of second System.out.println() statement:

    java.lang.ClassCastException: java.lang.Integer (in module: java.base)
        cannot be cast to java.lang.Long (in module: java.base)
    

Why do I get the first output? Isn't there a cast as well? Why do I get the exception in the second output?

PS: I use Java 9; I tried it with the JShell and I got an exception on both outputs. Then I tried it with IntelliJ IDE and got the first output but the exception at the second.

4

There are 4 answers

12
Calculator On BEST ANSWER

The behavior that IntelliJ shows is clear to me:

You have an unchecked cast in MyClass. This means new Integer(8) is not immediately cast to Long but to the erasure Number (which works), when this line is executed: N n =(N)(new Integer(8));

Now let's look at the output statements:

System.out.println(new MyClass<Long>().n);

boils down to String.valueOf(new MyClass<Long>().n) -> ((Object)new MyClass<Long>().n).toString() which works fine, because n is accessed through Object and also the toString() method is accessed through static type Object -> no cast to Long occurs. new MyClass<Long>().n.toString() would fail with an exception, because toString() is tried to be accessed via static type Long. Therefore a cast of n to type Longoccurs which is not possible(Integer can't be cast to Long).

The same thing occurs when executing the 2nd statement:

System.out.println(new MyClass<Long>().n.getClass()); 

The getClass method (declared in Object) of type Long is tried to be accessed through static type Long. Therefore a cast of n to type Long occurs which yields a cast exception.

JShell behavior:

I tried to reproduce the resulting exception for the first output statement on JShell - Java 9 early access Build 151:

jshell> class MyClass<N extends Number> {
   ...>     N n = (N) (new Integer(8));
   ...> }
|  Warning:
|  unchecked cast
|    required: N
|    found:    java.lang.Integer
|      N n = (N) (new Integer(8));
|                ^--------------^
|  created class MyClass

jshell> System.out.println(new MyClass<Long>().n);
8

jshell> System.out.println(new MyClass<Long>().n.getClass());
|  java.lang.ClassCastException thrown: java.base/java.lang.Integer cannot be cast to java.base/java.lang.Long
|        at (#4:1)

But it seems that JShell gives the exact same results as IntelliJ. System.out.println(new MyClass<Long>().n); outputs 8 - no exception.

2
Raphaël On

This happen because of Java erasure.

Since Integer extends Number, the compiler accepts the cast to N. At runtime, since N is replaced by Number (due to the erasure), there is no problem to store an Integer inside n.

The argument of method System.out.println is of type Object so there is no problem to print the value of n.

However, when calling a method on n, a type check is added by the compiler to ensure the right method will be called. Hence resulting in a ClassCastException.

2
Aman Goyal On

This is happening because you have already defined n as object of integer so it won't cast it to long

either use Integer in the MyClass in the sysout like

System.out.println(new MyClass<Integer>().n);

or define n as: N n =(N)(new Long(8));.

1
newacct On

Both exception and no exception are permissible behaviors. Basically, this goes down to how the compiler erases the statements, whether it is to something like this without casts:

System.out.println(new MyClass().n);
System.out.println(new MyClass().n.getClass());

or to something like this with casts:

System.out.println((Long)new MyClass().n);
System.out.println(((Long)new MyClass().n).getClass());

or one for one statement and one for the other. Both versions are valid Java code that will compile. The question is whether it is permissible for the compiler to compile to one version, or the other, or both.

It is permissible to insert a cast here because that is usually what happens when you take something from a generic context where the type is a type variable, and return it into a context where the type variable takes on a specific type. For example, you could assign new MyClass<Long>().n into a variable of type Long without any casts, or pass new MyClass<Long>().n into a place that expects Long without any casts, both cases of which obviously will require the compiler to insert a cast. The compiler can just decide to always insert a cast when you have new MyClass<Long>().n, and it's not wrong to do so, as the expression is supposed to have type Long.

On the other hand, it is also permissible to not have a cast in these two statements, because in both cases the expression is used in a context where any Object can be used, so no cast is needed to make it compile and be type safe. Furthermore, in both statements, a cast or no cast would make no difference in behavior if the value was indeed a Long. In the first statement, it is passed to the version of .println() that takes Object, and there is no more specific overload of println that takes Long or Number or anything like that, so the same overload will be chosen regardless of whether the argument is considered as Long or Object. For the second statement, .getClass() is provided by Object, so it is available regardless of whether the thing on the left is a Long or Object. Since the erased code is valid both with and without the cast and the behavior would be the same with and without the cast (assuming the thing is indeed a Long), the compiler could choose to optimize the cast out.

A compiler could even have a cast in one case and not in the other, perhaps because it only optimizes out the cast in certain simple cases, but doesn't bother to perform analysis in more complicated cases. We don't need to dwell on why a particular compiler decided to compile to one form or another for a particular statement, because both are permissible, and you should not rely on it to work one way or the other.