Newsletter sign-up
View all newsletters

Enterprise Java Newsletter
Stay up to date on the latest tutorials and Java community news posted on JavaWorld

Sponsored Links

Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs

J2SE 1.4 premieres Java's assertion capabilities, Part 2

Understand the methodology impact of Java's new assertion facility

  • Print
  • Feedback

Page 4 of 5

To form a software contract, DBC identifies three common uses for assertions:

  1. Preconditions: conditions that must be true when entering a method
  2. Postconditions: conditions that must be true when exiting a method
  3. Invariants: conditions that must be true between all method calls


Java's new assertion facility can and should be used for all three cases. Of particular interest is the use of assertions for preconditions. In Java development, such checks have, by convention, been performed using the exception-handling facility. Now that Java has an assertion facility, we should rethink that convention.

As an example for investigating the issues of using exceptions to check preconditions, consider the following method for setting the sample rate in a class called Sensor:

  public void setSampleRate( int rate )
  {
    this.rate = rate;
  }


The method simply sets the Sensor rate to the value passed as the rate argument. As implemented, setSampleRate() contains no safeguards for preventing the sample rate from being set to a meaningless or possibly harmful value. Suppose in the Sensor class the unit of measure for the variable rate is Hertz. As an engineering unit, Hertz cannot be negative, so the setSampleRate() method should not set the sample rate to a negative value. Furthermore, sampling a sensor at too high a frequency could prove damaging. The following version of setSampleRate() uses an IllegalArgumentException to restrict the sample rate's setting:

  public void setSampleRate( int rate )
    throws IllegalArgumentException
  {
    if( rate < MIN_HERTZ  ||  MAX_HERTZ < rate )
      throw new IllegalArgumentException
        ( "Illegal rate: " + rate + " Hz is outside of range [ " +
          MIN_HERTZ + ", " + MAX_HERTZ + " ]" );
    this.rate = rate;
  }


Providing safeguards on the sample rate's permissible values is unquestionably good programming practice. Using exceptions as the enforcing mechanism, however, is questionable. Shift focus from the method supplier to a client object calling the method. Since IllegalArgumentException is an unchecked exception, the client can call the method without using a try/catch block. That is, the client can easily ignore the thrown exception and possibly unwittingly so if the client developer overlooks the throws clause in the supplier's method documentation. More commonly, developers can see the exception, think to themselves, "Well, I won't do that," and blithely omit a cumbersome try/catch construct.

Consider what happens if the client chooses to ignore the exception, which is actually quite common for unchecked exceptions. An attempt to set the rate outside the permissible range results in an uncaught IllegalArgumentException percolating to the top of the runtime stack. For example, if MIN_HERTZ=1 and MAX_HERTZ=60, the call setSampleRate( 100 ) causes the system to halt with the message:

  Exception in thread "main" java.lang.IllegalArgumentException: Illegal
   rate: 100 Hz is outside of range [ 1, 60 ]
          at tmp.Sensor.setSampleRate(Sensor.java:9)
          at tmp.Sensor.main(Sensor.java:20)


One solution for preventing this type of client developer neglect is to change the thrown exception to a checked exception. The following setSampleRate() method uses a supplier-defined checked exception named SensorException in place of the previously unchecked IllegalArgumentException:

  public void setSampleRate( int rate )
    throws SensorException
  {
    if( rate < MIN_HERTZ  ||  MAX_HERTZ < rate )
      throw new SensorException
        ( "Illegal rate: " + rate + " Hz is outside of range [ " +
          MIN_HERTZ + ", " + MAX_HERTZ + " ]" );
    this.rate = rate;
  }


The client developer now must heed the exception documentation and wrap the call in a try/catch clause. Although the supplier has forced the client to deal with the exception, nothing prevents the client developer from lazily implementing the try/catch clause as:

  try
  {
    sensor.setSampleRate( 100 );
  }
  catch( SensorException se )
  {}


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.

  • Print
  • Feedback

Resources
  • Recent JavaWorld articles on exception handling:
  • Browse our Topical Index for more stories on the Java 2 Platform, Standard Edition: http://www.javaworld.com/channel_content/jw-j2se-index.shtml
  • Read more JavaWorld articles by Wm. Paul Rogers