Exception restrictions

Bruce Eckel’s Thinking in Java Contents | Prev | Next

When
you override a method, you can throw only the exceptions that have been
specified in the base-class version of the method. This is a useful
restriction, since it means that code that works with the base class will
automatically work with any object derived from the base class (a fundamental
OOP concept, of course), including exceptions.

This
example demonstrates the kinds of restrictions imposed (at compile time) for
exceptions:

//: StormyInning.java
// Overridden methods may throw only the 
// exceptions specified in their base-class 
// versions, or exceptions derived from the 
// base-class exceptions.
 
class BaseballException extends Exception {}
class Foul extends BaseballException {}
class Strike extends BaseballException {}
 
abstract class Inning {
  Inning() throws BaseballException {}
  void event () throws BaseballException {
   // Doesn't actually have to throw anything
  }
  abstract void atBat() throws Strike, Foul;
  void walk() {} // Throws nothing
}
 
class StormException extends Exception {}
class RainedOut extends StormException {}
class PopFoul extends Foul {}
 
interface Storm {
  void event() throws RainedOut;
  void rainHard() throws RainedOut;
}
 
public class StormyInning extends Inning
    implements Storm {
  // OK to add new exceptions for constructors,
  // but you must deal with the base constructor
  // exceptions:
  StormyInning() throws RainedOut,
    BaseballException {}
  StormyInning(String s) throws Foul,
    BaseballException {}
  // Regular methods must conform to base class:
//! void walk() throws PopFoul {} //Compile error
  // Interface CANNOT add exceptions to existing
  // methods from the base class:
//! public void event() throws RainedOut {}
  // If the method doesn't already exist in the
  // base class, the exception is OK:
  public void rainHard() throws RainedOut {}
  // You can choose to not throw any exceptions,
  // even if base version does:
  public void event() {}
  // Overridden methods can throw 
  // inherited exceptions:
  void atBat() throws PopFoul {}
  public static void main(String[] args) {
    try {
      StormyInning si = new StormyInning();
      si.atBat();
    } catch(PopFoul e) {
    } catch(RainedOut e) {
    } catch(BaseballException e) {}
    // Strike not thrown in derived version.
    try {
      // What happens if you upcast?
      Inning i = new StormyInning();
      i.atBat();
      // You must catch the exceptions from the
      // base-class version of the method:
    } catch(Strike e) {
    } catch(Foul e) {
    } catch(RainedOut e) {
    } catch(BaseballException e) {}
  }
} ///:~ 

In
Inning,
you can see that both the constructor and the
event( )
method say they will throw an exception, but they never do. This is legal
because it allows you to force the user to catch any exceptions that you might
add in overridden versions of
event( ).
The same idea holds for
abstract
methods, as seen in
atBat( ).

The
interface
Storm

is interesting because it contains one method (
event( ))that
is defined in
Inning,
and one method that isn’t. Both methods throw a new type of exception,
RainedOut.
When
StormyInning
extends
Inning

and
implements
Storm
,
you’ll see that the
event( )
method in
Storm
cannot
change the exception interface of
event( )
in
Inning.
Again, this makes sense because otherwise you’d never know if you were
catching the correct thing when working with the base class. Of course, if a
method described in an
interface
is not in the base class, such as
rainHard( ),
then
there’s no problem if it throws exceptions.

The
reason
StormyInning.walk( )
will not compile is that it throws an exception, while
Inning.walk( )
does not. If this was allowed, then you could write code that called
Inning.walk( )
and that didn’t have to handle any exceptions, but then when you
substituted an object of a class derived from
Inning,
exceptions would be thrown so your code would break. By forcing the
derived-class methods to conform to the exception specifications of the
base-class methods, substitutability of objects is maintained.

The
overridden
event( )
method shows that a derived-class version of a method may choose to not throw
any exceptions, even if the base-class version does. Again, this is fine since
it doesn’t break any code that is written assuming the base-class version
throws exceptions. Similar logic applies to
atBat( ),
which throws
PopFoul,
an exception that is derived from
Foul
thrown by the base-class version of
atBat( ).
This way, if someone writes code that works with
Inning
and calls
atBat( ),
they must catch the
Foul
exception. Since
PopFoul
is derived from
Foul,
the exception handler will also catch
PopFoul.

It’s
useful to realize that although exception specifications are enforced by the
compiler during inheritance, the exception specifications are not part of the
type of a method, which is comprised of only the method name and argument
types. Therefore, you cannot overload methods based on exception
specifications. In addition, because an exception specification exists in a
base-class version of a method doesn’t mean that it must exist in the
derived-class version of the method, and this is quite different from
inheriting the methods (that is, a method in the base class must also exist in
the derived class). Put another way, the “exception specification
interface” for a particular method may narrow during inheritance and
overriding, but it may not widen – this is precisely the opposite of the
rule for the class interface during inheritance.


[43]
ANSI/ISO C++ added similar constraints that require derived-method exceptions
to be the same as, or derived from, the exceptions thrown by the base-class
method. This is one case in which C++ is actually able to check exception
specifications at compile time.

More by Author

Must Read