Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

JVM Languages

Implementing Assertions for Java


Jeffery is president and CEO of Reliable Software Technologies (RST). Michael and Matthew are developers in RST's research division. The authors can be contacted at http://www.rstcorp.com/.


Sidebar: Design by Contract

Debugging is hard work. Even more frustrating is when the bug turns out to be something that could easily have been prevented or detected. Perhaps you misunderstood the specification or design. Maybe your testing wasn't adequate. Or maybe you just weren't thinking clearly at one a.m. before that big delivery deadline. Regardless, you wasted hours or days tracking down a problem that should never have existed. How can you avoid this in the future?

Software assertions combat this problem. Assertions are Boolean expressions that define the correct state of your program at particular locations in the code. Think of them as watchdogs that check method calls for proper invocation, method code for correct computation, class data states for consistency, and individual statements for errors.

The format of an assertion typically looks like <assertion_type>(<condition>, <message>);, where <assertion_type> indicates the purpose of the assertion, <condition> indicates the Boolean expression evaluated to determine whether the assertion has been violated, and <message> indicates what information should be output if the assertion is violated.

There are many assertion types, including:

  • Preconditions, which define the conditions that must hold when calling a particular method. Preconditions for a method are evaluated at the entry point of the method.
  • Postconditions, which define precisely what a method does. Postconditions for a method are evaluated at all exit points of the method.
  • Invariants, which define state-space consistency for a class. Invariants are evaluated at the entry and exit points of all methods in a class.
  • Data assertions, which define the conditions that must hold at a particular location in your code. Data assertions are evaluated at the location they are placed in the code.

Software assertions can assist you in finding bugs earlier in the process (when they are easier to fix). As developers move toward object-oriented design and component reuse, concepts such as design by contract (see the accompanying textbox entitled "Design by Contract") use assertions to assure proper implementation of classes, component interface, and internal data states. Many believe that assertion technology is the key to designing for testability -- the holy grail of software quality. Perhaps these checks can be used to move toward built-in self-test (bist) as a way of automatically checking the consistency and correctness of our implementations.

Unfortunately, the developers of Java did not include assertion capabilities in the language. We are thus left to roll our own assertion capabilities in order to use design by contract and assertions effectively. The developers of Java also decided not to include a preprocessing capability in the language. Since many assertion capabilities developed for other languages have used their preprocessors to automatically enable/disable the assertions, this makes providing support for assertions in Java more difficult.

In this article, we'll discuss the issues associated with building assertion capabilities for Java. We'll also discuss the ideal Java assertion capability and our implementation of this ideal. Finally, we'll present a basic assertion class that you can use.

Ideal Java Assertion Capabilities

Assertions should be powerful, easy to write, easy to read, and easy to use. The language used to specify assertion statements must provide a simple means of expressing complex Boolean conditions. It must be easy for the programmer or tester to write and understand these expressions. A framework that allows users to easily manage assertion statements in source code must exist.

Part of an assertion tool's usefulness will come from the ability to configure the tool to the task at hand. Users may require different capabilities during development phases of a project than during testing or release phases. One example of this is the ability to decide which assertions should be evaluated by the program. Preconditions, postconditions, invariants, and data state assertions should be capable of being toggled on or off independently. Users should have the choice of making this decision at either compile time or run time. Additionally, users should have the option of having any or all of the types of assertion statements omitted from compilation.

A good default behavior for when an assertion fails is to terminate the program. When an assertion has failed, it means that the program has entered an incorrect state and may no longer function properly. However, this does not necessarily have to result in the termination of a program. Ending the program is just one of several useful behaviors that can be built into an assertion tool. Other behaviors include putting the program into a suspended state or allowing the program to continue as though nothing has happened. Each of these behaviors can be useful in different situations. Pausing a program would allow you to load it into a debugger and examine the state of the program. Allowing a program to continue after the failure of an assertion might be used to examine the propagation of an error through the rest of that program. A useful assertion tool would provide a variety of behaviors, and these behaviors would be configurable at either run time or compile time.

Another aspect of an assertion's behavior that users should be able to configure is the destination of the assertion's output. There are times when output should be sent to stdout, stderr, a file, or even a pop-up window. Like the other behaviors, this should also be configurable at either run time or compile time.

The placement of assertion statements (preconditions, postconditions, and invariants) is crucial to the readability of the program. Java assertions must be placed in the class definition section of a program. Placing preconditions and postconditions at the beginning of a class method makes them easy to identify. Although the beginning of a method is the proper place to evaluate a precondition, postconditions must be treated differently. Postconditions need to be evaluated before each exit point in the method. For example, a method that has multiple return statements should evaluate the postcondition before each of these return statements. Ideally, users should be able to place the postcondition at the top of the method and have the assertion tool evaluate it at all of the appropriate locations. This helps keep the program readable and adds to the usefulness of the assertion.

