The other day, I wasted time debugging some Java code. When I say “wasted” I do not complain about debugging per se — debugging is part of my life as a developer. Time was wasted because debugging should not have been necessary in this case. Let me explain…
It just so happened that I called a method but violated a constraint on a parameter. Within the called method, the constraint was properly enforced via the use of an assertion, just like in this example:
1 2 3 4 5 6 |
public int invertValues(int rows, int[] values) { assert rows <= MAX_ROWS; ... } |
Normally, my violation of the method’s contract would have been immediately reported and I wouldn’t have had to debug this bug. Normally, yes, but not in this case, as I forgot to run my program with assertions enabled. So instead of
1 2 3 |
java -ea MyProgram |
I wrote what I had written thousands of times before:
1 2 3 |
java MyProgram |
Silly me, silly me, silly me! That’s what I thought initially. But then I was reminded of the words of Donald A. Norman. In his best-selling book “The Design of Everyday Things” he observes that users frequently — and falsely — blame themselves when they make a mistake, when in fact it is the failure of the designer to prevent such mistakes in the first place. Is it possible that Java’s assertion facility is ill-designed? After having thought about it for some time, I’m convinced it is.
Assertions first appeared in the C programming language and they came with two promises: first, assertions are enabled by default (that is, until you explicitly define NDEBUG) and second, they don’t incur any inefficiencies once turned off. These two properties are essential and Java’s implementation misses both of them.
The violation of the first principle means that you cannot trust your assertion safety net: It is just too easy for you, your teammates or your users to forget the ‘-ea’ command-line switch. If you don’t trust a feature, you don’t want to use it. What use is an anti-lock break system that you have to enable manually every time you start your car?
Efficiency has always been a major concern to developers. If you execute your Java code with assertions disabled (which is, as we know, unfortunately the default) you will most likely not notice any speed penalty. What you will notice, however, is the additional footprint for your assertions that will always travel with your Java program. There is no way to compile assertions out. Take a look at this C example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
int binary_search(const int* values, size_t values_len, int search_value) { #ifndef NDEBUG // Ensure that given values are sorted. int i; for (i = 1; i < values_len; ++i) { assert(values[i] >= values[i - 1]; } #endif ... // Actual implementation of binary search. ... } |
A prerequisite of any binary search implementation is that the input values are sorted, so why not assert it? Since we need to iterate over all elements, a simple assert expression is not sufficient. Contrary to Java, this is not a problem in C and C++: the code for the assert as well as the for-loop will be removed from the release build, thanks to the pre-processor.
While assertions — especially non-trivial assertions that require supporting debug code — already waste memory, you can do worse if you use the kind of assertion that allows you to specify a string to be displayed when an assertion fails:
1 2 3 4 5 |
... assert factory != null : "Factory must exist at this point!"; ... |
This string is of little use. If a programmer ever sees it, (s)he will have to look at the surrounding code anyway (as provided by the filename, line number pairs in the stack trace), since it is unlikely that such an assertion message can provide enough context. But, hey, I wouldn’t really mind the string if it came at no cost, but in my view, wasting dozens of bytes in addition for the string is not justified. I prefer the traditional approach, that is, an explanation in the form of a comment:
1 2 3 4 5 6 7 8 9 10 |
... // Ensure that a factory exists. // If this assertion fails, it is highly likely that the // initialization order in Startup.init() is messed-up. // Double-check that Factory.init() is called right // at the beginning. assert factory != null; ... |
Assertions are like built-in self-tests and one of the cheapest and most effective bug-prevention tools available; this fact has been confirmed once again in a recently published study by Microsoft Research. If developers cannot rely on them (because someone did forget to pass ‘-ea’ or inadvertently swallowed the assertion by catching ‘Throwable’ or ‘Error’ in surrounding code) or always have to worry about assertion code-bloat, they won’t use them. This is the true waste of Java assertions.