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!