This question just asks why the generic cast is not used. The questions about Erasure do not seem to explain this edge case.
Before I start let me preface this by saying I am NOT interested in type inference as described here:
A peculiar feature of exception type inference in Java 8
This is just to avoid confusion.
What I am interested in is why the following code works without throwing a ClassCastException ?
import java.sql.SQLException;
public class GenericThrows {
static <T extends Exception> void test(Exception d) throws T {
throw (T) d;
}
public static void main(String[] args) {
GenericTest.<RuntimeException>test(new SQLException());
}
}
Compile the code with:
javac -source 1.7 -target 1.7 GenericThrows.java
And it produces:
Exception in thread "main" java.sql.SQLException
at GenericTest.main(GenericTest.java:9)
My mental model of Java Generics and Type Erasure (and why I think this makes no sense):
When the static method compiles:
static <T extends Exception> void test(Exception d) throws T {
throw (T) d;
}
Type Erasure wipes out all generic types and replaces them with the upper bound of given type, so the method effectively becomes:
static void test(Exception d) throws Exception {
throw (Exception) d;
}
I hope I am correct.
When the main method is compiled:
static <T extends Exception> void test(Exception d) throws T {
throw (T) d;
}
public static void main(String[] args) {
GenericTest.<RuntimeException>test(new SQLException());
}
The type parameter is replaced by the concrete type : java.lang.RuntimeException.
So the method effectively becomes:
static void test(Exception d) throws RuntimeException {
throw (RuntimeException) d;
}
I hope I am correct.
So, when I try to cast a SQLException to a RuntimeException I should get a ClassCastException, this is exactly what happens if I write the code without generics:
import java.sql.SQLException;
public class NonGenericThrows {
static void test(Exception d) throws RuntimeException {
throw (RuntimeException) d;
}
public static void main(String[] args) {
NonGenericThrows.test(new SQLException());
}
}
Compilation and execution:
javac -source 1.7 -target 1.7 NonGenericThrows.java
java NonGenericThrows
Results:
Exception in thread "main" java.lang.ClassCastException: class java.sql.SQLException cannot be cast to class java.lang.RuntimeException (java.sql.SQLException is in module java.sql of loader 'platform'; java.lang.RuntimeException is in module java.base of loader 'bootstrap')
at NonGenericThrows.test(NonGenericThrows.java:5)
at NonGenericThrows.main(NonGenericThrows.java:9)
Then why does the generic version not give a ClassCastException ?
Where am I going wrong in my mental model ?
Type Erasure
You are correct that type variables are erased to their leftmost bound. This is stated in §4.6 Type Erasure of the Java Language Specification (JLS).
That means that the erasure of:
Is:
But note this only relies on the type parameter. The type argument is irrelevant. Unlike languages such as C++, where each unique parameterization of a template results in unique compiled code, Java only compiles generics once. So, if you call the above with:
You don't end up with byte code where
T's bound was changed toRuntimeException. The leftmost bound is stillException.Byte-code
Generics do not completely disappear after compilation. If they did, then generics would be unusable when compiling against pre-compiled libraries.
With the
testmethod above, it's true that the method itself cannot know what it was parameterized with. However, the fact that the method is generic with the type parameterT extends Exception, and that itthrows T, is preserved. In other words, static generic information is recorded in the byte-code. You can even get this information via reflection.Basically, generics are an "illusion" of the compiler. They are a language feature, not a run-time (JVM) feature. From the point of view of the compiler, generics always exist (unless raw types are involved).
This is why you do not need to handle the exception when calling
testwithRuntimeException(or some subclass) as the type argument. The compiler knows aboutT, and thus "knows" in this case that the method does not throw a checked exception. Yet notice the(T) dline of thetestmethod gives an "unchecked cast" compiler warning. This warning occurs precisely because the cast may succeed, but that doesn't mean the code is not broken (e.g., claimingTis an unchecked exception, but then having the method actually throw a checked exception).Casting
From §5.5 Casting Contexts of the JLS:
And from §5.1.6.2 Checked and Unchecked Narrowing Reference Conversions:
And from §5.1.6.3 Narrowing Reference Conversions at Run Time:
Taking all that into account, the following:
Is a completely unchecked cast. Thus, there is no check a run-time. You can verify this by looking at the byte code.