Invariants need to be treated in a similar manner. A Java programmer would like the ability to specify a class invariant once in the class definition, then have it evaluated at the entry and exit of every public method for that class. A Java assertion tool should provide you with the ability to place preconditions, postconditions, and invariants at convenient locations in the class definition, and still evaluate them at the proper locations in the code.

A useful assertion tool will have to provide extensions to Java's Boolean expression syntax. Boolean expressions used in assertion statements should be easier to read and provide more functionality than ordinary Java Boolean expressions. The means of specifying Boolean expressions for assertion statements will be referred to as the "assertion language." The assertion language should include functions like the existential quantifiers "for-all" and "there-exists." This provides users with a simple means of expressing complex Boolean expressions that are difficult to write using traditional Java expressions. The tool should provide other functions (perhaps Range and Equals functions), as well as tokens to make Java expressions more readable. An example of such tokens would be to allow use of the words "AND" and "NOT" to respectively replace "&&" and "!". The more functions an assertion language provides, the easier it should be to write powerful yet readable Boolean expressions.

There are a couple of metavariables that the assertion language should provide that are unique to a postcondition statement. Postcondition statements should have a way to refer both to the return value of the function and to the initial value of any of the function's arguments. Providing these capabilities will add a great deal of flexibility to postcondition statements, and allow them to specify conditions that they would otherwise be unable to express. For example, Listing One is code for a class written using Pajama (RST's assertion tool). Note that Pajama uses special comment blocks to allow assertions to be toggled on and off during development and testing. It supports automatic placement of preconditions and postconditions. It also provides a full-featured assertion language.

Implementation Issues

The biggest obstacle to implementing your own Java assertions is the lack of a preprocessor. A preprocessor can be used to provide the ability to compile programs without including the assertions that are embedded in the source code. The only way to implement this optional compilation ability in Java is to either add assertion support directly to the Java compiler, or write a specialized preprocessor. Writing a script that goes through a program's source code and places all assertions in comments (or pulls them out of comments) could provide this functionality.

Java's lack of system calls makes it difficult to modify the behavior of an assertion at run time. The only way to pass parameters into the assertion library at run time is via the Java Virtual Machine (JVM) system parameters. The type of behavior that you want when an assertion fires depends on how your code is being used. Typically, you want your program to terminate when an assertion is fired during the debugging process because the violation of an assertion indicates that a bug exists somewhere in the program. Java, however, makes implementing even this simple behavior more difficult than it first appears. The assertion tool must be able to recognize that a Java program could be an applet (which can't terminate) and execute some behavior other than termination.

Properly dealing with inheritance requires that when a precondition or postcondition of a polymorphic method is evaluated, the precondition or postcondition in the superclass' corresponding method may also have to be evaluated. This means that an assertion needs to be duplicated in both the superclass and derived classes. Duplicating the assertion can be done by the JVM, Java compiler, or perhaps an advanced preprocessor. Using the JVM would involve adding the ability for it to look in the superclass for the appropriate assertions and then evaluate them at run time. The preprocessor or Java compiler could be used to copy the assertions from the superclasses to the derived class at compile time. To perform this copying at compile time, the compiler would have to be able to identify the assertions in the .class file of the superclass without relying on the .java file.

A postcondition should have the ability to refer to the value that a variable had at the beginning of a method. Java adds a level of complexity to this because, in Java, every assignment is made by reference. For example, if the statements in Example 1 are executed in a C++ program, x is left with the value 10, and old_x is left with the value 5. If, however, these statements are contained in a Java program, then x finishes with the value 10, and old_x also has the value 10. This is because old_x has been assigned to be a reference to x. Java requires that copy semantics are used to correctly implement the "old" function.

Rolling Your Own Assertions

Listing Two is a simple assertion tool that supports PreCondition, PostCondition, and Assert statements. When an assertion is violated, a message is printed and a stack trace of the program is displayed. The stack trace is useful for locating the bug in the program. This assertion tool doesn't support many of the features an assertion tool should (such as a full-featured assertion language and optional compilation). However, it provides the basic capabilities necessary to begin using assertions in your code.

The assertion tool provides the methods PreCondition, Assert, and PostCondition. These methods should be used to check the correctness of input to a method, internal state of a method, and output of a method, respectively. These methods all accept a Boolean condition and a message as their two parameters. The Boolean condition is the condition that is being asserted to be True. If the condition does evaluate to True, the method simply returns. If the condition is False, then the message is printed along with a stack trace (determined by the printStackTrace method). The function System.exit() is called to terminate the program after it has reached an internally corrupt state.

