Assertions

This paper takes a brief look Java's new assert keyword, why it is used, and how it is used in Java programs.


Introduction

Every programmer does his or her share of debugging. Some programmers think of assertions as debugging tools. At JFastTrack, we think of assertions as anti-bugging tools. Assertions are used to detect and prevent bugs. They check conditions at run time, providing an easier way to create programs that are more stable and more robust.

Java's assertions are modeled after the assert macro in ANSI C. Assertions appeared in James Gosling's original specification for Java, when the language was called Oak. They were removed because there was not enough time to do a proper design and implementation. Assertions became a permanent part of Java in JDK 1.4.


A First Example

An assertion checks a condition at run time. For example, a GUI panel might have a method that displays a customer:

    private void display(Customer c)

What can go wrong? For one thing, the parameter might be null. That can cause a NullPointerException. We can check for a null parameter by including the following code at the beginning of the method:

    if (c == null) {
        throw new IllegalArgumentException("Customer is null");
    }

A lot of Java programmers use this kind of code to protect against bad parameters.

Here's a single line of code that serves the same purpose:

    assert c != null;

The keyword assert is followed by a boolean expression. The assert statement tests the expression at run time. If the expression is true, program execution continues normally. If the expression is false, the assert statement creates and throws an AssertionError object.


Syntax

In its simpler form, an assertion looks like this:

    assert <boolean expression>;

For example:

    assert (c != null);

The expression is one that the programmer expects will be true at run time. When the Java virtual machine encounters an assertion, it evaluates the boolean expression. If the expression is true, program execution continues normally. If the expression is false, the JVM creates and throws an AssertionError.

An assertion can also be written:

    assert <boolean expression> : <String>;

For example:

    assert (c != null) : "Customer is null";

This form of assertion works just like the previous one. However, if the expression is false, the String is passed as a parameter to the AssertionError constructor. (The String can also be any expression that can be evaluated as a String.)

Some programmers like to put parentheses around the expression, as in:

    assert (c != null);

The parentheses are not required. Java's assertions are modeled after the assert macro in C. C and C++ require parentheses around the expression. A lot of Java programmers are former C and C++ programmers who write the parentheses out of habit.


The AssertionError Class

AssertionError is a member of package java.lang and a subclass of Error. It has several constructors:

Some programmers are surprised that an assertion would throw an Error rather than a RunTimeException. Assertion failures are intended to report program bugs. They are meant to be critical failures. The people who designed assertions felt that programmers might try to catch and recover from exceptions.


Advantages of Assertions

One obvious advantage is that assertions are easier to read and write than comparable if statements.

Another advantage is efficiency. Thorough error checking can be quite helpful during software development. However, this kind of checking should not be needed once a program goes into production. In fact, a program with a lot of error checking can run considerably slower than a program that does no error checking.

Java's assertions can be enabled and disabled at run time. The simplest way is with a command-line switch. For example:

    java –enableassertions ReservationServer

The class loader checks the setting of the –enableassertions switch whenever a class is loaded. If this switch is set, then assertions are enabled — the virtual machine will check each assertion.

Production programs are run without this switch, so assertions are disabled. If assertions are disabled when a class is loaded, assertions in that class are ignored. This lets production programs run at full speed.

–enableassertions can be abbreviated –ea.

Note: It is also possible to load some classes with assertions enabled and other classes with assertions disabled. However, this is rarely done and is of use only in specialized situations.


Side Effects

Because assertions are ignored in production programs, the programmer must ensure that assertions do not have any side effects. In other words, assertions must not change anything in the class or classes being tested.

Here's an example of how assertions should not be used. The following code snippet reads data from an input stream:

    byte[] buffer = new byte[BUFFER_SIZE];
    int nBytesRead;
    // Don't do this!!
    assert ((nBytesRead = inputStream.read(buffer, 0, BUFFER_SIZE)) > 0);

