Effective JavaException handling in large systems

IN MY LAST column,1 I showed how Java's cloning mechanism could be safely subverted to support cloning for inner classes and classes with final fields. While it is almost always a good idea to implement the clone method in terms of calling super.clone and patching up the object returned to you, there were two cases where flaws in the design of the cloning mechanism meant cloning was only possible when it was implemented in terms of copy construction.

Another irksome feature of Java's cloning mechanism is the CloneNotSupportedException class, a checked exception appearing in the throws clause of Object.clone. Java requires that every checked exception be caught by some method in the stack frame, somewhere between the call to the method threatening to throw a checked exception and the mother of all methods, main. In the case of this particular checked exception, it begs the question, should the throws clause of Object.clone be propagated by all clone methods or pruned off by the subclass closest to the Object superclass?

In answering this question, I will highlight a more general technique and an approach to exception handling in large systems that runs contrary to established Java wisdom.

Maxim 26: Declare the clone Method With No Checked Exceptions
Because the default cloning operation performed by Object.clone will almost certainly produce booby-trapped clones, cloning must be explicitly enabled for a class by having it implement the Cloneable interface, a marker interface that disables the generation of the exception in Object.clone.

The clone method of the Object class has a unique responsibility: It checks whether the class implements the Cloneable interface and will throw a CloneNotSupportedException exception if it is not; hence its throws clause:


public class Object {
  protected native Object clone() throws
             CloneNotSupportedException;
  // ...
}
Any class that implements Cloneable (or has inherited it from a superclass or superinterface) can be sure the chain of calls through super.clone—eventually terminating in a call to Object.clone—will not generate this checked exception, so it does not need to be propagated:


