Senior Project Design
Reliability with Assertions

Course Map

J2SE (Java 2 Platform, Standard Edition) 1.4 adds a simple assertion facility to Java. At the simplest level, an assertion checks a boolean-typed expression that a developer specifically proclaims must be true during program runtime execution. To support the new assertion facility in J2SE 1.4, the Java platform adds the keyword assert to the language, an AssertionError class, and a few additional methods to java.lang.ClassLoader.

Assertion is a boolean expression that a developer specifically proclaims to be true during program runtime execution. The simple idea of using assertions can have an unexpected influence on a software program's design and implementation.

 

Agenda


Declaring Assertions

You declare assertions with a Java language keyword, assert. An assert statement has two permissible forms:

  1. assert expression1;
  2. assert expression1 : expression2;

In each form, expression1 is the boolean-typed expression being asserted. The expression represents a program condition that the developer specifically proclaims must be true during program execution. In the second form, expression2 provides a means of passing a String message to the assertion facility. The following are a few examples of the first form:

  1. assert 0 < value;
  2. assert ref != null;
  3. assert count == (oldCount + 1);
  4. assert ref.m1(parm);


As an example of using assertions, class Foo listed below contains a simple assertion in the method m1(int):

public class Foo {
    public void m1( int value )  {
       assert <give any boolean expression of what you want to assert>
       System.out.println( "OK" );
    }

public static void main( String[] args )  {
    Foo foo = new Foo();
    System.out.print( "foo.m1(  1 ): " );
    foo.m1( 1 );
    System.out.print( "foo.m1( -1 ): " );
    foo.m1( -1 );
  }
} 

Since assert is a new Java keyword, to see this example in action, you must compile the class with a J2SE 1.4-compliant compiler. Furthermore, the compiler requires a command-line option, -source 1.4, to signal source compilation using the assertion facility.

javac -source 1.4 Foo.java

Enabling Assertions

Command-line options to the java command allow enabling or disabling assertions down to the individual class level. The command-line switch -enableassertions, or -ea for short, enables assertions. The switch has the following permissible forms:

  1. -ea                   ---> enables assertions in all classes except system classes.
  2. -ea:<class name>      ---> turns on assertions for the named class only.
  3. -ea:...               ---> enable assertions for for the default, or unnamed package.
  4. -ea:<package name>... ---> enables assertions for the specified package name and all pkgs under.
  5. -esa                         --->  enables assertions for system classes.


The following shows the resulting output:

foo.m1(  1 ): OK
foo.m1( -1 ): Exception in thread "main" java.lang.AssertionError
        at Foo.m1(Foo.java:6)
        at Foo.main(Foo.java:17)

java.lang.AssertionError

The assertion facility adds the class AssertionError to the java.lang package. AssertionError contains a default constructor and seven single-parameter constructors. The assert statement's single-expression form uses the default constructor, whereas the two-expression form uses one of the seven single-parameter constructors.

To understand which AssertionError constructor is used, consider how assertions are processed when enabled:

Evaluate expression1

So the first form of assertions does not give any information about the error. Hence nor recommended.
Excerise
: Modify the previous code to use assertion of second form.

Robustness and Correctness

Robustness

Correctness

Be assertive :  Assertions clearly and definitively document program expectation for normal execution. The clearer, the better.

Using Assertions for Design by Contract (DBC)

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

  1. Preconditions: conditions that must be true when entering a method - assert precondition();
  2. Postconditions: conditions that must be true when exiting a method - assert postcondition();
  3. Invariants: conditions that must be true between all method calls (upon entry to the method and between leaving the method). An effective invariant would be defined as a method, e.g. invariant() invoked after construction, and at the beginning and end of each method: assert invariant();
Example
public void setSampleRate( int rate ) {
    this.rate = rate;
}
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.
  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;
  }

  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 named SensorException in place of the previously unchecked IllegalArgumentException:

//PROVIDER
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;
  }
//CLIENT

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

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.

The million-dollar question: what is the sensible thing do?
The developer could perhaps, check the value, realize it was out of range, and attempt to gracefully handle the situation.

why wait for a thrown exception before performing such checks?
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;
  }
Assertion throws AssertionException, which is a RuntimeException like IllegalArgumentException.
So what's the difference?
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.

Relaxing DBC

Although he emphasizes the importance of being able to express preconditions, postconditions, and invariants, and the value of using these during development, Bertrand Meyer admits that it is not always practical to include all DBC code in a shipping product. You may relax DBC checking based on the amount of trust you can place in the code at a particular point.

Here is the order of relaxation, from safest to least safe:

  1. The invariant check at the beginning of each method may be disabled first, since the invariant check at the end of each method will guarantee that the object’s state will be valid at the beginning of every method call. That is, you can generally trust that the state of the object will not change between method calls. This one is such a safe assumption that you might choose to write code with invariant checks only at the end.
  2. The postcondition check may be disabled next, if you have reasonable unit testing that verifies that your methods are returning appropriate values. Since the invariant check is watching the state of the object, the postcondition check is only validating the results of the calculation during the method, and therefore may be discarded in favor of unit testing. The unit testing will not be as safe as a run-time postcondition check, but it may be enough, especially if you have enough confidence in the code.
  3. The invariant check at the end of a method call may be disabled if you have enough certainty that the method body does not put the object into an invalid state. It may be possible to verify this with white-box unit testing (that is, unit tests that have access to private fields, so they may validate the object state). Thus, although it may not be quite as robust as calls to invariant( ), it is possible to “migrate” the invariant checking from run-time tests to build-time tests (via unit testing), just as with postconditions.
  4. Finally, as a last resort you may disable precondition checks. This is the least safe and least advisable thing to do, because although you know and have control over your own code, you have no control over what arguments the client may pass to a method. However, in a situation where (a) performance is desperately needed and profiling has pointed at precondition checks as a bottleneck and (b) you have some kind of reasonable assurance that the client will not violate preconditions (as in the case where you’ve written the client code yourself) it may be acceptable to disable precondition checks.

You shouldn’t remove the code that performs the checks described here as you disable the checks. If a bug is discovered, you’ll want to easily turn on the checks so that you can rapidly discover the problem.

Example: DBC + White-Box unit Testing

A circular array - an array used in a circular fashion. When the end of the array is reached, the class wraps back around to the beginning.

We can make a number of contractual definitions for this queue:

  1. Precondition (for a put( )): Null elements are not allowed to be added to the queue.
  2. Precondition (for a put( )): It is illegal to put elements into a full queue.
  3. Precondition (for a get( )): It is illegal to try to get elements from an empty queue.
  4. Postcondition (for a get( )): Null elements cannot be produced from the array.
  5. Invariant: The region in the array that contains objects cannot contain any null elements.
  6. Invariant: The region in the array that doesn’t contain objects must have only null values.

Queue.java

Conclusions

Assertions are a welcome addition to the Java programming language.