ng.newbie
ng.newbie

Reputation: 3226

Why doesn't @SneakyThrows throw a ClassCastException?

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 ?

Upvotes: -1

Views: 122

Answers (3)

Slaw
Slaw

Reputation: 46170

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

  • The erasure of a type variable (§4.4) is the erasure of its leftmost bound.

That means that the erasure of:

static <T extends Exception> void test(Exception d) throws T {
    throw (T) d;
}

Is:

static void test(Exception d) throws Exception {
    throw (Exception) d;
}

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:

ClassName.<RuntimeException>test(new SQLException());

You don't end up with byte code where T's bound was changed to RuntimeException. The leftmost bound is still Exception.

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 test method 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 parameter T extends Exception, and that it throws 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 test with RuntimeException (or some subclass) as the type argument. The compiler knows about T, and thus "knows" in this case that the method does not throw a checked exception. Yet notice the (T) d line of the test method 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., claiming T is an unchecked exception, but then having the method actually throw a checked exception).


Casting

From §5.5 Casting Contexts of the JLS:

If a casting context makes use of a narrowing reference conversion that is checked or partially unchecked (§5.1.6.2, §5.1.6.3), then a run time check will be performed on the class of the expression's value, possibly causing a ClassCastException. Otherwise, no run time check is performed. [emphasis added]

And from §5.1.6.2 Checked and Unchecked Narrowing Reference Conversions:

  • A narrowing reference conversion from a type S to a type variable T is unchecked.

And from §5.1.6.3 Narrowing Reference Conversions at Run Time:

  • An unchecked narrowing reference conversion from S to a non-intersection type T is completely unchecked if |S| <: |T|.

    Otherwise, it is partially unchecked.

Taking all that into account, the following:

static <T extends Exception> void test(Exception d) throws T {
    throw (T) d;
}

Is a completely unchecked cast. Thus, there is no check a run-time. You can verify this by looking at the byte code.

Upvotes: 2

Thomas Kl&#228;ger
Thomas Kl&#228;ger

Reputation: 21630

Your misconception is here:

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: << this is your misconception: the method doesn't change!

static void test(Exception d) throws RuntimeException {
    throw (RuntimeException) d;
}

This is not how generics in Java work - the test() method is not recompiled nor otherwise changed in any way. The method is:

static <T extends Exception> void test(Exception d) throws T {
    throw (T) d;
}

Since T could by any Exception, the cast (T) d is effectively a no-op and the compiler doesn't produce any code for this cast (it cannot do a cast: the caller doesn't pass in the type T and the code must work for all T extends Exception, but d is necessarily an Exception anyway because this is the declared type of the parameter.)

The compiler only checks the call site. Is the passed parameter (SQLException) an Exception or any subclass of Exception? Yes, SQLException is a subclass of Exception and therefore the call compiles.

Upvotes: 2

Vivick
Vivick

Reputation: 4991

You're confusing generics with templates (à la C++). You were on the right track with mentioning type erasure which, as the name suggests, erases any trace of type information for compilation.

Thus, your cast doesn't really happen, or if it does it's not with the type you'd expect.

You'd need to use Class#cast to get the desired behavior, but getting an instance of Class<T> ain't convenient for your use case.

Upvotes: -1

Related Questions