J2SE 1.4 premieres Java's assertion capabilities, Part 2

Understand the methodology impact of Java's new assertion facility

1 2 3 Page 2
Page 2 of 3

Although the supplier can't assume responsibility for the client's lack of effort, the above code is nonetheless troublesome. Sure, the call to setSampleRate( 100 ) doesn't set the sample rate to an invalid value, but neither does it sensibly report the attempt. The sample rate is unchanged, and program execution blithely continues, presumably with fingers crossed.

Now let's consider the worthy client developer faced with dutifully catching SensorException. Suppose the developer sketches the setSampleRate() call as follows:

  try
  {
    sensor.setSampleRate( rate );
  }
  catch( SensorException se )
  {
    // Do something sensible.
  }

The million-dollar question: what is the sensible thing do? Recall that exceptions facilitate handling unusual circumstances during program execution. The developer could ponder what was unusual about the value of the variable rate passed to the setSampleRate() method. The developer could then, perhaps, check the value, realize it was out of range, and attempt to gracefully handle the situation.

But this begs the question: why wait for a thrown exception before performing such checks? The developer shouldn't handle this condition in the catch block, but rather before the setSampleRate() call. The unusual condition should be considered setting the variable rate to an invalid value, not the call to setSampleRate() with invalid input. Such problems are correctly handled as close to the error source as possible.

So if the developer doesn't check the variable rate's value in the catch block, what should be done? The developer should question using the exception facility to handle a program correctness issue. During the catch block execution, it is simply too late to do anything sensible.

As an alternative, the following supplier code replaces the previous use of exceptions with an assertion:

  public void setSampleRate( int rate )
  {
    assert MIN_HERTZ <= rate  &&  rate <= MAX_HERTZ :
      "Illegal rate: " + rate + " Hz is outside of range [ " +
      MIN_HERTZ + ", " + MAX_HERTZ + " ]";
    this.rate = rate;
  }

On the surface, a client's use of this solution resembles setSampleRate()'s first version, which threw an unchecked IllegalArgumentException. Since Java's assertion facility does not provide a means for catching the assertion, the client developer needn't call setSampleRate() within a try block. There is, however, a significant philosophical shift in responsibility. Calling setSampleRate() with an invalid input is no longer documented or handled as an unusual condition, but as an incorrect condition. Client code can no longer mask an incorrect call to setSampleRate() with a no-op catch block. Having used an assertion, incorrect calls to setSampleRate() are now dutifully reported through the Java error-handling mechanism. Yes, Java's assertion facility can be disabled at runtime, but that's not really under the control of the client developer, who cannot now lazily or unwittingly use the supplier code incorrectly.

Tough love and the end to double duty

Asserting a method's preconditions provides a dose of tough love. Initially the practice can seem too harsh, but with system reliability at stake, a dose of harsh reality can prove quite effective. Preconditions assist in clearly and definitively drawing the boundaries of responsibility for proper object interactions. Static type checking performed at compile time acts similarly. Through rigid type conformance enforcement, the compiler raises issues of incorrect object interaction as early as possible. But static type checking can't catch all error classes, and preconditions add another layer of valuable development assistance. Though the incorrect object interaction isn't discovered at compile time, importantly, with assertions enabled, the interaction isn't quietly swept under a rug.

As for being too harsh or rigid, consider the earlier house-painting service example. Suppose I contracted to have my house painted desert sage. How should I react if I came home to discover the service painted my neighbor's house fuchsia instead? Would it make sense to pay the service and continue to live in a house in need of paint next to a fuming neighbor? I certainly wouldn't react that way. Similarly, if I discover a violation in terms of contract between client and service objects in a software system, I don't silently forgive the client and scold the service.

Though I can't anticipate all objections to changing a long-standing Java convention, I can preemptively comment on the following common arguments for using exceptions to check method preconditions:

Defensive programming

Defensive programming attempts to bulletproof code against unexpected input. Using assertions rather than exceptions does not alter or violate this defensive programming tenet, the goal of which is robustness, not correctness. Exceptions and assertions play complementary roles, not competitive ones. If you need to provide a robust mechanism for inputting a method parameter, then do so before the method call. In the example explored above, the client developer should ensure variable rate's validity before calling setSampleRate(). Validating the rate after receiving an exception is simply too late. At that point, the client code performs the same checks in the catch block as the supplier performs in the setSampleRate() method. Using exceptions to ensure program correctness leads to duplicate effort and duplicate code, which in turn leads to increased testing and a program that works twice as hard.

