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

## 6.4 Sonstige Anweisungen

- 6.4 Sonstige Anweisungen
  - 6.4.1 Die assert-Anweisung
    - Syntax
    - Bedeutung
    - An- und Abschalten der Assertions
    - Anwendungen
    - Nebeneffekte
    - Kompatibilität

### 6.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 7) setzt es insbesondere Kenntnisse über das Auslösen und Behandeln von Ausnahmen voraus, die erst in Kapitel 12 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. Diese, ursprünglich schon für die erste JDK-Version vorgesehene, dann aber aus Zeitgründen zurückgestellte Anweisung wird C- oder C++-Programmierern wohlbekannt sein. Sie dient dazu, an einer bestimmten Stelle im Programmablauf einen logischen Ausdruck zu plazieren, von dem der Programmierer annimmt, daß er stets wahr ist. 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. |  |
JDK1.1-1.4

Assertions, wie `assert`-Anweisungen vereinfachend genannt werden, dienen also dazu, bestimmte Annahmen über den Zustand des Programmes zu verifizieren und sicherzustellen, daß diese eingehalten werden. Soll im Programm beispielsweise überprüft werden, ob eine Variable `x` nicht-negativ 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 Programmcode ist kürzer.
- Auf den ersten Blick ist zu erkennen, daß es sich um einen Korrektheits-Check handelt und nicht um eine Verzweigung zur Steuerung des Programmablaufs.
- Assertions lassen sich zur Laufzeit wahlweise an- oder ausschalten. Im Gegensatz zu einer einfachen `if`-Anweisung verursachen sie praktisch keine Verschlechterung des Laufzeitverhaltens, wenn sie deaktiviert sind.

Der im Syntaxdiagramm angegebene `ausdruck1` muß 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

Aus Kompatibilität zu älteren JDK-Versionen sind Assertions sowohl im Compiler als auch im Interpreter standardmäßig deaktiviert. Um einen Quellcode zu übersetzen, der Assertions enthält, kann dem Java-Compiler ab der Version 1.4 die Option `-source 1.4` übergeben werden. Wird dies nicht gemacht, gibt es eine Fehlermeldung, nach der "assert" ab der Version 1.4 ein Schlüsselwort ist und nicht mehr als Bezeichner verwendet werden darf. Ältere Compilerversionen melden einen Syntaxfehler. 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 6.7: Anwendung von Assertions

Das Beispielprogramm verwendet zwei Assertions, um sicherzustellen, daß mindestens zwei Kommandozeilenargumente übergeben werden und daß das zweite von ihnen nicht 0 ist. Es kann mit folgendem Kommando übersetzt werden, wenn der Compiler mindestens die Version 1.4 hat:

    javac -source 1.4 AssertionTest.java

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 Listing0608
    002 {
    003   public static void main(String[] args)
    004   {
    005     if (args.length < 2) {
    006       throw new IllegalArgumentException();
    007     }
    008     ...
    009   }
    010 }

Listing 6.8: 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 nicht-abschaltbare 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:

- Die Überprüfung von Parametern, die an nicht-öffentliche Methoden übergeben werden. Diese können nur dann falsch sein, wenn das eigene Programm sie fehlerhaft übergibt. Kann nach dem Test der Software davon ausgegangen werden, daß ein bestimmter Codeteil fehlerfrei ist, ist es legitim, die Assertions in diesem Bereich zu deaktivieren und so die Performance des Programmes zu verbessern. Innerhalb der Test- und Debugphase dagegen sind die Assertions eine große Hilfe beim Aufdecken von verdeckten Fehlern.
- Die Verwendung in Postconditions, also in Bedingungen, die am Ende einer Methode erfüllt sein müssen. Postconditions dienen dazu, sicherzustellen, daß eine Methode bei korrekten Eingabewerten (die ihrerseits durch Preconditions abgesichert werden) auch korrekte Ergebnisse produziert. Beispielsweise könnte am Ende der `push`-Methode eines Stacks sichergestellt werden, daß der Stack nicht-leer ist.
- Die Überprüfung von Schleifeninvarianten, also von Bedingungen, die am Anfang oder Ende einer Schleife bei jedem Schleifendurchlauf erfüllt sein müssen. Dies ist besonders bei Schleifen sinnvoll, die so komplex sind, daß beim Programmieren eine Art "Restunsicherheit" bezüglich ihrer Korrektheit bestehen bleibt.
- Die Markierung von "toten Zweigen" in `if`- oder `case`-Anweisungen, die normalerweise niemals erreicht werden sollten. Anstatt hier einen Kommentar der Art "kann niemals erreicht werden" zu plazieren, könnte auch eine Assertion mit `false` als Argument gesetzt werden. Wird dieser Zweig später tatsächlich einmal aufgrund eines Programmfehlers durchlaufen, geschieht dies wenigstens nicht unerkannt, sondern das Programm liefert eine Fehlermeldung und zeigt den Ort des Problems an.

|  | 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 Meyer's 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 6.9: Anwendung von Assertions

Precondition-Assertions sind in den Zeilen 058 und 059 zu sehen. Sie stellen sicher, daß 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, daß der Bubblesort nicht zu viele Arraydurchläufe macht, was ein Indiz dafür wäre, daß 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 Programmes auf andere Weise modifiziert. Der Grund dafür ist, daß 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, daß 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:

- Ein Quelltext, der `assert`-Anweisungen enthält, kann nur mit einem Compiler übersetzt werden, der mindestens die Version 1.4 unterstützt. Ältere Compiler halten eine `assert`-Anweisung für eine Schlüsselwort und melden einen Syntaxfehler. Das Gleiche passiert, wenn der JDK-1.4-Compiler ohne die Option `-source 1.4` aufgerufen wird, nur ist dann die Fehlermeldung klarer.
- Bytecode von Klassen, die `assert`-Anweisungen enthalten, läßt sich nicht auf älteren virtuellen Maschinen ausführen, denn die vom Compiler generierten Zugriffe auf Felder und Klassen sind dort nicht vorhanden.

Was folgt daraus? Assertions lassen sich nur dann sinnvoll einsetzen, wenn der Entwickler einen JDK-1.4.Compiler besitzt und davon ausgehen kann, daß auf allen Zielsystemen ein 1.4-kompatibles Laufzeitsystem vorhanden ist. Letzteres ist - zumindest zum Zeitpunkt, an dem diese Zeilen geschrieben werden - leider nicht unbedingt der Fall. Der Reiz von Java liegt ja gerade in seiner Plattformunabhängigkeit, und bis auf allen wichtigen Zielsystemen ein JDK-1.4-Laufzeitsystem zur Verfügung stehen wird, könnte es noch einige Zeit dauern. Wegen der zur Installation eines neueren JDKs erforderlichen Betriebssystempatches auf UNIX-Systemen sind heute - mehr als zwei Jahre nach der Veröffentlichung des JDK 1.3 - immer noch viele UNIX-Systeme lediglich mit einem 1.2er JDK ausgestattet oder wurden gerade erst auf 1.3 umgestellt. Vermutlich wird es noch einige Zeit dauern, bis das JDK 1.4 flächendeckend verfügbar sein wird.

Fazit: Assertions sind ein ausgezeichnetes Mittel, um Code lesbarer und robuster zu gestalten. Während der Debugphase 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, daß die Zielsysteme mindestens das JDK 1.4 unterstützen, können Assertions nicht verwendet werden.

Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 3. Auflage, Addison Wesley, Version 3.0
<< | < | > | >> | API | © 1998-2002 Guido Krüger, http://www.javabuch.de