Eine Assert-Klasse in Java

Die Programmierer unter C(++) haben mitunter die Funktion bzw. das Makro assert() lieb gewonnen. Auch in Java lässt sich eine solche Funktionalität implementieren.

assert() prüft eine Bedingung und bricht das Programm ab, wenn sie nicht zutrifft. Damit können Zusicherungen geschaffen werden. Sind diese nicht erfüllt, kann die Funktion nicht arbeiten. Dies bringt die Software-Entwicklung in die Richtung der Vertragsbasierten Programmierung. Die Software und der Anwender gehen einen Vertrag ein. Hält sich der Entwickler nicht an den Vertrag, in dem er beispielsweise einer Funktion ungeeignete (verbotene) Parameterwerte übergibt, wird der Vertrag gebrochen und über assert zum Beispiel ein Fehler gemeldet.

Unter C(++) wird assert() als Makro implementiert, welches bei der Compilierung zu einer if-Anweisung ausgebaut wird. Mithilfe der Funktion lassen sich Laufzeitzeitfehler abfangen. Das schöne unter C(++): Wenn die Präprozessordefinition NDEBUG gesetzt ist, entfernt der Präprozessor sämtliche Aufrufe von assert(). So erhält man, nach erfolgreichem Software-Test, eine schnellere Programmversion, die keine (überflüssigen) Überprüfungen der Zusicherungen mehr vornimmt.

Damit die Assert-Funktionalität in Java genutzt werden kann, bieten sich zwei Realisierungen an. Zum einen bietet Java ab der Version 1.4 eine Assert-Möglichkeit auf Compiler-Ebene an. Zum anderen können wir diese praktische Funktion einfach in eine Klasse packen. Wir wollen den zweiten Weg gehen.

Die Methode assert() ist mehrfach überladen, damit sie unter verschiedenen Bedingungen aufgerufen werden kann. Damit nicht erst ein Assert-Objekt erzeugt werden muss, ist die Funktion static gekennzeichnet. Zusätzlich wollen wir unser assert() noch erweitern, denn es kann zusätzlich die Aufrufreihenfolge auflisten, die sich durch den Aufruf-Stack ergibt.

public class Assert
{
  private static void fail( String s )
  {
    System.err.println( "Assertion failed on " + s );

    Throwable e = new Throwable();
    e.printStackTrace();

    System.exit( 1 );
  }

  public static void assert( boolean aBoolean ) {
    if ( !aBoolean )
      fail( "Boolean" );
  }

  public static void assert( char aChar ) {
    if ( aChar == '\0' )
      fail( "Char" );
  }

  public static void assert( long aLong ) {
    if ( aLong == 0L )
      fail( "Long" );
  }

  public static void assert( double aDouble ) {
    if ( aDouble == 0.0 )
      fail( "Double" );
  }

  public static void assert( Object anObject ) {
    if ( anObject == null )
      fail( "Object" );
  }
}

Immer dann, wenn assert() bemüht wird, ruft dies wiederum im Fehlerfall fail() auf. Dies erzeugt seinerseits die Ausgabe des Stacks über das Throwable-Objekt, welches eine printStackTrace()-Methode versteht.

 

class java.lang.Throwable implements Serializable

  • void printStackTrace()
    Schreibt das Throwable und anschließend den Stack-Inhalt in den Standard-Ausgabe-Strom.
  • void printStackTrace( PrintStream s )
    Schreibt das Throwable und anschließend den Stack-Inhalt in den angegebenen PrintStream.
  • void printStackTrace( PrintWriter s )
    Schreibt das Throwable und anschließend den Stack-Inhalt in den angegebenen PrintWriter.

 

Schreiben wir nun ein Beispielprogramm, welches das Verhalten ausnutzt:

class AssertTest
{
  public static void main( String args[] )
  {
    int i = 0;
    Assert.assert( i > 10 );
  }
}

Dieses kleine Programm erzeugt dann etwa folgende Stack-Ausgabe:

Assertion failed on Boolean
java.lang.Throwable
at Assert.fail(AssertTest.java:10)
at Assert.assert(AssertTest.java:18)
at AssertTest.main(AssertTest.java:105)

Sehr schön deutlich sind die Funktionsaufrufe sichtbar. Dazu noch mit den Nummern der Fehlerzeilen.

Stack-Ausgabe neu formatieren

Die Ausgabe der ersten Zeile (java.lang.Throwable) lässt sich nur dann vermeiden, wenn der Aufruf von printStackTrace() mit einem Stream oder Writer als Parameter geschieht und dannumformatiert wird. An dieser Stelle greifen wir etwas vor.

Die Implementierung von printStackTrace() schreibt die Ausgabe inklusive der ersten Zeile auf System.err. (Die printStackTrace0()-Methode ist im Übrigen privat und nativ.)

void printStackTrace()
{
  synchronized ( System.err )
  {
    System.err.println( this );
    printStackTrace0( System.err );
  }
}

Auch bei einem PrintStream beziehungsweise PrintWriter, wird das this-Objekt ausgegeben, nur dann in den Stream beziehungsweise Writer hinein. Wollen wir eine benutzerdefinierte Ausgabe erreichen, die später zum Beispiel geparst werden kann, so müssen wir eine dieser Methoden benutzen. So lässt sich die Ausgabe auch in einen StringWriter leiten und dann verarbeiten. Ändern wir die Methode fail() nun so, dass sie nur die wirkliche Fehlerzeile ausgibt und auf die ersten vier Zeilen - die ja unsere Implementierung verraten - verzichtet:

private static void fail( String s )
{
  System.err.println( "Assertion failed on " + s );

  StringWriter sw = new StringWriter();

  Throwable e = new Throwable();
  e.printStackTrace( new PrintWriter( sw ) );

  StringTokenizer st =
     new StringTokenizer( sw.toString(), "\n");

  // Don't think about time and space
  st.nextToken(); st.nextToken(); st.nextToken();

  while ( st.hasMoreTokens() )
    System.out.println(
      " " + ( (String) st.nextToken() ).trim().substring(3) );

  System.exit( 1 );
}

Die Ausgabe ist dann bei einer Fehlerzeile etwas kompakter. Betrachten wir dazu das folgende Test-Programm:

class AssertTest
{
  static void dummfug()
  {
    double d = 0.0;
    Assert2.assert( d );

    int i = 0;
    Assert.assert( i < 10 );
  }
	
  public static void main( String args[] )
  {
    dummfug();
  }
}

Assert2 ist die Klasse mit der erweiterten Fehlerbehandlung. Dann erscheint auf dem Bildschirm:

Assertion failed on Double
AssertTest.dummfug(AssertTest.java:96)
AssertTest.main(AssertTest.java:104)

Natürlich müssen wie diesen Hack mit Vorsicht genießen. Das Format der printStackTrace()-Ausgabe ist nicht standardisiert und das Analysieren der Ausgabe problematisch. Es können etwa einige Zwischenstufen oder die Zeilennummern bei optimierender JIT-Übersetzung fehlen.

Nun könnte das Originalverhalten auch abgebildet werden, denn assert() unter C(++) gibt eine wohldefiniert aufgebaute Meldung aus:

Assertion failed: test, file Dateiname, line Zeilennummer

Der Dateiname ist der Name der Quelldatei und die Zeilennummer ist die Zeilennummer, in der das Makro erscheint.