8.10 Assertions *
Die Übersetzung des englischen Wortes assertion lässt vermuten, worum es geht: um Zusicherungen. Assertions formulieren Aussagen, die beim korrekten Ablauf des Codes immer wahr sein müssen. Ist eine Bedingung nicht erfüllt, folgt eine Ausnahme, die darauf hinweist, dass im Programm etwas falsch gelaufen sein muss. Der Einsatz von Assertions im Code fördert die Dokumentation für den gültigen Programmzustand. Wir unterscheiden:
Precondition: Zustand, der vor einer Operation immer wahr sein muss
Postcondition: Zustand, der nach einer Operation immer wahr sein muss
Die Formulierung korrekter Zustände ist ein wesentliches Element von Design by Contract, einer Entwicklungsmethode, bei der es darum geht, einen »Vertrag« (engl. contract) aufzustellen über das, was ein Programm leisten muss. Bertrand Meyer, auch Erfinder der Programmiersprache Eiffel, prägte diesen Begriff.
8.10.1 Assertions in eigenen Programmen nutzen
Java-Programme nutzen für Assertions im Quellcode die assert-Anweisung. Es gibt zwei Varianten, eine mit und eine ohne Meldung:
assert AssertConditionExpression;
assert AssertConditionExpression : MessageExpression;
AssertConditionExpression steht für ein Prädikat, das zur Laufzeit ausgewertet wird. Der Ausdruck wird nicht automatisch geklammert, da er sich nicht wie ein Methodenaufruf liest.
[»] Java-Geschichte
Neue Schlüsselwörter wurden immer wieder eingeführt: in Java 1.3 das Schlüsselwort strictfp, in Java 1.4 das Schlüsselwort assert für die »Behauptungen« und in Java 5 Aufzählungstypen mit dem neuen Schlüsselwort enum.
8.10.2 Assertions aktivieren und Laufzeit-Errors
Assertions stehen immer in der Klassendatei, da der Compiler sie immer in Bytecode abbildet. Jedoch werden Assertions zur Laufzeit standardmäßig nicht beachtet, da sie abgeschaltet sind. Somit entsteht kein Geschwindigkeitsverlust bei der Ausführung der Programme. Um Assertions zu aktivieren, muss die Laufzeitumgebung mit dem Schalter -ea (enable assertions) gestartet werden.
Sind Assertions aktiviert und wertet die JVM das Ergebnis der assert-Anweisungen zu true aus, führt die Laufzeitumgebung die Abarbeitung normal weiter. Ergibt die Auswertung false, wird das Programm mit einem java.lang.AssertionError beendet. Wir haben gesehen, dass es zwei Varianten gibt:
assert AssertConditionExpression;
assert AssertConditionExpression : MessageExpression;
Der optionale zweite Parameter, MessageExpression, ist ein Text, der beim Stack-Trace als Nachricht in der Fehlermeldung erscheint. Die in Java ausgelösten Ausnahmen sind vom Typ »Error« und nicht vom Typ »Exception« und sollten daher auch nicht aufgefangen werden, da eine nicht erfüllte Bedingung ein Programmierfehler ist.
[»] Hinweis
Die JVM ignoriert Assertions standardmäßig bei der Ausführung, und eine Aktivierung erfolgt nur auf Befehl. Ein Ablauf ohne Bedingungstests ist eher der Normalfall. Daraus folgt, dass Ausdrücke in den assert-Anweisungen ohne Nebeneffekte sein müssen. So etwas wie
assert counter-- == 0;
ist keine gute Idee, denn das Vermindern der Variablen ist ein Nebeneffekt, der nur dann stattfindet, wenn die JVM auch Assertions aktiviert hat. Allerdings lässt sich das auch für einen Trick nutzen, um Assertions bei der Ausführung zu erzwingen. Im statischen Initialisierer einer Klasse können wir Folgendes setzen:
boolean assertEnabled = false;
assert assertEnabled = true;
if ( ! assertEnabled )
throw new RuntimeException( "Assertions müssen aktiviert werden" );
Beispiel
Eine eigene statische Methode subAndSqrt(double, double) bildet die Differenz zweier Zahlen und zieht aus dem Ergebnis die Wurzel. Natürlich weiß jeder Entwickler, dass die Wurzel aus negativen Zahlen nicht erlaubt ist, aber dennoch ginge so etwas in Java durch, nur ist das Ergebnis ein NaN. Sollte irgendein Programmteil nun die Methode subAndSqrt(double, double) mit einem falschen Paar Zahlen aufrufen und das Ergebnis NaN sein, muss ein Assert-Error erfolgen, da es einen internen Programmfehler zu korrigieren gilt:
public class AssertKeyword {
public static double subAndSqrt( double a, double b ) {
double result = Math.sqrt( a - b );
assert ! Double.isNaN( result ) : "Berechnungsergebnis ist NaN!";
return result;
}
public static void main( String[] args ) {
System.out.println( "Sqrt(10-2)=" + subAndSqrt(10, 2) );
System.out.println( "Sqrt(2-10)=" + subAndSqrt(2, 10) );
}
}
Rufen wir das Programm mit dem Schalter -ea auf:
$ java -ea com.tutego.insel.assertion.AssertKeyword
Die Ausgabe ist dann:
Sqrt(10-2)=2.8284271247461903
Exception in thread "main" java.lang.AssertionError: Berechnungsergebnis ist NaN!
at com.tutego.insel.assertion.AssertKeyword.subAndSqrt(AssertKeyword.java:8)
at com.tutego.insel.assertion.AssertKeyword.main(AssertKeyword.java:15)
8.10.3 Assertions feiner aktivieren oder deaktivieren
Assertions müssen nicht global für das ganze Programm gesetzt werden, sondern können auch feiner deklariert werden, etwa für eine Klasse oder ein Paket. Mit geschickter Variation von –ea (Assertions aktivieren) und –da (Assertions deaktivieren) lässt sich sehr gut steuern, was die Laufzeitumgebung prüfen soll.
[zB] Beispiel
Aktiviere Assertions für die Klasse com.tutego.App:
$ java -ea:com.tutego.App ClassWithMain
Aktiviere Assertions für das Default-Paket (dafür stehen die drei Punkte):
$ java -ea:... ClassWithMain
Aktiviere Assertions für das Paket com.tutego inklusive aller Unterpakete (auch dafür stehen drei Punkte):
$ java -ea:com.tutego... ClassWithMain
Aktiviere Assertions für das Paket com.tutego inklusive aller Unterpakete, aber deaktiviere sie für die Klasse App in dem Paket com.tutego:
$ java -ea:com.tutego... -da:com.tutego.App ClassWithMain