Titel   Inhalt   Suchen   Index   DOC  Handbuch der Java-Programmierung, 7. Auflage
 <<    <     >    >>   API  Kapitel 7 - Anweisungen

7.4 Sonstige Anweisungen



7.4.1 Die assert-Anweisung

Die Ausführungen in diesem Abschnitt können beim ersten Lesen übersprungen werden. Bei der assert-Anweisung handelt es sich um ein weiterführendes Konzept, das seit dem JDK 1.4 zur Verfügung steht. Neben einem grundlegenden Verständnis von Klassen und Methoden (ab Kapitel 8) setzt es insbesondere Kenntnisse über das Auslösen und Behandeln von Ausnahmen voraus, die erst in Kapitel 13 vermittelt werden. Aus systematischen Gründen soll das Thema jedoch an dieser Stelle behandelt werden.

 Hinweis 

Syntax

assert ausdruck1 [ : ausdruck2 ] ;

Bedeutung

Seit dem JDK 1.4 gibt es in Java eine assert-Anweisung. Sie dient dazu, an einer bestimmten Stelle im Programmablauf einen logischen Ausdruck zu platzieren, von dem der Programmierer annimmt, dass er stets wahr ist, solange das Programm korrekt funktioniert. Ist das der Fall, fährt das Programm mit der nächsten Anweisung fort, andernfalls wird eine Ausnahme des Typs AssertionError ausgelöst. Ein solcher Ausdruck wird auch als Invariante bezeichnet, denn er bleibt unverändert true, solange das Programm korrekt funktioniert.

Assertions, wie assert-Anweisungen vereinfachend genannt werden, dienen also dazu, bestimmte Annahmen über den Zustand des Programms zu verifizieren und sicherzustellen, dass diese eingehalten werden. Soll im Programm beispielsweise überprüft werden, ob eine Variable x nichtnegativ ist, könnte dazu die folgende assert-Anweisung verwendet werden:

assert x >= 0;

Das Programm überprüft die Bedingung und fährt fort, wenn sie erfüllt ist. Andernfalls wird eine Ausnahme ausgelöst. Natürlich hätte derselbe Test auch mit Hilfe einer einfachen if-Anweisung ausgeführt werden können. Die assert-Anweisung hat ihr gegenüber jedoch einige Vorteile:

Der im Syntaxdiagramm angegebene ausdruck1 muss immer vom Typ boolean sein, andernfalls gibt es einen Compilerfehler. Fehlt ausdruck2 (er darf von beliebigem Typ sein), wird im Falle des Nichterfülltseins der Bedingung ein AssertionError mit einer leeren Fehlermeldung erzeugt. Wurde ausdruck2 dagegen angegeben, wird er im Fehlerfall an den Konstruktor von AssertionError übergeben und dient als Meldungstext für das Fehlerobjekt.

An- und Abschalten der Assertions

Wir wollen uns ein einfaches Beispielprogramm ansehen:

001 /* AssertionTest.java */
002 
003 public class AssertionTest
004 {
005   public static void main(String[] args)
006   {
007     assert args.length >= 2; 
008     int i1 = Integer.parseInt(args[0]);
009     int i2 = Integer.parseInt(args[1]);
010     assert i2 != 0 : "Teilen durch 0 nicht moeglich"; 
011     System.out.println(i1 + "/" + i2 + "=" + (i1/i2));
012   }
013 }
AssertionTest.java
Listing 7.9: Anwendung von Assertions

Das Beispielprogramm verwendet zwei Assertions, um sicherzustellen, dass mindestens zwei Kommandozeilenargumente übergeben werden und dass das zweite von ihnen nicht 0 ist.

Um das Programm laufen zu lassen, kennt der Java-Interpreter ab der Version 1.4 die Kommandozeilenoptionen -enableassertions und -disableassertions, die mit -ea bzw. -da abgekürzt werden können. Ihre Syntax lautet:

java [ -enableassertions | -ea  ] [:PaketName... | :KlassenName ]

java [ -disableassertions | -da  ] [:PaketName... | :KlassenName ]

Werden die Optionen ohne nachfolgenden Paket- oder Klassennamen angegeben, werden die Assertions für alle Klassen mit Ausnahme der Systemklassen java.* an- bzw. ausgeschaltet. Wird, durch einen Doppelpunkt getrennt, ein Klassenname angegeben, gilt die jeweilige Option nur für diese Klasse. Wird ein Paketname angegeben (von einem Klassennamen durch drei angehängte Punkte zu unterscheiden), erstreckt sich die Option auf alle Klassen innerhalb des angegebenen Pakets. Die Optionen können mehrfach angegeben werden, sie werden der Reihe nach ausgewertet. Wird keine dieser Optionen angegeben, sind die Assertions deaktiviert.

