51

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.

Bakuriu
  • 98,325
  • 22
  • 197
  • 231
Socke
  • 533
  • 4
  • 7
  • 3
    Interesting question related to generics. – RudolphEst Jan 06 '17 at 11:11
  • 3
    I tried assigning the `new MyClass().n` to a variable and then checked the actual type. It turns out the type of `new MyClass().n` is in fact `java.lang.Integer`. – RudolphEst Jan 06 '17 at 11:23
  • 2
    I guess it has something to do with the fact that a long can be cast to an integer, while a `Long` cannot be cast to an `Integer`. In combination with auto-boxing/unboxing, strange results like this happen. – MC Emperor Jan 06 '17 at 11:26
  • 2
    I have tried the same code on JDK 7, 8 and 9. They all give the same result. I am not sure why the new JShell is different, but I assume that when executing the script, it probably does a `getClass()` call internally and that is why the error happens there on the first line – RudolphEst Jan 06 '17 at 11:27
  • 1
    `Object l = new MyClass().n; System.out.println(l.getClass());` Output: `class java.lang.Integer` But why is it an Integer if it is MyClass -->posted a bit late; – Socke Jan 06 '17 at 11:29
  • @Socke exactly, this is a very interesting question! I hope someone has an answer for us! – RudolphEst Jan 06 '17 at 11:30
  • 1
    The runtime type of n is `Integer`. The runtime type is not affected by casting. – Calculator Jan 06 '17 at 11:31
  • Again, please give more information about what Java Version exactly JShell and IntelliJ are using. – realvictorprm Jan 06 '17 at 11:34
  • 1
    This has nothing to do with auto-boxing/unboxing. If you replace `Number` by `Super`, `Integer` by `Sub1` and `Long` by `Sub2`, same results occur. – MC Emperor Jan 06 '17 at 11:35
  • 1
    @MCEmperor - agreed, there should never be any boxing in this example – RudolphEst Jan 06 '17 at 11:37
  • @ArthurM. JShell -- Version 9-ea ; Version 9-ea for intelliJ aswell. If that is enough. EDIT: (build 9-ea+143 – Socke Jan 06 '17 at 11:37
  • 1
    @Calculator the question is actually why the run-time type of `new MyClass().n` is `Integer` and not `Long`. The type at the declaration is which is `Long` for this case – RudolphEst Jan 06 '17 at 11:38
  • 1
    @ArthurM. I think you are focusing on the wrong thing. Every java runtime gives this error for all versions of the JDK I've tested so far. You can effectively ignore what the runtime is. – RudolphEst Jan 06 '17 at 11:39
  • @RudolphEst Does IntelliJ only shows the wrong output or what did you mean with that? – realvictorprm Jan 06 '17 at 11:41
  • 1
    I just tried it on JShell and the first ouput statement is executed correctly. It ouputs 8. I included it in my answer. – Calculator Jan 06 '17 at 14:26

4 Answers4

39

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.

Calculator
  • 2,769
  • 1
  • 13
  • 18
  • Yep, that's the solution. However, is such a behauviour correct? This should not compile! – realvictorprm Jan 06 '17 at 11:43
  • 1
    @ArthurM. it's correct according to the definition of the Java language (particularly the way generics are treated in the language). Therefore it's correct that the compiler should accept it. Whether the language definition is deficient in this respect is another matter. – Klitos Kyriacou Jan 06 '17 at 11:45
  • 2
    @ArthurM. there is no problem with the compiler. It compiles correctly because of the type cast to `(N)` – RudolphEst Jan 06 '17 at 11:45
  • 1
    @ArthurM. This is the odd nature of unchecked casts. The actual cast occurs not immediately. However, my answer is just a partial solution. I can't explain why JShell behaves differently. – Calculator Jan 06 '17 at 11:45
  • This seems to clarify it! I'll mark it as answer. Thank you very much. – Socke Jan 06 '17 at 11:46
  • That's what I meant. I did not asked, whether it's a bug. I asked whether we should accept such behaviour :D – realvictorprm Jan 06 '17 at 11:46
  • I am still a little confused as to why `Object myN = new MyClass().n;` and then `System.out.println( myN.getClass() );` does not throw the same error. I need to go back to the Java language spec. – RudolphEst Jan 06 '17 at 11:47
  • Maybe this stuff is another question worth. I'm interested in what the JShell is doing different – realvictorprm Jan 06 '17 at 11:48
  • 2
    @RudolphEst it's because the compiler considers `new MyClass().n` as a Long (when it's actually an Integer), but doesn't need to cast it because it's being assigned to a superclass, Object. But the call `new MyClass().n.anyMethodNameHere()` is asking the compiler to actually do `((Long)(new MyClass().n)).anyMethodNameHere()`. This is the mess that is Java generics and type erasure. – Klitos Kyriacou Jan 06 '17 at 11:50
  • @RudolphEst: for method invocations, it is mandatory to use the static receiver type. The fact that this method has been inherited — *at compile time* — must not appear in the compiled class, as, at runtime, the method might not be inherited but declared directly. This allows certain compatible refactorings. This formal rule is applied even in scenarios, where such refactoring will never happen, i.e. `Long.getClass()` is encoded as such, even if the method will always be inherited from `Object`. – Holger Jan 06 '17 at 15:10
  • 2
    Ok, i must admit that i didn't put my `new MyClass().n' into a 'System.out.println();' , because normally JShell prints that variable ... but of course the JShell saves it this way : '$1 = new MyClass().n' that is probably the reason why i got confused!. – Socke Jan 06 '17 at 16:18
  • @Socke This makes sense. On the assignment you have again this hidden invalid cast from Integer to Long. – Calculator Jan 06 '17 at 16:26
25

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.

Raphaël
  • 3,646
  • 27
  • 28
  • Is `N` beeing replaced by `Objects` or by `Numbers`.? "Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded" src: https://docs.oracle.com/javase/tutorial/java/generics/erasure.html – Socke Jan 06 '17 at 11:54
  • 1
    @Socke: My bad, it is replaced by `Number`. – Raphaël Jan 06 '17 at 11:55
1

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.

newacct
  • 119,665
  • 29
  • 163
  • 224
  • 2
    The Java Language Specification is pretty strict when it comes to types, generics, casts, compile time and runtime checks. I don't think a (conforming) Java compiler has the choice to do the cast or to leave it out (along with the required checks and resulting exceptions). I guess [JLS §5.5.2 "Checked Casts and Unchecked Casts"](https://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.5.2) is the relevant part for this case, but I did not look at it in detail. – siegi Jan 10 '17 at 18:39
0

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));.

Maroun
  • 94,125
  • 30
  • 188
  • 241
Aman Goyal
  • 99
  • 10