Tuesday, 18 September 2012

Puzzle 40: The Reluctant Constructor


Although it is common to see a throws clause on a method declaration, it is less common to see one on a constructor declaration. The following program has such a declaration. What does it print?
public class Reluctant {

    private Reluctant internalInstance = new Reluctant();



    public Reluctant() throws Exception {

        throw new Exception("I'm not coming out");

    }



    public static void main(String[] args) {

        try {

            Reluctant b = new Reluctant();

            System.out.println("Surprise!");

        } catch (Exception ex) {

            System.out.println("I told you so");

        }

    }

}


Solution 40: The Reluctant Constructor

The main method invokes the Reluctant constructor, which throws an exception. You might expect the catch clause to catch this exception and print I told you so. A closer look at the program reveals that the Reluctant instance contains a second internal instance, and its constructor also throws an exception. Whichever exception gets thrown, it looks as though the catch clause in main should catch it, so it seems a safe bet that the program will print I told you so. But if you tried running it, you found that it does nothing of the sort: It throws a StackOverflowError. Why?
Like most programs that throw a StackOverflowError, this one contains an infinite recursion. When you invoke a constructor, the instance variable initializers run before the body of the constructor [JLS 12.5]. In this case, the initializer for the variable internalInstance invokes the constructor recursively. That constructor, in turn, initializes its own internalInstance field by invoking the Reluctant constructor again and so on, ad infinitum. These recursive invocations cause a StackOverflowError before the constructor body ever gets a chance to execute. Because StackOverflowError is a subtype of Error rather than Exception, the catch clause in main doesn't catch it.
It is not uncommon for an object to contain instances of its own type. This happens, for example, in linked list nodes, tree nodes, and graph nodes. You must initialize such contained instances carefully to avoid a StackOverflowError.
As for the nominal topic of this puzzle—constructors declared to throw exceptions—note that a constructor must declare any checked exceptions thrown by its instance initializers. This program, which illustrates a common service-provider pattern, won't compile, because it violates this rule:
public class Car {

    private static Class engineClass = ... ; // Service provider

    private Engine engine = (Engine) engineClass.newInstance();

    public Car() { }  // Throws two checked exceptions!

}


Although it has no body, the constructor throws two checked exceptions: InstantiationException and IllegalAccessException. They are thrown by Class.newInstance, which is called when initializing the engine field. The best way to fix this is to create a private static helper method that computes the initial value of the field and handles exceptions appropriately. In this case, let's assume that the Class object referred to by engineClass was chosen to guarantee that it is both accessible and instantiable. The following version of Car compiles without error:
// Fixed - instance initializers don't throw checked exceptions

public class Car {

    private static Class engineClass = ... ;

    private Engine engine = newEngine();

    private static Engine newEngine() {

        try {

            return (Engine) engineClass.newInstance();

        } catch (IllegalAccessException e) {

            throw new AssertionError(e);

        } catch (InstantiationException e) {

            throw new AssertionError(e);

        }

    }

    public Car() { }

}


In summary, instance initializers run before constructor bodies. Any exceptions thrown by instance initializers propagate to constructors. If initializers throw checked exceptions, constructors must be declared to throw them too, but this should be avoided, because it is confusing. Finally, beware of infinite recursion when designing classes whose instances contain other instances of the same class.

No comments:

Post a Comment

Your comments are welcome!