Soll unser Beispielprogramm mit aktivierten Assertions ausgeführt werden, kann also folgendes Kommando verwendet werden:

java -ea AssertionTest

In diesem Fall gibt es sofort eine Fehlermeldung, denn die erste assert-Anweisung ist nicht erfüllt. Rufen wir das Programm mit zwei Zahlen als Argumente auf, wird erwartungsgemäß deren Quotient berechnet:

java -ea AssertionTest 100 25

Die Ausgabe lautet:

100/25=4

Wenn das zweite Argument dagegen 0 ist, gibt es eine Fehlermeldung, weil die zweite Assertion nicht erfüllt ist. Auch in diesem Fall steigt das Programm mit einem AssertionError aus, der zusätzlich die Fehlermeldung »Teilen durch 0 nicht moeglich« enthält, die wir nach dem Doppelpunkt in Zeile 010 angegeben haben:

Exception in thread "main" java.lang.AssertionError:
  Teilen durch 0 nicht moeglich
...

Wird das Programm mit deaktivierten Assertions aufgerufen, verhält es sich, als wären die Zeilen 007 und 010 gar nicht vorhanden. In diesem Fall gibt es die üblichen Laufzeitfehler, die bei einem Zugriff auf ein nicht vorhandenes Array-Element oder die Division durch 0 entstehen.

Anwendungen

Genau genommen war das vorige Programm kein wirklich gutes Beispiel für die Anwendung von Assertions. Das Überprüfen der an ein Programm übergebenen Kommandozeilenparameter sollte nämlich besser einer IllegalArgumentException überlassen werden:

001 public class Listing0710
002 {
003   public static void main(String[] args)
004   {
005     if (args.length < 2) {
006       throw new IllegalArgumentException();
007     }
008     ...
009   }
010 }
Listing 7.10: Verwendung einer IllegalArgumentException

Genau wie bei der Übergabe eines Arguments an eine öffentliche Methode sollte es nicht einfach möglich sein, deren Überprüfung zur Laufzeit abzuschalten. Da ein Programm bei der Übergabe von Werten an öffentliche Schnittstellen keinerlei Annahmen darüber machen kann, ob diese Werte korrekt sind, sollte die Überprüfung dieser Werte immer aktiv sein. Die Verwendung von Assertions empfiehlt sich also in diesem Fall nicht. Weitere Beispiele für derartige öffentliche Schnittstellen sind etwa die Daten, die über eine grafische Benutzerschnittstelle in ein Programm gelangen oder die aus Dateien oder über Netzwerkverbindungen eingelesen werden. In all diesen Fällen sollten nichtabschaltbare Fehlerprüfungen anstelle von Assertions verwendet werden.

Der Einsatz von Assertions ist dagegen immer dann sinnvoll, wenn Daten aus unbekannten Quellen bereits verifiziert sind. Wenn also das Nichterfülltsein einer Assertion einen Programmfehler anzeigt und nicht fehlerhafte Eingabedaten. Beispiele sind:

Sowohl Pre- als auch Postconditions wurden mit der Programmiersprache Eiffel, die Bertrand Meyer in seinem Buch »Object-oriented Software Construction« 1988 vorgestellt hat, einer breiteren Öffentlichkeit bekannt. Das im JDK 1.4 implementierte Assertion-Konzept entspricht allerdings nicht in allen Aspekten Meyers ausgefeiltem »Programming by Contract«. Dort gibt es beispielsweise explizite Schlüsselwörter für Pre- und Postconditions und es ist möglich, in Postconditions auf den »alten« Wert eines Parameters zuzugreifen (also auf den, den er beim Eintritt in die Methode hatte).

 Hinweis 

Dennoch stellen Assertions ein wichtiges Hilfsmittel dar, um Programme zuverlässiger und besser lesbar zu machen. Das folgende Programm zeigt verschiedene Anwendungen von Assertions am Beispiel einer einfachen Klasse zur Speicherung von Listen von Ganzzahlen:

