J2SE 1.4 premieres Java's assertion capabilities, Part 2

Understand the methodology impact of Java's new assertion facility

Assertions are a fairly simple construct added to the soon-to-be released J2SE (Java 2 Platform, Standard Edition) 1.4. As a basic description, an assertion specifies a boolean-typed expression a developer explicitly demands must be true at a specific point of program execution. In Part 1 of this two-part series, I covered the mechanics of using the new J2SE assertion facility. This article discusses the methodology of using assertions and explores the assertion facility's ramifications on Java design and implementation.

Read the whole series on J2SE 1.4's assertion capabilities:

Though a simple construct, assertions have broad implications on the approach to writing solid Java programs. Developers might aspire to create right programs, but attaining such an elusive and subjective quality actually proves quite difficult. What exactly is right, and what measures or metrics determine rightness? Those questions, of course, have no definitive answer, but the software engineering community does recognize and discuss software quality attributes. One such attribute is software reliability, and many of software engineering's best practices take direct aim at improving software products' reliability. In this article, I show how assertions deal with the reliability aspect known as correctness, which complements another reliability aspect, robustness. I also show the Java assertion facility to be but a small step toward a more complete and formal approach to reliability in software development known as Design by Contract.

Robustness and correctness

Reliability ranks as a highly desirable trait in many products. I expect my car to provide reliable transportation. I expect my raincoat to keep me reliably dry in a pouring rain. And, yes, I expect the software I use to be reliable as well.

But what exactly is reliable software? Software engineering texts define reliability as the probability of a system operating without failure over a specified time under a specified set of conditions. That, unfortunately, comes across as pedantic. Users typically consider reliability to mean the software does what it's supposed to do without crashing. That, unfortunately, is quite subjective, just the type of characteristic engineers find an anathema, which explains why they fashioned the "over a specified time under a specified set of conditions" clause. That clause attempts to create a measurable event, and engineers find comfort in measurable events.

A middle ground accepts that users expectations are subjective, but still strives to produce reliable software. Although subjective, reliability can be dealt with objectively. Reliability can be categorized by two broad strokes: robustness and correctness. Robustness pertains to a system's ability to reasonably react to a wide variety of circumstances and possibly unexpected conditions. Correctness pertains to a system's adherence to an explicit or external specification.

Java's exception-handling facility addresses robustness. Exceptions provide a structured means of handling unusual circumstances during program execution. Specifically, the exception facility allows explicitly noting exceptional conditions and provides a mechanism for handling such exceptional conditions in specific code blocks. Java draws praise for building exceptions into the base language.

Correctness addresses a slightly different reliability concern. Whereas exceptions facilitate robustness through an ability to recover gracefully from a range of exceptional conditions, correctness deals with ensuring a program does the right thing during normal program flow. Since correctness pertains to normal conditions, Java's exception-handling facilities do not readily assist correct program creation.

For example, a system specification might declare that a user can load a local configuration file. The specification might not, however, detail the steps to take if the file has the wrong format. As a robustness technique, the system could catch this exceptional condition, notify the user of the error, and allow the user to choose another file. Having chosen a correctly formatted configuration file, program correctness ensures proper file processing. That is, the program behaves correctly by successfully reading the specified file format; it behaves robustly by gracefully handling attempts to read the wrong file format.

So if exceptions don't facilitate correctness, what does? Enter assertions. Through a simple programming language construct, assertions allow explicit declarations of program correctness. Assertions are boolean-typed expressions that must be true during normal program execution. Viewed in this manner, assertions provide a series of checkpoints tied together by program language statements that move the system between consistent program states.

Whoa, wait a moment! That sounds like formal mathematical logic designed to prove program correctness. Engineering is not a mathematical absolute, but an active process of juggling reasonable tradeoffs imposed by constraints such as time-to-market, total cost, execution speed, ease-of-use, and the myriad of other details that make software development a profession for the stout of heart. Engineers seek reasonable solutions, not perfect solutions. Of course, if the perfect solution is reasonable, so be it; but seldom is that the case.

Although assertions entered the software engineering canon through the mathematical study of proving program correctness, assertions in a less theoretical setting provide valuable engineering assistance in building reliable software. Assertions enforce valid runtime state at discrete checkpoints in an executing system. Perhaps just as importantly, assertions explicitly declare developer intent in the program text itself. Through assertions, developers can definitively mark the boundaries of correct program execution versus robust program execution, and provide valuable clues into expected system behavior.

Unfortunately, Java's assertion facility does not mesh with the standard documentation system as closely as the exception facility. The Javadoc system includes information regarding all throws clauses declared by a method. Assertions do not draw such direct attention. This is certainly sensible for assertions in general, but rather unfortunate when using assertions to check the validity of input arguments to a public method. Though using assertions to check input arguments contends with the current Java convention of using exceptions for such checks, I argue below that assertions are more appropriate. Before entertaining that argument, I state a pedagogic point in using assertions.

Be assertive

A first point in using assertions initially appears tautological: be assertive with assertions. Assertions are often described as something a programmer believes to be true during program execution. Believes is not strong enough. Proclaims sounds nice, but in actual fact, demands is more appropriate. Assertions help define the boundaries of correct system behavior, and as such, warrant a strong, consistent approach.

This point is primarily a psychological issue. The boolean-typed expressions used in assertions are programmatically definitive. There is no maybe state for a Boolean value. Maybe creeps in when a developer ponders, "Maybe I should use an assertion here, but I don't want to be too restrictive." If you expect a condition to be true at a certain point of program execution, then assert that condition. Assertions clearly and definitively document program expectation for normal execution. The clearer, the better.

Challenge convention

Convention holds that you should use Java's exception-handling facility to ensure the validity of input arguments to a public method. Conventions are established for many reasons, particularly for necessity. Through the first three major Java releases, exceptions provided the only language mechanism for dealing with illegal arguments passed to a method. With the addition of the assertion facility, however, this convention should be revisited and scrapped.

To argue the issue, I'll use the jargon of Design by Contract (DBC). There are many excellent resources on DBC, so I won't attempt to explain the concepts in detail. (A good place to start is "Applying 'Design by Contract'" (IEEE Computer, October 1992) by Bertrand Meyer, who introduced DBC (no URL available for this article).)

Central to DBC is the notion of a contract between client and supplier. The interaction between software classes is viewed as analogous to a contract between two legal entities, each of which assumes specific responsibilities in exchange for certain expectations. For example, I might contract with a painting service to paint my house. The service assumes the responsibility to paint the house, and I assume the responsibility to pay for the service. I expect to have my house painted, and the painting service expects to be paid. There is a clear connection between expectation and responsibility.

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 )
  {}
1 2 3 Page 1