This question was inducted by this StackOverflow question about unsafe casts: Java Casting method without knowing what to cast to. While answering the question I encountered this behaviour I couldn't explain based on purely the specification
I found the following statement in The Java Tutorials at the Oracle docs:
- Insert type casts if necessary to preserve type safety. The Java Tutorials: Type Erasure
It is not explained what "if necessary" means exactly, and I've found no mention about these casts in the Java Language Specification at all, so I started to experiment.
Let's look at the following piece of code:
// Java source
public static <T> T identity(T x) {
return x;
}
public static void main(String args[]) {
String a = identity("foo");
System.out.println(a.getClass().getName());
// Prints 'java.lang.String'
Object b = identity("foo");
System.out.println(b.getClass().getName());
// Prints 'java.lang.String'
}
Compiled with javac
and decompiled with the Java Decompiler:
// Decompiled code
public static void main(String[] paramArrayOfString)
{
// The compiler inserted a cast to String to ensure type safety
String str = (String)identity("foo");
System.out.println(str.getClass().getName());
// The compiler omitted the cast, as it is not needed
// in terms of runtime type safety, but it actually could
// do an additional check. Is it some kind of optimization
// to decrease overhead? Where is this behaviour specified?
Object localObject1 = identity("foo");
System.out.println(localObject1.getClass().getName());
}
I can see that there is a cast which ensures type safety in the first case,
but in the second case it is omitted. It is
fine of course, because I want to store the return value in an Object
typed variable, so the cast is not strictly necessary as per type safety. However it leads to an interesting behaviour with unsafe casts:
public class Erasure {
public static <T> T unsafeIdentity(Object x) {
return (T) x;
}
public static void main(String args[]) {
// I would expect c to be either an Integer after this
// call, or a ClassCastException to be thrown when the
// return value is not Integer
Object c = Erasure.<Integer>unsafeIdentity("foo");
System.out.println(c.getClass().getName());
// but Prints 'java.lang.String'
}
}
Compiled and decompiled, I see no type cast to ensure correct return type at runtime:
// The type of the return value of unsafeIdentity is not checked,
// just as in the second example.
Object localObject2 = unsafeIdentity("foo");
System.out.println(localObject2.getClass().getName());
This means that if a generic function should return an object of a given
type, it is not guaranteed it will return that type ultimately. An
application using the above code will fail at the first point where it tries
to cast the return value to an Integer
if it does so at all, so I feel like
it breaks the fail-fast principle.
What are the exact rules of the compiler inserting this cast during compilation that ensures type safety and where are those rules specified?
EDIT:
I see that the compiler will not dig into the code and try to prove that the generic code really returns what it should, but it could insert an assertation, or at least a type cast (which it already does in specific cases, as seen in the first example) to ensure correct return type, so the latter would throw a ClassCastException
:
// It could compile to this, throwing ClassCastException:
Object localObject2 = (Integer)unsafeIdentity("foo");
return (T) x;
, it has no way to know statically thatx
can't be converted toT
; and (2) when you actually callunsafeIdentity
, the compiler can't know that this will fail because it will not delve into the code of the method and look for statements that will fail. Basically, I think this means that the cast to(T)
in the method is useless. – PauloObject o = (Integer)unsafeIdentity("foo");
, and that would throw aClassCastException
or am I missing something? – GustyT
, so I can see how the compiler might be able to add this check without reading the code of the method. But that would add unnecessary overhead in the vast majority of cases, including lots ofCollections
classes where, say, aget()
method returns a generic type. That's probably an unacceptable tradeoff. – PaulounsafeIdentity
method, and from that on, there are no guarantees about the type anyhow. However, I think that hg.openjdk.java.net/jdk8/jdk8/langtools/file/756ae3791c45/src/… might be relevant here, as it clearly says that it simply does not insert the cast when it is not necessary (and in fact, this may even be the answer to your question - but I'm not sure) – BakemanInteger
if it does so at all, so I feel like it breaks the fail-fast principle." When a cast fails at runtime, it always fails only when it actually tries the cast. When they intend to detect it early, they do so during compile-time. – Rabassa