The inputStream.read method returns the number of bytes actually read. If the end of the stream has already been reached, it returns –1. An assertion is used to ensure that the end of the stream has not been reached. We dislike this code for three reasons:

  1. (Least important) Reading and checking the return value in the same statement makes this code harder to read and maintain.
  2. Assertions are designed to test for bugs in programs. The inputStream.read method may return –1 if the data in the stream has been changed unexpectedly. This is not necessarily a program bug, so an AssertionError may be misleading.
  3. (Most important) What happens when this code goes into production? In production, the assert statement is ignored. This means that the read method will not be called and the stream's data will not be read.

The Java compiler cannot check for side effects in assertions. This responsibility is left entirely in the hands of the programmer.

General rule: assertions must never change the state of the system.


Uses of Assertions

Assertions are used primarily to detect bugs in programs. They test for conditions that should never occur in a working program.

Here are a few of the ways in which we use assertions at JFastTrack.


Parameter Validation

We have already seen an assertion used to validate a parameter. This is good practice for private methods, where an invalid parameter indicates a bug within its class.

We do not use assertions to check parameters of public methods. public methods can be called from many places, and parameters can come from many different sources. In a public method, we may not know whether an invalid parameter indicates a bug in the calling program. In this case, we prefer to keep parameter checking active, even in production code. For public methods, we still use the old-fashioned if statement that will throw an IllegalArgumentException when a bad parameter is detected.


Invariants

Most programmers define an invariant as a condition that must always be true. In reality, an invariant may become false briefly during an update, but must always be true by the time that update is complete.


Class Invariants

A class invariant is an invariant that applies to an object or an entire class.

Example 1:

Consider the ArrayList class. An ArrayList stores its elements in an internal array. One invariant of the class is that the reference to the array is not null. If the reference is null, then the code has a bug that must be fixed.

Example 2:

Consider an Account class that represents a bank account. Each deposit or withdrawal is a Transaction. Each Account object has a Vector of Transactions. An account's balance can be calculated by adding the deposits and subtracting the withdrawals. This is simple to write. It can also be quite slow for an account that has a lot of transactions.

We can increase the speed of this system by adding a balance field to the Account class. Each time a deposit is created, we add its amount to the balance. Each time a withdrawal is created, we subtract its amount from the balance. This saves the time needed to do a calculation whenever we want to access an account's balance.

An invariant of the Account class is that its balance must equal the sum of its deposits minus the sum of its withdrawals. We can use assertions to check this condition at various places in the Account class.

Notice that the Account class' invariant briefly becomes false when a new Transaction is added. It does not matter whether we add the Transaction to the Vector before or after we add its amount to the balance. We can't do both at the same time, so our invariant is false for a very short time during the update. This is normal and perfectly acceptable, as long as the invariant becomes true again by the time the update is finished.


Class invariants are not checked during updates. They must be true at the beginning of each update and again at the end of each update. When a program runs in a multi-threaded environment, we can avoid problems by putting updates into synchronized blocks of code. (In a database, this update would be done as a single transaction.)

If a class has a lot of invariants, it may pay to provide a separate method that checks them. This method can be called from various places within the class:

    assert invariantsAreTrue();


Control Flow Invariants

Consider a switch statement where the controlling expression must match one of the case values:

    switch (day) {
        case SUNDAY :
            ...
        case MONDAY :
        case TUESDAY :
        case WEDNESDAY :
        case THURSDAY :
        case FRIDAY :
            ...
        case SATURDAY :
            ...
    }

The value of day must match one of the constants. If it doesn't, then there is a bug in the program. This bug can be reported by adding a default case to the switch:

    default :
        assert false;

An even better technique is to use the second form of the assert statement:

    default :
        assert false : "Invalid day: " + day;

The String passed to the AssertionError constructor will include the value of day. This value will appear when the AssertionError is reported. This can help to make debugging easier.

Some programmers would say that the statement assert false; can be placed at any point that we believe can not or should not be reached. At first glance, it seems to make sense. However, it may not always be the best solution. For example, consider a Vector of credit cards. We might use a sequential search of the Vector to find a Card that contains a particular name. Here is one way to do it:

    Card findCard(String name) {
        for (Enumeration e = getCards(); e.hasMoreElements(); ) {
            Card card = (Card) e.nextElement();
            if (card.getName().equalsIgnoreCase(name)) {
                return card;
            }
        }
        ...
    }

