Introduction
We propose to add a simple assertion facility to the Java programming
language. An assertion is a statement containing a boolean expression
that the programmer believes to be true at the time the statement is
executed. For example, after unmarshalling all of the arguments from a data
buffer, a programmer might assert that the number of bytes of data remaining
in the buffer is zero. The system executes the assertion by evaluating the
boolean expression and reporting an error if it evaluates to false.
By verifying that the boolean expression is indeed true, the system
corroborates the programmer's knowledge of the program and increases his
confidence that the program is free of bugs. Assertion checking may be
disabled for increased performance. Typically, assertion-checking is enabled
during program development and testing, and disabled during deployment.
Because assertions may be disabled, programs must not assume that the
expressions contained in assertions will be evaluated. Thus, it is
extremely bad practice for these expressions to have any side
effects: evaluating the boolean expression should never affect any state
that is visible after the evaluation is complete. (It is not, in
general, possible for the compiler to enforce a prohibition on
side-effects in assertions, so the language specification does not
disallow the practice.)
Along similar lines, assertions should not be used for
argument-checking in public methods. Argument-checking is typically
part of the contract of a method, and this contract must be upheld whether
assertions are enabled or disabled. Another problem with using assertions for
argument checking is that erroneous arguments should result in an appropriate
runtime exception (e.g., IllegalArgumentException,
IndexOutOfBoundsException, NullPointerException). An
assertion failure will not throw an appropriate exception. (Again, it is not,
in general, possible for the compiler to enforce a prohibition on
the use of assertions for argument checking on public methods, so the
language specification will not disallow the practice.)
Syntax
A new keyword would be added to the language: assert. The use of
this keyword is governed by two new productions in the grammar:
StatementWithoutTrailingSubstatement:
<All current possibilities, as per JLS, Section 14.4>
AssertStatement
AssertStatement:
assert(Expression) ;
assert(Expression, Expression) ;
In both forms of the assert statement, the initial
Expression must have type boolean or a compile-time error
occurs. In the second form of the assert statement, the second
Expression must have a reference type or a compile-time error
occurs.
Semantics
Assertions may be enabled or disabled on a per-class basis. At
the time that a class is loaded, its class loader determines whether
assertions are enabled or disabled as described below.
Once a class has been loaded, its assertion status (enabled or
disabled) does not change as long as it remains loaded.
If assertions are disabled in a class, all of the assertions contained in
methods of that class have no effect at runtime. In particular, the
"arguments" to the assert statements (the boolean Expression and the
reference-type Expression, if present) are not evaluated.
If assertions are enabled in a class, the execution of an assert
statement proceeds as follows:
- Evaluate the boolean Expression in the
assert statement. If evaluation of the Expression
completes abruptly for some reason, the assert statement
completes abruptly for the same reason.
- If the resulting boolean value is false:
- If the assert statement is of the second form (with a
reference-type Expression), evaluate the reference type
Expression and invoke the toString() method on the result.
- If the computation in the previous step completes abruptly for some
reason, or if the assert statement is of the first form (with only a
boolean Expression), obtain an AssertionError object with no
"detail message"; otherwise construct an AssertionError object with
with the String object computed in the previous step as its detail
message.
- Throw the AssertionError object computed in the previous step.
Each class loader maintains a default assertion status, a boolean value
that determines whether assertions are, by default, enabled or disabled in new
classes that are subsequently loaded by the class loader. A newly created
class loader's default assertion status is false (disabled). It can
be changed at any time by invoking a new method in class ClassLoader:
/**
* Sets the default assertion status for this class loader.
* This setting determines whether classes loaded by this class loader
* will have assertions enabled or disabled by default. This setting
* may be overridden on a per-package basis with the
* setDefaultAssertionStatus(String, boolean) method, or on a
* per-class basis, with the setAssertionStatus(String, boolean) method.
*
* @param enabled true if classes loaded by this class loader
* will henceforth have assertions enabled by default,
* false if they will have assertions disabled by default.
*/
public void setDefaultAssertionStatus(boolean enabled);
If, at the time that a class is loaded, its class loader has been given
specific instructions regarding the assertion status of the class's package
name or its class name (via either of the two new methods described below),
those instructions take precedence over the class loader's default assertion
status. Otherwise, the class's assertions are enabled or disabled as
specified by its class loader's default assertion status.
The following method allows the invoker to set a per-package default
assertion status:
/**
* Sets the package-default assertion status for the named
* package in this class loader. The package-default assertion status
* determines the assertion status for classes belonging to the
* named package that are loaded in the future. This setting takes
* precedence over the class loader's default assertion status, and
* may be overridden on a per-class basis by invoking the
* setAssertionStatus method.
*
* @param packageName the name of the package whose default assertion
* status is to be set.
* @param enabled true if classes loaded by this class loader
* and belonging to the named package will henceforth have
* assertions enabled by default, false if they will
* have assertions disabled by default.
*/
public boolean setDefaultAssertionStatus(String packageName,
boolean enabled);
The following method is used to set assertion status on a per-class basis:
/**
* Sets the assertion status for the named class in this class
* loader. This method overrides the class loader default and
* package default (if applicable). This method is permitted only
* if the named class has not yet been loaded by this class loader.
* Once a class is loaded, its assertion status may not change.
*
* The assertion status of an anonymous inner class cannot be set
* with this method. Anonymous inner classes automatically take on
* the assertion status of their immediate outer class. Named inner
* classes take on the assertion status of their immediate outer class
* by default, but this default may be overridden with this method.
*
* @param className the fully qualified class name of the class whose
* assertion status is to be set.
* @param enabled true if the named class is to have assertions
* enabled when (and if) it is loaded by this class loader,
* false if the class is to have assertions disabled.
* @throws IllegalStateException if the named class has already been
* loaded.
* @see setDefaultAssertionStatus(boolean)
*/
public boolean setAssertionStatus(String className, boolean enabled);
Two switches would be added to our java interpreters to selectively
enable or disable assertions. (These command-line switches are not
part of the specification for assertions, but are just illustrative of how an
implementation of the Java runtime environment might enable and disable
assertions. We do not specify flags as part of the Java platform, they
are just documented as things known to our tools.)
The following switch enables assertions at various granularities:
java [ -enableassertions | -ea ] [:<package name>".*" | :<class name> ]
With no arguments, the switch enables assertions by default. With one
argument ending in ".*", it enables assertions in the named
package by default. With one argument not ending in ".*", it
enables assertions in the named class.
The following switch disables assertions in similar fashion:
java [ -disableassertions | -da ] [ :<package name>".*" | :<class name> ]
If a single command line contains multiple instances of these switches,
they are processed in order before loading any classes. So, for example, to
run a program with assertions enabled only in package
com.wombat.fruitbat, the following command could be used:
java -da -ea:com.wombat.fruitbat.* ...
To run a program with assertions enabled only in package
com.wombat.fruitbat but disabled in class
com.wombat.fruitbat.Brickbat, the following
command could be used:
java -da -ea:com.wombat.fruitbat.* -da:com.wombat.fruitbat.Brickbat ...
Also, a system-specific mechanism, such as a well-known Properties
file, can be used to persistently specify a set of packages or classes that
should have assertions turned on (or off).
Installation scripts could set such properties appropriately. For example,
installing a production release might turn off assertions in all of its
packages for enhanced performance, while installing a Beta release might
turn on assertions to provide more thorough testing. (There are dangers
associated with this approach: turning off asserts could expose
timing-dependent bugs.) Command line switches take precedence over
persistent specifications.
New Error Class
The following error class is added to package java.lang:
class AssertionError extends Error () {
public AssertionError(Class clazz, java.lang.reflect.Method method,
String fileName, int lineNumber);
/**
* Returns the method containing the failed assertion.
*/
java.lang.reflect.Method getMethod();
/**
* Returns the name of the "source unit" containing the source code of
* the failed assertion. The exact interpretation of this value is
* implementation-dependent, to allow for repository-based
* implementations, classes for which no source code exists, and
* the like. Implementations are encouraged to return reasonably
* complete information. For example, a fully qualified file name
* is more appropriate than a "simple file name."
*
* If no file name information exists, this method should return
* the empty string ("").
*/
String getSourceName();
/**
* Returns the line number in the "source unit" of the line containing
* the failed assertion. The exact interpretation of this value is
* implementation-dependent, to allow for repository-based
* implementations, classes for which no source code exists, and the
* like.
*
* If no line number information exists, this method should return -1.
*/
int getLineNumber();
}
Implementation Notes
It is highly desirable that the execution of disabled assertions have little
or no cost at runtime. Further, it is highly desirable that the assertion
facility require minimal change to existing JVM implementations and no change
to the class file format. An implementation that meets these criteria is
sketched below.
Class ClassLoader gets three new private fields, which it uses to
keep track of the desired assertion status of classes not yet loaded:
private boolean defaultAssertionStatus = false;
// Maps String packageName to Boolean assertionStatus
private Map packageDefaultAssertionStatus = new HashMap();
// Maps String fullyQualifiedClassName to Boolean assertionStatus
private Map classAssertionStatus = new HashMap();
These fields are maintained by the three new public ClassLoader
methods, in the obvious fashion. In fact, the only thing that these
methods do is to modify these fields (or the maps they reference).
The compiler adds a "synthetic" field to every class
(except anonymous inner classes), containing the actual
assertion status of the class (and any anonymous inner classes).
final private static boolean $assertionsEnabled =
ClassLoader.desiredAssertionStatus(className);
The name of this field need not be part of the specification: it is
local to the class and can vary from implementation to implementation and
class to class.
The method ClassLoader.desiredAssertionStatus(className) returns
the desired assertion status for the named class by consulting the three new
ClassLoader fields (described above) in the obvious fashion. (The
behavior is slightly more complicated in the case of named inner classes.
This detail is omitted for brevity.) I'm afraid that this method must be
public, as it's invoked from every every package. If it is possible to avoid
this through some (ab)use of native methods, I believe that is probably the
right thing to do.
The assert statement, then, is merely syntactic sugar for this
if statement:
if ($assertionsEnabled && !(Expression))
throw new AssertionError(Expression);
This implementation works equally well in static and instance methods (and
constructors and static blocks, for that matter).
The runtime does not need to treat this code specially in order to "strip
out" assertions from classes where they're disabled! Because
$assertionsEnabled is final, the JIT can easily determine that the
entire if statement is a no-op, and eliminate it in the compiled code
that it generates. I believe that present day JITs (including HotSpot) do
this optimization (though I've yet to confirm this).
Note also that $assertionsEnabled resolves to the so-named field in
the "declaring class" for the containing method. The resulting behavior is
clearly desirable, though it may be somewhat surprising. If one class extends
another, and assertions are enabled only in the subclass, assertions contained
in (any) inherited superclass methods will not be executed.
Note that I'm glossing over the details of computing the correct
AssertionError object to throw (as specified in the
Semantics section of this proposal). It should be
clear, however, that this presents no real difficulties.
It is unfortunate that
ClassLoader.desiredAssertionStatus(className) must be made public.
The desirable semantics for a public method differ slightly from what's
required to make the implementation work, but performance concerns probably
dictate that we stick with what's required (which is to return the assertion
status that would apply if the class were to be loaded at the time of the
invocation, whether or not it's already loaded). If the class is already
loaded at the time of the invocation, its actual assertion status may differ
from the value returned by the invocation, though it typically won't.
As described above, this implementation requires no change to the JVM
implementation, but does not support control over assertions in "low-level"
system classes (ClassLoader and any classes that are loaded before
ClassLoader). In order to support this level of control (via command
line switches to java), minimal changes to the JVM implementation
would be required. The maps maintained in
packageDefaultAssertionStatus and classAssertionStatus would
be internal (C/C++) hash tables, and the public methods that manipulate these
maps would become (or would rely on) native methods. There is no reason even
to consider doing this if none of these "system kernel classes" contain any
assertions.
Open Issues
- It would be nice if there were a simply way to enable assertions
for an entire "package tree", such as javax.swing.*. In other words,
one should be able to specify, either on the command line or via an API,
that assertions in a particular package and all of its subpackages
are enabled. The current proposal does not provide any way of doing this.
- It is somewhat surprising that an exception encountered during the
evaluation of the second (String) expression in a two-expression
assert statement is intercepted. On the other hand, it seems wrong
not to throw an AssertionError once it has been determined that an
assertion has failed.
- The proposal currently contains no way to query the assertion status of a
class (enabled or disabled). Is there any compelling reason to provide
a method to return this information?
|