001 public class SimpleIntList
002 {
003   private int[] data;
004   private int   len;
005 
006   public SimpleIntList(int size)
007   {
008     this.data = new int[size];
009     this.len  = 0;
010   }
011 
012   public void add(int value)
013   {
014     //Precondition als RuntimeException
015     if (full()) { 
016       throw new RuntimeException("Liste voll");
017     }
018     //Implementierung
019     data[len++] = value;
020     //Postcondition
021     assert !empty(); 
022   }
023 
024   public void bubblesort()
025   {
026     if (!empty()) {
027       int cnt = 0;
028       while (true) {
029         //Schleifeninvariante
030         assert cnt++ < len: "Zu viele Iterationen"; 
031         //Implementierung...
032         boolean sorted = true;
033         for (int i = 1; i < len; ++i) {
034           if (sortTwoElements(i - 1, i)) {
035             sorted = false;
036           }
037         }
038         if (sorted) {
039           break;
040         }
041       }
042     }
043   }
044 
045   public boolean empty()
046   {
047     return len <= 0;
048   }
049 
050   public boolean full()
051   {
052     return len >= data.length;
053   }
054 
055   private boolean sortTwoElements(int pos1, int pos2)
056   {
057     //Private Preconditions
058     assert (pos1 >= 0 && pos1 < len); 
059     assert (pos2 >= 0 && pos2 < len); 
060     //Implementierung...
061     boolean ret = false;
062     if (data[pos1] > data[pos2]) {
063       int tmp = data[pos1];
064       data[pos1] = data[pos2];
065       data[pos2] = tmp;
066       ret = true;
067     }
068     //Postcondition
069     assert data[pos1] <= data[pos2] : "Sortierfehler"; 
070     return ret;
071   }
072 }
SimpleIntList.java
Listing 7.11: Anwendung von Assertions

Precondition-Assertions sind in den Zeilen 058 und 059 zu sehen. Sie stellen sicher, dass die Methode mit gültigen Array-Indizes aufgerufen wird. Eine Precondition, die als RuntimeException implementiert wurde, findet sich in Zeile 015 und prüft, ob die Liste vor dem Einfügen eines weiteren Elements bereits voll ist. Während die Postcondition in Zeile 021 eher formaler Natur ist, überprüft jene in Zeile 069, ob das Sortieren der Elemente tatsächlich erfolgreich war. Die Schleifeninvariante in Zeile 030 stellt sicher, dass der Bubblesort nicht zu viele Array-Durchläufe macht, was ein Indiz dafür wäre, dass er in eine Endlosschleife geraten wäre.

Nebeneffekte

Normalerweise sollten die Ausdrücke in Assertions keine Nebeneffekte enthalten, also keinen Code, der Variablen verändert oder den Status des Programms auf andere Weise modifiziert. Der Grund dafür ist, dass diese Nebeneffekte bei abgeschalteten Assertions natürlich nicht ausgeführt werden. Das Programm würde sich also anders verhalten, als wenn die Assertions angeschaltet wären. Dadurch können sehr schwer zu findende Fehler entstehen. Generell gilt, dass die Funktionalität des Programms nicht davon abhängen sollte, ob die Assertions an- oder abgeschaltet sind.

 Warnung 

Allerdings kann es berechtigte Ausnahmen geben. Ein Gegenbeispiel liefert etwa das vorige Listing in Zeile 030. Hier wird innerhalb der Assertion die Zählervariable cnt inkrementiert, also eine Variable verändert. Das ist allerdings unkritisch, denn diese wird auch nur innerhalb der Assertion benötigt, spielt ansonsten im Programm aber keine Rolle. Variablen, die auch an anderen Stellen im Programm verwendet werden, sollten allerdings nicht innerhalb von Assertions verändert werden.

Kompatibilität

Wie schon erwähnt, gibt es Assertions erst seit der Version 1.4 des JDK. Ihre Verwendung wirft leider einige Kompatibilitätsprobleme auf:

Was folgt daraus? Assertions lassen sich nur dann sinnvoll einsetzen, wenn der Entwickler mindestens einen JDK-1.4.Compiler besitzt und davon ausgehen kann, dass auf allen Zielsystemen, für die er entwickelt, ein mindestens 1.4-kompatibles Laufzeitsystem vorhanden ist. Letzteres ist mitunter nicht unbedingt der Fall.

Fazit: Assertions sind ein ausgezeichnetes Mittel, um Code lesbarer und robuster zu gestalten. Während der Debug-Phase helfen sie bei der Fehlersuche. Sofern keine Kompatibilität zu älteren JDK-Versionen erforderlich ist, sollten sie daher unbedingt verwendet werden. Kann dagegen nicht sichergestellt werden, dass die Zielsysteme mindestens das JDK 1.4 unterstützen, können Assertions nicht verwendet werden.


 Titel   Inhalt   Suchen   Index   DOC  Handbuch der Java-Programmierung, 7. Auflage, Addison Wesley, Version 7.0
 <<    <     >    >>   API  © 1998, 2011 Guido Krüger & Heiko Hansen, http://www.javabuch.de