As soon as this method finds the card with the requested name, it returns a reference to that card.

The problem is: what should we do if the Vector does not contain a Card with the requested name? If we simply remove the "...", the compiler will report an error, saying that this method does not return a value. We must replace "..." with some other action. There are three basic choices:

  1. return null;
  2. throw an exception.
  3. assert false;

The first choice, return null, is almost always a bad idea. It means that the caller must explicitly check for a null return value or run the risk of a NullPointerException.

Which is better in this case, an assertion or an exception? First, let's consider an assertion. If we decide to use an assertion, it will be interesting to note that placing

    assert false;

at the end of this method is not enough. The compiler will report an error because the method does not return a value. The method must either return a reference or throw an exception. In production, the assertion is treated as a comment. This method will not return a value if assertions are disabled and the requested name is not found in the vector.

We must do something after the assertion, either return null or throw an exception. Returning null seems a bit of a hack, even though in theory the statement cannot be reached. Some day, in production, the unthinkable might happen. When it does, we break any reasonable contract with our caller and risk a NullPointerException.

We could put a throw statement after the assertion. Once again, we expect the statement will never be reached. This time, though, we are following one throw (the assertion) with another. Our considered opinion is that an assertion may not be appropriate in this case. A simple statement throwing a checked exception may be the simplest and most effective solution to this kind of situation. Of course, this solution will require a throws clause specifying the exception in the method's signature.

Design by Contract

Assertions also provide a limited form of design by contract.

Assertions can be placed at the beginning of a method to ensure that its pre-conditions have been satisfied. Assertions are appropriate for pre-conditions in private methods.

Assertions can also be placed at the end of a method to ensure that the method has fulfilled its post-conditions. Assertions are appropriate for post-conditions, whether a method is public or private.

Example:

Consider a sales database. Part of this database's contract is that every sales transaction must have an associated customer record. Whenever we remove a customer record from this database, we must also remove that customer's transactions.

This database's deleteCustomer method will have two post-conditions:

  1. the customer has been deleted; and
  2. all of the customer's transactions have also been deleted.

For the sake of the contract, it does not matter how the records are deleted. The contract can be fulfilled either by explicit Java code or by a trigger or stored procedure in the database.

We can use assertions at the end of the method to ensure that the records have actually been deleted.

The Java Community Process has indicated that it does not intend to provide full support for design by contract. Anyone seeking more information on design by contract in Java may wish to take a look at iContract.


Compatibility


Source Code

assert is a new keyword, added to the language in JDK 1.4. This new keyword can be a concern for people who have developed their own versions of assert. This includes us here at JFastTrack, as well as the developers of the excellent JUnit package. The developers of JUnit solved the problem by renaming their assert method, calling it assertTrue. (JUnit is used heavily here at JFastTrack. It is highly recommended and free.)

To maintain compatibility with older code, Sun's Java compiler now provides a –source switch that tells which version of Java is being compiled. When the switch is set to 1.3, assert is treated as an identifier. Setting the switch to 1.4 tells the compiler to treat assert as a keyword. Some integrated development environments (Eclipse, for example) have similar options.

The default setting of this switch is 1.3. It's been said that Sun intends to change the default setting and that support for 1.3 may be phased out over time.


JVM

There is no need to recompile older programs. They will continue to work as they always have, even if they use assert as an identifier.

However, older versions of the Java Runtime Environment do not support assertions. Programs that use assert as a keyword cannot be run on any runtime environment prior to release 1.4.


Conclusion

We at JFastTrack eagerly awaited the release of JDK 1.4 because it provides easier ways to write more robust, more reliable programs. Assertions are now included in all of our introductory Java courses.

For more information, please visit us at www.jfasttrack.com.

If you have any questions or comments on this material, please feel free to contact us.


Copyright © 1999-2006 JFastTrack, LLC. All rights reserved.