Use a meaningful exception

Since Java's exception-handling facility utilizes a system's type hierarchy, exceptions do provide a means of conveying exceptional condition information within the name of the exception itself. For example, setSampleRate() alternatively used an IllegalArgumentException and a SensorException to signal that an exceptional condition occurred. However, short of extending the granularity of exception classes to a ridiculous degree, meaningful exception information must still reside in the exception class itself, typically in the String message maintained by the base exception object, java.lang.Throwable.

As a further example, consider a method that takes two int arguments. Throwing an unadorned IllegalArgumentException from such a method provides scant information to the client. Which argument is illegal? And why? Since Java's assertion facility provides an assert statement format for specifying a string message, assertions are just as useful as exceptions for conveying detailed error information.

Assertions can be disabled at runtime

As previously noted, a client developer can render exceptions ineffective by implementing a no-op catch clause, thereby locally disabling the exception. So the fact that assertions can be disabled at runtime does not increase system exposure to failure. In fact, in each case, look who has erred and who has control during a precondition violation. Using exceptions, the client is in error and there is no control for exposing the client's behavior. Using assertions, the client is in error and the error can be exposed at runtime. Either way, the client is in error. Assertions merely provide a means of exposing the incorrect condition.

Not Design by Contract

Though Java's new assertion facility is a welcome language addition, it is far from a complete manifestation of DBC. Interestingly, the last remaining specification for Oak, Java's precursor, contains a section on a nascent DBC form. Gosling ripped the section out under schedule pressure to move Java out of the lab and onto Internet developers' computers. The Oak specification shows assertions of the form:

  • int month assert( month >= 1 && month <= 12 );
    
  • Element pop() {
      precondition: !empty();
      /* . . . */
      postcondition: !full();
    

The first form declares a class invariant for the variable month. The other form explicitly declares a precondition and a postcondition. The specification also notes that preconditions and postconditions are inherited and cannot be restricted or redefined in subclasses.

These glimpses at Gosling's early thoughts on including assertions in Java reveal a striking difference between DBC and the Java assertion facility added to J2SE 1.4. In DBC, assertions provide the bedrock on which the DBC method is built. DBC specifies various assertion types and the mechanism for inheriting assertions in subclasses. By specifying distinct types of assertions, DBC facilitates the selective runtime to enable or disable different assertion types. For example, the Eiffel system enables only preconditions by default. Contrast that to the Java convention of not using assertions to check preconditions! The subdivision also allows different assertion types to be incorporated into the standard documentation system. For example, were preconditions a separate assertion type in Java, the Javadoc system could easily include valuable precondition information in the method description.

As previously mentioned, I defer to other resources and don't provide any detailed discussion of the DBC method. But suffice it to say, the new Java assertion facility is a positive step in helping developers create reliable programs, but a far cry short of DBC.

Assert this

Assertions are a welcome addition to the Java programming language. For the first time, the language supports a developer distinguishing between the reliability issues of robustness and correctness. As a robustness technique, Java exceptions facilitate handling unexpected or unusual conditions. Java assertions, on the other hand, enable the explicit declaration of acceptable program state as a correctness technique. Prior to the introduction of assertions, both robustness and correctness techniques necessitated the use of the Java exception facility.

Preconditions, conditions that must be true upon entering a method, are issues of correctness, not robustness. Correspondingly, preconditions are best handled via the assertion facility. This can seem harsh at first, but is actually a dose of tough love. As with parenting or teaching, you should not confuse the granting of leniency in exceptional circumstances with the misguided leniency of being nice all the time. A dose of discipline in the form of assertions provides valuable assistance in defining and maintaining the boundaries necessary to create reliable systems.

Although certainly welcomed, assertions do not bring Design by Contract to Java. Assertions merely provide the bedrock on which DBC is built. Missing are the facilities to distinguish between important assertion uses and a mechanism to allow the inheritance of assertions in subclasses. Only time will tell how valuable Java's limited assertion capability will prove to developers.

Wm. Paul Rogers is a Java/object-oriented architect whose interests include teaching an understanding of Java design and implementation through stressing the first principles of fundamental object-oriented programming. He began using Java in the fall of 1995 in support of oceanographic studies conducted at the Monterey Bay Aquarium Research Institute, where he led the charge in utilizing new technologies to expand the possibilities of ocean science research. Paul has been using object-oriented methods for 10 years and works as an independent consultant in Bellingham, Wash., where he also teaches computer science at Western Washington University.
1 2 3 Page 2
Page 2 of 3