Tuesday, 18 September 2012

Puzzle 43: Exceptionally Unsafe


In JDK 1.2, Thread.stop, Thread.suspend, and a few other thread-related methods were deprecated because they are unsafe [ThreadStop]. The following method demonstrates one of the horrible things you could do with Thread.stop:
// Don't do this - circumvents exception checking!

public static void sneakyThrow(Throwable t) {

    Thread.currentThread().stop(t); // Deprecated!!

}


This nasty little method does exactly what the tHRow statement does, except that it bypasses all exception checking by the compiler. You can (sneakily) throw any exception, checked or otherwise, from any point in your code, and the compiler won't bat an eyelash.
It is possible to write a method that is functionally equivalent to sneakyThrow without using any deprecated methods. In fact, there are at least two ways to do it. One of them works only in release 5.0 and later releases. Can you write such a method? It must be written in Java, not in JVM bytecode. You must not change the method after its clients are compiled. Your method doesn't have to be perfect: It is acceptable if it can't throw one or two subclasses of Exception.

Solution 43: Exceptionally Unsafe

One solution to this puzzle takes advantage of a design deficiency in the Class.newInstance method, which instantiates a class reflectively. To quote from the documentation for this method [Java-API]: "Note that this method propagates any exception thrown by the nullary [in other words, parameterless] constructor, including a checked exception. Use of this method effectively bypasses the compile-time exception checking that would otherwise be performed." Once you know this, it's not too hard to write a sneakyThrow equivalent:
// Don't do this either - circumvents exception checking!

public class Thrower {

    private static Throwable t;



    private Thrower() throws Throwable {

        throw t;

    }



    public static synchronized void sneakyThrow(Throwable t) {

        Thrower.t = t;

        try {

            Thrower.class.newInstance();

        } catch (InstantiationException e) {

            throw new IllegalArgumentException();

        } catch (IllegalAccessException e) {

            throw new IllegalArgumentException();

        } finally {

            Thrower.t = null; // Avoid memory leak

        }

    }

}


A few subtle things are going on in this solution. The exception to be thrown during constructor execution can't be passed to the constructor as a parameter, because Class.newInstance invokes a class's parameterless constructor. Therefore, the sneakyThrow method stashes this exception in a static variable. To make the method thread-safe, it must be synchronized. This causes concurrent invocations to take turns using the static t field.
Note that the t field is nulled out in a finally block: Just because the method is sneaky doesn't mean it should also be leaky. If this field weren't nulled out, it would prevent the exception from being garbage collected. Finally, note that the method will fail with an IllegalArgumentException if you ask it to throw an InstantiationException or IllegalAccessException. This is an inherent limitation of the technique.
The documentation for Class.newInstance goes on to say that "the Constructor.newInstance method avoids this problem by wrapping any exception thrown by the constructor in a (checked) InvocationTargetException." Clearly, Class.newInstance should have done the same thing, but it's far too late to correct this deficiency. Doing so would introduce a source-level incompatibility, breaking the many programs that depend on Class.newInstance. It would not be practical to deprecate this method either, because it is so commonly used. Just be aware when you use it that Class.newInstance can throw checked exceptions that it does not declare.
Generics, which were added in release 5.0, enable a completely different solution to this puzzle. For maximal compatibility generics are implemented by type erasure: Generic type information is checked at compile time but not at run time [JLS 4.7]. The following solution exploits this:
// Don't do this either - circumvents exception checking!

class TigerThrower<T extends Throwable> {

    public static void sneakyThrow(Throwable t) {

        new TigerThrower<Error>().sneakyThrow2(t);

    }



    private void sneakyThrow2(Throwable t) throws T {

        throw (T) t;

    }

}


This program will generate a warning when you compile it:
TigerThrower.java:7: warning: [unchecked] unchecked cast

found   : java.lang.Throwable, required: T

        throw (T) t;

                  ^


A warning is the compiler's way of telling you that you may be shooting yourself in the foot, and in fact you are. The unchecked cast warning tells you that the cast in question will not be checked at run time. When you get an unchecked cast warning, modify your program to eliminate it, or convince yourself that the cast cannot fail. If you don't, some other cast may fail at an undetermined time in the future, and you may have a hard time tracing the error to its source. In this case, it's even worse: The exception that is thrown at run time may not conform to the signature of the method. The sneakyThrow2 method exploits this methodically.
There are several lessons for platform designers. When designing libraries that are implemented outside the language, such as the reflection library, preserve all guarantees made by the language. When designing from scratch a platform that supports generic types, consider enforcing their correctness at run time. The designers of Java's generic typing facility did not have this luxury, as they were constrained by the requirement that generified libraries interoperate with existing clients. To eliminate the possibility of an exception that violates a method's signature, consider enforcing exception checking at run time.
In summary, Java's exception checking is not enforced by the virtual machine. It is a compile-time facility designed to make it easier to write correct programs, but it can be circumvented at run time. To reduce your exposure, do not ignore compiler warnings.

No comments:

Post a Comment

Your comments are welcome!