As Listing Three shows, this example tool provides basic assertion support and can be modified to better fit into your development environment. If you want to provide behaviors other than termination when the assertion fires, replace the System.exit() call with code that performs the desired behavior. If you have a GUI-based application or applet, you may want to replace the System.out.println() calls with calls that will display the message to a pop-up window. In the case of applets, this can also display a warning to users stating that continuing may have unpredictable behavior.

Conclusion

Assertions provide a powerful mechanism for assisting you in the creation of Java classes and integration of Java components. Ideally, assertions would be part of the Java language. Since they aren't, we've provided a basic assertion capability and presented information on what a more powerful assertion capability should include.

DDJ

Listing One

public class math{ 
private static int double_it(int x)		// doubling algorithm
  {
    /*+ POST_CONDITION( $RETURN = 2 * $OLD(x), "didn't double correctly"); +*/
    x *= 3;		// bug in method
    return x;
  }
public static int sqrt(int x)		// square root algorithm
  {
    /*+ PRE_CONDITION(x >= 0, "Can't take the sqrt of a negative number"); +*/
    int i;
    for (i=0;i*i <= x; i++)
      {
      }
    return i-1;
  }
public static void switcheroo(char c)	// prints a gender
  {
    switch ( c )
      {
      case 'm' :
      case 'M' :  System.out.print("Male");  break;
      case 'f' :
      case 'F' :  System.out.print("Female");  break;
      default :  /*+ ASSERT(false, "Not a valid gender!");  +*/  break;
      }
  }
public static void main(String argv[])	// test method for class
  {
    /*  These all work:  */
    System.out.println("2 * 0 = " + double_it(0));
    System.out.println("sqrt(9) = " + sqrt(9));
    switcheroo('m');
    System.out.print(" ");
    switcheroo('f');
    System.out.println();
    /*  These all fail */
    System.out.println("2 * 3 = " + double_it(3));
    System.out.println("sqrt(-9) = " + sqrt(-9));
    switcheroo('q');
  }
} 
}

Back to Article

Listing Two

import java.io.*;public class Assert
{
private static String getStackTrace()	// gets a stack trace
  {
    Throwable t = new Throwable();		// for getting stack trace
    ByteArrayOutputStream os = new ByteArrayOutputStream();
				// for storing stack trace
    PrintStream ps = new PrintStream(os);	// printing destination
    t.printStackTrace(ps);
    return os.toString();
  }
public static void PreCondition(boolean condition, String message)
			           // code to check PreConditions
  {
    if (!condition)	// was PreCondition violated?
      {
        System.out.println("Precondition [" + message + "] fired at:");
				// print user msg
        System.out.println(getStackTrace());	// print stack
        System.exit(1);
      }
  }  
public static void Assert(boolean condition, String message)
			           // code for data state assertions
  {
    if (!condition)	// was assertion violated?
      {
        System.out.println("Assert [" + message + "] fired at:");
        System.out.println(getStackTrace());
        System.exit(1);
      }
  }  
public static void PostCondition(boolean condition, String message)
			           // code for PostCondition check
  {
    if (!condition)	// was PostCondition violated?
      {
        System.out.println("Postcondition [" + message + "] fired at:");
        System.out.println(getStackTrace());
        System.exit(1);
      }
  }  

Back to Article

Listing Three

public class math		// doubling algorithm{ 
private static int double_it(int x)
  {
    int ret = x*3;  // bug
    Assert.PostCondition(ret == 2*x, "didn't double correctly"); 
    return ret;
  }
public static int sqrt(int x)		// square root algorithm
  {
    Assert.PreCondition(x >= 0, "Can't take the sqrt of a negative number"); 
    int i;
    for (i=0;i*i <= x; i++)
      {
      }
    return i-1;
  }
public static void switcheroo(char c)	// prints a gender
  {
    switch ( c )
      {
      case 'm' :
      case 'M' :  System.out.print("Male");  break;
      case 'f' :
      case 'F' :  System.out.print("Female");  break;
      default :  Assert.Assert(false, "Not a valid gender!"); break;
      }
  }
public static void main(String argv[])	// test method for class
  {
    /*  These all work:  */
    System.out.println("2 * 0 = " + double_it(0));
    System.out.println("sqrt(9) = " + sqrt(9));
    switcheroo('m');
    System.out.print(" ");
    switcheroo('f');
    System.out.println();
    /*  These all fail */
    System.out.println("2 * 3 = " + double_it(3));
    System.out.println("sqrt(-9) = " + sqrt(-9));
    switcheroo('q');
  }


</p>


</p>

Back to Article


Copyright © 1998, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.