Reputation: 220762
I have recently discovered and blogged about the fact that it is possible to sneak a checked exception through the javac compiler and throw it where it mustn't be thrown. This compiles and runs in Java 6 and 7, throwing a SQLException
without throws
or catch
clause:
public class Test {
// No throws clause here
public static void main(String[] args) {
doThrow(new SQLException());
}
static void doThrow(Exception e) {
Test.<RuntimeException> doThrow0(e);
}
static <E extends Exception> void doThrow0(Exception e) throws E {
throw (E) e;
}
}
The generated bytecode indicates that the JVM doesn't really care about checked / unchecked exceptions:
// Method descriptor #22 (Ljava/lang/Exception;)V
// Stack: 1, Locals: 1
static void doThrow(java.lang.Exception e);
0 aload_0 [e]
1 invokestatic Test.doThrow0(java.lang.Exception) : void [25]
4 return
Line numbers:
[pc: 0, line: 11]
[pc: 4, line: 12]
Local variable table:
[pc: 0, pc: 5] local: e index: 0 type: java.lang.Exception
// Method descriptor #22 (Ljava/lang/Exception;)V
// Signature: <E:Ljava/lang/Exception;>(Ljava/lang/Exception;)V^TE;
// Stack: 1, Locals: 1
static void doThrow0(java.lang.Exception e) throws java.lang.Exception;
0 aload_0 [e]
1 athrow
Line numbers:
[pc: 0, line: 16]
Local variable table:
[pc: 0, pc: 2] local: e index: 0 type: java.lang.Exception
The JVM accepting this is one thing. But I have some doubts whether Java-the-language should. Which parts of the JLS justify this behaviour? Is it a bug? Or a well-hidden "feature" of the Java language?
My feelings are:
doThrow0()
's <E>
is bound to RuntimeException
in doThrow()
. Hence, no throws
clause along the lines of JLS §11.2 is needed in doThrow()
.RuntimeException
is assignment-compatible with Exception
, hence no cast (which would result in a ClassCastException
) is generated by the compiler.Upvotes: 29
Views: 4846
Reputation: 200148
All this amounts to exploiting the loophole that an unchecked cast to a generic type is not a compiler error. Your code is explicitly made type-unsafe if it contains such an expression. And since the checking of checked exceptions is strictly a compile-time procedure, nothing will break in the runtime.
The answer from the authors of Generics would most probably be along the lines of "If you are using unchecked casts, it's your problem".
I see something quite positive in your discovery—a break out of the stronghold of checked exceptions. Unfortunately, this can't turn existing checked-exception-poisoned APIs into something more pleasant to use.
At a typical layered application project of mine there will be a lot of boilerplate like this:
try {
... business logic stuff ...
}
catch (RuntimeException e) { throw e; }
catch (Exception e) { throw new RuntimeException(e); }
Why do I do it? Simple: there are no business-value exceptions to catch; any exception is a symptom of a runtime error. The exception must be propagated up the call stack towards the global exception barrier. With Lukas' fine contribution I can now write
try {
... business logic stuff ...
} catch (Exception e) { throwUnchecked(e); }
This may not seem like much, but the benefits accumulate after repeating it 100 times throughout the project.
In my projects there is high discipline regarding exceptions so this is a nice fit for them. This kind of trickery is not something to adopt as a general coding principle. Wrapping the exception is still the only safe option in many cases.
Upvotes: 9
Reputation: 6181
Well, that's one of multiple ways to raise a check exception as if it were unchecked. Class.newInstance()
is another, Thread.stop(Trowable)
a deprecated one.
The only way for the JLS not to accept this behavior is if the runtime (JVM) will enforce it.
As to were it is specified: It isn't. Checked and unchecked exceptions behave the same. Checked exceptions simply require a catch block or a throws clause.
Edit: Based on the discussion in the comments, a list-based example exposing the root cause: Erasure
public class Main {
public static void main(String[] args) {
List<Exception> myCheckedExceptions = new ArrayList<Exception>();
myCheckedExceptions.add(new IOException());
// here we're tricking the compiler
@SuppressWarnings("unchecked")
List<RuntimeException> myUncheckedExceptions = (List<RuntimeException>) (Object) myCheckedExceptions;
// here we aren't any more, at least not beyond the fact that type arguments are erased
throw throwAny(myUncheckedExceptions);
}
public static <T extends Throwable> T throwAny(Collection<T> throwables) throws T {
// here the compiler will insert an implicit cast to T (just like your explicit one)
// however, since T is a type variable, it gets erased to the erasure of its bound
// and that happens to be Throwable, so a useless cast...
throw throwables.iterator().next();
}
}
Upvotes: 2
Reputation: 4571
I don't think it can be disputed that the JLS allows that code. The question is not whether that code should be legal. The compiler simply does not have enough information to issue an error.
The issue here with the code emitted when you call a generic-throwing method. The compiler currently inserts type casts after calling a generic-returning methods where the returned value will be immediately assigned to a reference.
If the compiler wanted, it could prevent the problem by silently surrounding all generic-throwing method invocations in a try-catch-rethrow fragment. Since the exception would be caught and assigned to a local variable, a type cast would be mandatory. So you would get a ClassCastException
if it was not an instance of RuntimeException
.
But the spec does not mandate anything special about generic-throwing methods. I believe that's an spec bug. Why not file a bug about that, so that it can be fixed or at least documented.
By the way, the ClassCastException
would suppress the original exception, which can result in hard-to-find bugs. But that problem has a simple solution since JDK 7.
Upvotes: 1
Reputation: 19185
This example is documented in The CERT Oracle Secure Coding Standard for Java which documents several non compliant code examples.
Noncompliant Code Example (Generic Exception)
An unchecked cast of a generic type with parameterized exception declaration can also result in unexpected checked exceptions. All such casts are diagnosed by the compiler unless the warnings are suppressed.
interface Thr<EXC extends Exception> {
void fn() throws EXC;
}
public class UndeclaredGen {
static void undeclaredThrow() throws RuntimeException {
@SuppressWarnings("unchecked") // Suppresses warnings
Thr<RuntimeException> thr = (Thr<RuntimeException>)(Thr)
new Thr<IOException>() {
public void fn() throws IOException {
throw new IOException();
}
};
thr.fn();
}
public static void main(String[] args) {
undeclaredThrow();
}
}
This works because RuntimeException
is child class of Exception
and you can not convert any class extending from Exception
to RuntimeException
but if you cast like below it will work
Exception e = new IOException();
throw (RuntimeException) (e);
The case you are doing is same as this.
Because this is explicit type casting this call will result in ClassCastException
however compiler is allowing it.
Because of Erasure however in your case there is no cast involved and hence in your scenario it does not throw ClassCastException
In this case there is no way for compiler to restrict the type conversion you are doing.
However if you change method signature to below it will start complaining about it.
static <E extends Exception> void doThrow0(E e) throws E {
Upvotes: 2
Reputation: 41
JLS 11.2:
For each checked exception which is a possible result, the throws clause for the method (§8.4.6) or constructor (§8.8.5) must mention the class of that exception or one of the superclasses of the class of that exception (§11.2.3).
This clearly indicates doThrow must have Exception in it's throws clause. Or, as downcasting (Exception to RuntimeException) is involved, the check must be done that Exception IS RuntimeException, which should fail in example because the exception being cast is SQLException. So, ClassCastException should be thrown in runtime.
As of practical side, this java bug allows to create a hack unsafe for any standard exception handling code, like next:
try {
doCall();
} catch (RuntimeException e) {
handle();
}
The exception will go up without being handled.
Upvotes: 1