11

I am using java 8.

I recently came across this:

public class Test {
    public static void main(String[] args) {
        String ss = "" + (Test.<Integer>abc(2));
        System.out.println(Test.<Integer>abc(2));
    }
    public static <T> T abc(T a) {
        String s = "adsa";
        return (T) s;
    }
}

This does not throw a java.lang.ClassCastException. Why is that?

I always thought + and System.out.println calls toString. But when I try to do that it throws an Exception as expected.

String sss = (Test.<Integer>abc(2)).toString();
MaximilianMairinger
  • 2,176
  • 2
  • 15
  • 36
  • 10
    Enable all compiler warnings, and you will see the answer: your cast is not “safe.” Meaning, it is not doing what you think it’s doing. Specifically, generics are subject to type erasure, meaning they are only a means of enforcing safety at compile time, not runtime. At runtime, the method doesn’t know `T` is `Integer`. The compiler translated `(T)` to `(Object)` due to the declaration of ``. – VGR Nov 06 '18 at 15:42
  • Relates question: https://stackoverflow.com/questions/45757141/java-raw-type-value-assigned-to-generic-type-run-time-getclss-method-error – tsolakp Nov 06 '18 at 16:45
  • See also [this quite clear answer](https://stackoverflow.com/a/47548635/6413377) to my question related. – pirho Nov 06 '18 at 19:56

2 Answers2

12

It doesn't throw a ClassCastException because all generic type information is stripped from the compiled code (a process called type erasure). Basically, any type parameter is replaced by Object. That's why the first version works. It's also why the code compiles at all. If you ask the compiler to warn about unchecked or unsafe operations with the -Xlint:unchecked flag, you'll get a warning about an unchecked cast in the return statement of abc().

With this statement:

String sss = (Test.<Integer>abc(2)).toString();

the story is a bit different. While the type parameter T is replaced by Object, the calling code gets translated into byte code that explicitly casts the result to Integer. It is as if the code were written with a method with signature static Object abc(Object) and the statement were written:

String sss = ((Integer) Test.abc(Integer.valueOf(2))).toString();

That is, not only does the cast inside abc() go away due to type erasure, a new cast is inserted by the compiler in the calling code. This cast generates a ClassCastException at run time because the object returned from abc() is a String, not an Integer.

Note that the statement

String ss = "" + (Test.<Integer>abc(2));

doesn't require a cast because the compiler simply feeds the object returned by abc() into a string concatenation operation for objects. (The details of how this is done varies with the Java compiler, but it is either a call to a StringBuilder append method or, as of Java 9, a call to a method created by StringConcatFactory.) The details here don't matter; the point is that the compiler is smart enough to recognize that no cast is needed.

Ted Hopp
  • 232,168
  • 48
  • 399
  • 521
  • 2
    @KevinCruijssen The compiler doesn't care or know about `String.valueOf`. String concatenation compiles to a series of `StringBuilder.append`s. The append [may be called with a string](https://docs.oracle.com/javase/8/docs/api/java/lang/StringBuilder.html#append-java.lang.String-) if you explicitly called `toString` (might NPE), or [with an object if you didn't](https://docs.oracle.com/javase/8/docs/api/java/lang/StringBuilder.html#append-java.lang.Object-). In the latter case, `String.valueOf` is used by `StringBuilder`, but that's an implementation detail. – Michael Nov 06 '18 at 16:47
  • @Michael - You're right about that. I was just trying to make the point that the compiler didn't need to insert a cast check into the compiled code; I wasn't trying to explain how the compiler dealt with string concatenation. Unfortunately, in the process I generated a misleading example. I'll see if I can rework that piece of the answer. – Ted Hopp Nov 06 '18 at 18:43
6

Generics disappear at runtime, so a cast to T is really just a cast to Object (which the compiler will actually just get rid of), so there's no class cast exception.

abc is just a method which takes an object and returns an object. StringBuilder.append(Object) is the method which is called, as can be seen from the bytecode:

...  
16: invokestatic  #7   // Method abc:(Ljava/lang/Object;)Ljava/lang/Object;
19: invokevirtual #8   // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
...

When you do

String sss = (Test.<Integer>abc(2)).toString();

Then the bytecode is

...
 4: invokestatic  #3   // Method abc:(Ljava/lang/Object;)Ljava/lang/Object;
 7: checkcast     #4   // class java/lang/Integer
10: invokevirtual #5   // Method java/lang/Integer.toString:()Ljava/lang/String;
...

Your code fails at the checkcast operation, which was not present before.

Michael
  • 41,989
  • 11
  • 82
  • 128