class Point implements Cloneable {
  public Object clone() {
    try {
      return super.clone();
    } catch (CloneNotSupportedException e) {
      // cannot happen since we are Cloneable
      throw new InternalError(e.toString());
    }
  }
In this regard, handling the redundant exception is an implementation detail of a clone method that wishes to call Object.clone directly through its super reference.

The alternative is to propagate the throws clause inherited from the superclass (Object), even though the overriding clone method will never produce one:


class Point implements Cloneable {
  public Object clone() throws CloneNotSupportedException {
    return super.clone();
  }
This would place users of the Point class in the position of having to deal with the same "impossible" situation—the only difference is they must investigate the class to find out if the exception is actually a possibility. If, as in this case, they discover that it is not, the dummy exception must simply be trapped and absorbed in the user code every time the method is called, instead of once in the class. It is more expedient to handle its absorption in just one place.

The Point class is typical of classes that support cloning, in that its clone method declares no checked exceptions. One rationale for permitting overriding clone methods to throw CloneNotSupportedException objects would be to allow them to indicate that objects of the class cannot or should not be cloned. Once a class in a superclass chain (or superinterface hierarchy) has declared clone with no checked exceptions, no descendent class (or interface) may add it back in. This would provide a useful "out" for classes that inherit a working clone method but are unable to provide one themselves.

However, for better or for worse, this avenue has been largely closed off by the classes in the Java API; of the 40-odd Java 2 API classes that override the clonemethod, none declare any checked exception. Should these clone methods detect the impossible situation of catching a CloneNotSupportedException exception from a superclass, the methods raise an internal error* just as the Point class does:


public class Vector extends AbstractList implements List, Cloneable,
   java.io.Serializable {
 public synchronized Object clone() {
    try { 
      Vector v = (Vector) super.clone();
      v.elementData = new Object[elementCount];
      System.arraycopy(elementData, 0, v.elementData, 0, elementCount);
      v.modCount = 0;
      return v;
    } catch (CloneNotSupportedException e) { 
      // this shouldn't happen, since we are Cloneable
      throw new InternalError(e.toString());
    }
  }
  // ...
}
A class that extends Vector, for example, cannot declare a clone method that throws a CloneNotSupportedException exception should it wish to forcibly prohibit cloning of its instances; classes that extend any of those from the Java API will need a different technique for aborting attempted cloning operations. Rather than use one technique for dealing with subclasses of Java API classes and another for your own application classes, it is simpler to follow the lead of the Java API and use one consistent approach for both: Catch and absorb the exception at the first possible opportunity.

Maxim 27: If a clone Method Catches the CloneNotSupportedException Exception, Its Class Should Explicitly Implement Cloneable
A collateral benefit of this convention is that the try-catch block in a class's clone method also indicates whether the class should implement the Cloneable interface.

Java will not allow a method to attempt to catch a checked exception that is not declared by any of the methods called in the try block. Any attempt to catch a CloneNotSupportedException from a clone method that has not declared it in its throws clause will be rejected at compile time as an error.

If the exception has already been caught by some superclass's clone method, then that class assumed responsibility for implementing the Cloneable interface; without it, the clone method it defined would be a sure-fire failure.

If Maxim 26 is applied to your own classes (as it has been to Java API's classes), the presence of a try-catch block in a clone method to catch (and ignore) CloneNotSupportedException exceptions indicates the class should explicitly implement Cloneable. If the exception handling rigmarole is not required (or, indeed, permitted), then a superclass or superinterface has already looked after the problem and your class need not.

Maxim 28: Throw a RuntimeException to Really Disable Cloning
The rule of always declaring clone methods with no checked exceptions appears to leave us at an impasse when a class wishes to explicitly disallow cloning of its instances, but the Java API technique of raising an internal error suggests an interesting solution.

The developers of the Java API have resolved this difficulty by essentially "unchecking" the exception. The exception is encapsulated in an InternalError object, which, unlike CloneNotSupportedException, is an unchecked exception and slips through the compile-time checking.

Sometimes it is desirable to disable cloning in a subclass, perhaps because the complexity of its implementation is not justified by the likelihood it will ever be required. In other cases, providing a working clone method may simply be impossible. A case in point would be any subclass of the java.util.Observable class. Since the clone method of Observable produces booby-trapped clones, no "observable" subclass can fabricate reliable clones either.2

Whatever the motivation for disabling cloning for a particular class, if a superclass or superinterface has defined a clone method with no checked exceptions, then the avenue of simply throwing a CloneNotSupportedException exception to abort attempted cloning operations has been cut off. However, where you wish to disable cloning in a particular subclass variant, you may still throw an exception, so long as it is a subclass of the RuntimeException exception class (making it an unchecked exception).

An InternalError object is suitable for handling the "impossible" situation within the clone method, but where you wish to prohibit cloning operations by throwing an exception out of the method, a special-purpose class is a good idea. It can provide a more precise indication of why the cloning operation was not performed than a vague and misleading "internal error":


public class CannotCloneObjectException extends RuntimeException {
}
Exception classes that subclass RuntimeException are exempt from the usual compile-time checks, which prevent exception objects from being propagated indiscriminately from method to method; so, an object of this exception class can be unconditionally thrown by the clone method of any class that does not support cloning.


class UnclonableClass extends ClonableClass {

  public Object clone() throws CannotCloneObjectException {
    // this subclass may never be cloned
    throw new CannotCloneObjectException();
  }
}
Despite being unchecked, CannotCloneObjectException exceptions can still be caught in application code and handled in a controlled manner. The exception need not appear in the method's throws clause, but it makes for good documentation. A javadoc comment to the same effect is a good idea too:


/** Objects of this class may not be cloned.
    @throws CannotCloneObjectException Unconditionally thrown.
*/
public Object clone() throws CannotCloneObjectException {
Unchecked exceptions are not the natural mode of Java's exception handling model; i.e., RuntimeException is a subclass (and special case) of the more general Exception class. The motivation behind the provision of unchecked alternatives was that "having to declare such [runtime] exceptions would not aid significantly in establishing the correctness of Java programs."3

However, the provision of this facility does open the door to implementing unfamiliar models of exception handling in Java, which turn the role of unchecked exceptions to a different task.

Maxim 29: Consider the Unchecked Model of Exception Handling for
Large-Scale Systems Design

When systems are comparatively small (less than 50,000 lines), controlling the transmission of exceptions explicitly through a throws clause may be quite manageable. Each method is made clear about which exceptions may cause its abrupt termination, and it may pass the exception on to its caller until a method is encountered that can enact the appropriate response.

As systems grow in size, the need for centralization increases. Checked exceptions do not support the centralization of error handling at a single point unless every method is complicit in the knowledge of which exceptions may be thrown at the lowest levels. Apart from the nuisance value of maintaining the list of checked exceptions in these methods' throws clauses, the design of some participating classes may prohibit them from passing the exceptions along.

For example, consider a ServerWatchdog class, an implementation of the Observer interface. This class is responsible for alerting the system that the server it has been observing is out of service, and no further transaction with it may be initiated. Because of the severity of the condition, all bets are off; the program must terminate its current activities and either exit or attempt to reconnect with the server. Ideally, the watchdog could throw an exception and the program could catch it at some high level.

Unfortunately, the update method of the Observer interface, implemented by the ServerWatchdog class, cannot throw a checked exception because the method is declared without a throws clause:


class ServerWatchdog implements Observer {
  public void update(Observable o, Object arg) {
    if (((Server) o).offline()) {
      // unchecked exception
      throw new ServerOfflineException();
    }
  }
}
Unchecked exceptions (subclasses of RuntimeException) provide a means to throw an exception at any arbitrary point—to be caught higher up—without any intervening method needing to know that it passed it on.

For robust, mission-critical applications (particularly servlets), it is not acceptable to simply ignore runtime exceptions. For example, NullPointerException and IndexOutOfBounds exceptions, both unchecked exceptions, should not cause the server to exit with a console stack-trace message. Programs like these should catch all runtime exceptions, perform whatever actions need to be taken to rollback outstanding transactions, log the condition to an operator log, and attempt to continue providing service. Runtime exceptions of the application variety should receive the same treatment.

This mode of exception handling is the default one in C++.§ It is a model that allows exceptions to be thrown at any point in the program without prior warning. It allows low-level routines to be expanded in scope and handle new sources of input without requiring their consumers to be modified also. A large C++ system typically centralizes the handling of exceptions at several points, with the most serious conditions being handled at higher levels and less severe conditions being handled closer to the source. This is an ideal strategy for large systems development, and one, with the aid of runtime exceptions, that may be applied to the development of large Java systems. A method that prompts the user for his/her login name should not need to know that the user may be attempting to log in remotely; the task of responding to a network failure is not one in which it should become involved. Neither, probably, is its calling method; this task is for a higher level method. Runtime exceptions allow for this possibility.

Driving a Screw With a Hammer
The protection offered by the compile-time checking of checked exceptions comes at a cost. Where exceptions are passed from one class's methods to another's, and particularly when the "distance" between the origin of an exception and the most convenient place to catch and handle it is great, the management of the throws clauses can become a maintenance nightmare. The scheme may simply be infeasible for large-scale systems development.

If any method in the chain fails to declare the exception, the exception object's safe passage from its birthplace to its resting place is roadblocked. On the other hand, an exception may need to pass through many (possibly completely unrelated) classes on its way to the method best equipped to respond to it; in large systems, the maintenance of the throws clauses can become a significant impediment to systems development and evolution.

Unchecked exceptions are free from the constraint of needing to be declared (and therefore anticipated). They support a mode of exception handling that is more familiar to C++ programmers than Java programmers, but one whose suitability increases with the size of the system being developed.

The program designs and top-level architectural forms that are appropriate for small to medium projects may be far from ideal for the development of large systems. Java's default exception handling model works well for simple applications, but for the development of large or fault-tolerant systems, mixed modes of exception strategies may be the right tools for the task.

References

  1. Ball, S., "Solutions for implementing dependable clone methods," Java Report, Vol. 5, No. 4, April 2000, pp. 68–72,82.
  2. Ball, S., "Effective cloning," Java Report, Vol. 5, No. 1, Jan. 2000, pp. 60–67.
  3. Gosling, J., B. Joy, and G. Steele, The Java Language Specification, Addison–Wesley, 1996, 11.2.2.
FOOTNOTES
* An exception of some kind must be thrown, as the compiler will not allow the catch block to simply be empty—the method would contain a path of execution where it could fall off the bottom of the method without supplying a return value.

And compile-time checking is too; there is no runtime enforcement made by the JVM. An alternative to raising an internal error would have been to simply circumvent the compiler's checks at runtime and throw the forbidden checked exception with the expression Thread.currentThread().stop(e). However, as the Thread.stop method has been deprecated in Java 2, this once useful technique should now be avoided.

An (unchecked) exception thrown by the update method of a class implementing the Observer interface is thrown in the thread that invoked the notifyObservers method for the Observable object. That is, the calling of the update methods of all registered observers is performed synchronously within the notifyObservers method.

§ Although C++ supports exception specifications (the equivalent of throws clauses), the meaning of a missing exception specification indicates that the function may throw an exception of any kind. Because C++ strictly enforces that a function will not throw an exception it has not declared (unlike Java, which uses only compile-time checking), the exception specifications are rendered virtually useless and are rarely seen outside of tutorial example code.

Featured

Most   Popular
Upcoming Events

AppTrends

Sign up for our newsletter.

Terms and Privacy Policy consent

I agree to this site's Privacy Policy.