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 )
  {}

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.

Source: www.infoworld.com