NullPointerException

Ich erinnere mich… Als vor einigen Jahren (1995) die erste Version der Programmiersprache Java veröffentlicht wurde, waren die in die Sprachdefinition integrierten Exceptions eines der Themen, die in unserer Firma heiss diskutiert wurden.

Damals - ich bin alt genug dabei gewesen zu sein aber noch nicht so alt, so dass ich mich noch erinnern kann - haben wir unsere Programme im Wesentlichen auf NeXT Computern in Objective-C geschrieben. Die damals verwendete Objective-C Runtime unterstützte die Nutzung von Exceptions, die als Präprozessor Macros realisiert waren. Diese verwendeten die Funktionen setjmp() und longjmp() der Standard C Bibliothek. Der Mehrwert der Makros bestand in der dynamischen Verwaltung dieser Sprungziele und der Kennzeichnung des jeweils zuoberst liegenden Exception Handlers.

Die so definierten Exceptions hatten keinen eigenen Typ. Es wurde zwar eine Basisklasse definiert, die sinnvoll die notwendigen Informationen kapselte, doch eine Typidentität wurde so nicht gewährleistet. Daher wurden Exceptions innerhalb der Frameworks nur selten genutzt, die Wahrscheinlichkeit, dass beim Auftreten der Exception eine sinnvolle Reaktion möglich war, war sehr gering.

Mit Java wurde das anders. Das Typsystem der Exception definierte nun ab sofort inhaltlich abgrenzbare Klassen von Typen. Für die Erkennung der Ausnahmebedingung war nicht die konkrete Instanz vonnöten, alleine die Typinformation trug so viel Informationen, dass in der Regel die Reaktion auf die Ausnahmesituation gewählt werden konnte.

Mit dem Typsystem für Exception konnten diese dann auch Bestandteil einer Methodensignatur werden. Der Kontrakt einer Methode umfasst also nicht nur die Parameter und den Rückgabewert, sondern mit der throws Deklaration auch die möglichen Ausnahmebedingungen. In Objective-C war (und ist) das Verhalten in Ausnahmesituationen eben nicht Bestandteil des Schnittstellenkontrakts einer Klasse. Wurden also bei der Implementierung einer Funktion Exceptions ausgelöst, wurde man als Nutzer eben dieser Klasse häufig überrascht - eine Ausnahmebehandlung war selten implementiert. Wie hätte man auch erfahren sollen, dass diese sinnvoll gewesen wäre. Insofern war die Deklaration möglicher Exceptions ein Sprachfeature, das mit Freude aufgenommen wurde.

Die von uns erstellten Java Frameworks nutzten dann auch eifrig die Möglichkeit, neben dem erwarteten Ergebnis einer Methode optional durch eine Exception einen Ausnahmefall zu signalisieren. Gleichzeitig wurden aber drei unerwünschte, miteinander verwandte Effekte sichtbar:

  • Das sinnvolles Abarbeiten von Exceptions ist nur in Ausnahmefällen möglich. Entweder wird nur das wichtige try { ... } finally { ... } genutzt und die Ausnahmebedingung weitergeleitet oder der Exception-Handler nutzt das try { ... } catch ( Exception e) { e.printStackTrace(); } Antipattern.

  • Die Exception wird gefangen, umverpackt und weitergereicht try { ... } catch ( OneException e) { throw new SecondException(e); }. Dieses Muster kann man unter Anderem immer noch schön im Spring-Container beobachten. Wenn hier Ausnahmen auftreten, sind diese auch mehrfach umcodiert und umverpackt. Immerhin ist es an dieser Stelle möglich, die Ursache des Übels zu lokalisieren. Manchmal werden die ursprünglichen Exceptions einfach verschluckt und ein Reagieren auf den Fehler damit gänzlich erschwert.

  • Das Auftreten der Exceptions wird mehr oder weniger ignoriert und einfach über immer umfangreichere throws Deklaration weiter delegiert. Je größer die Entfernung zur eigentlichen Ursache dabei aber ist, um so unwahrscheinlicher wird eine sinnvolle Reaktion auf das Problem implementiert sein.

Damals konnte ich diese Probleme schon bald in unseren Programmen erkennen. Exceptions waren aber neu und hip und jeder Programmierer, der sich als Teil der Nerd-Avantgarde sah, sah sich ausserstande ein API ohne einen umfangreichen Zoo an Exceptions zu definieren. Noch viele Jahre später - 2004 und 2005 - musste ich mich mit einer Bibliothek herumschlagen, die Exceptions als Stilmittel nutzte. Immer wenn in einer Datenquelle gerade keine Daten verfügbar waren - was eigentlich für 99,99% aller Abfragen galt - wurde nicht etwa ein null Objekt zurückgeliefert sondern stattdessen eine Exception ausgelöst. Das wirkte sich verheerend auf unseren ExceptionReporter aus, der auf diese Weise tagtäglich viele tausend Ausnahmen in einer Datenbank protokollieren muss.

Kann man so machen - muss man aber nicht. Exceptions sind das Signal für Ausnahmesituationen und kein valider Rückgabewert. In diesem Fall war die eigentliche Idee wahrscheinlich das Bemühen, die unvermeidliche NullPointerException zu vermeiden. Diese Zugriffe auf ungültige Referenzen haben uns damals bereits in der Objective-C Runtime verfolgt. In Objective-C gab es neben dem Begriff NULL der als (void *) 0 definiert war, auch das Konzept des nil definiert als (id) 0. Beide Begriffe waren wertgleich und verwiesen auf einen Speicherbereich an der Adresse 0x00000000, der aus guten technischen Gründen besonders geschützt war. Bei einem Reset der Motorola CPU wurden aus diesen Speicherzellen die Startadresse des Bootloaders ausgelesen.

In den ersten Objective-C Runtimes war ein Zugriff auf diese Adresse insbesondere unerfreulich, da in diesem Fall das Programm durch das Betriebssystem zwangsweise abgebrochen wurde. Eine irgendwie geartete Reaktion auf diesen Fehler war dann in den seltensten Fällen noch möglich.

Auch in Java führt der Zugriff auf nicht initialisierte Objektreferenzen zu einer vergleichsweise drastischen Reaktion der Laufzeitumgebung. Es wird eine NullPointerException ausgelöst und das Programm kann diese fangen und situationsbedingt reagieren. Wenn ich aber in meiner Erinnerung und den vielen in der Zwischenzeit erstellten Java Programmen forsche - eine sinnvolle Kompensation einer NullPointerException kann ich nirgends erkennen. Diese Exception kann auch in der Regel nicht kompensiert werden, denn es fehlt ein Objekt mit dem gearbeitet werden könnte. Davon abgesehen ist auch der Kontext der Verarbeitung verloren gegangen. So rauscht diese Exception durch die vielen implementierten Exception-Handler (catch und finally Blocks), um (hoffentlich) erst im allerletzten Moment von einem Main-Loop gefangen und berichtet zu werden.

Die Objective-C Runtime wurde dann aber irgendwann überarbeitet und seither führt der Zugriff auf ein nil Objekt nicht mehr zum sofortigen Programmabbruch. Anstatt eine Ausnahmesituation zu verursachen wird die Nachricht einfach an das nil Objekt weitergesendet. Da kein Objekt bekannt ist, ist dabei das Ergebnis leicht zu ermitteln, es lautet einfach immer nil. Der Programmfluss wird aber hier nicht unterbrochen. Tatsächlich erscheint es einfach so, als wären Teile der Anwendung funktionslos.

Das Thema wurde schon vor 18 Jahren kontrovers diskutiert. Natürlich ist es nicht korrekt, wenn Teile der Anwendung nicht instanziiert sind und die Referenzen ins Leere weisen.

Ist es aber korrekter, wenn auf einen nicht-initialisierten Zeiger mit einem Programmabbruch in Form einer unbehandelten NullPointerException reagiert wird?

Welches Verhalten ist für die Stabilität einer Programms wichtiger?

Schaue ich mir eines meiner aktuellen Programme an, finde ich in Java eine Vielzahl von Sonderbehandlungen für null Werte. Angefangen bei fehlenden Parametern bis hin zu optionalen Attributen in einem Datenobjekt muss ich an vielen Stellen null Werte gesondert behandeln. Dabei ist gleichzeitig aber weiterhin die häufigste Ursache für den Programmabbruch eines Java Programms eine unbehandelte weil unerwartet auftretende NullPointerException.

Vergleichbare Algorithmen in Objective-C Programm können aufgrund der entspannten Behandlung von nil Referenzen sehr viel prägnanter formuliert werden. Eine fehlender Wert führt in der Regel nicht zu falsch berechneten Werten oder einem Programmabbruch. Stattdessen habe ich das Gefühl, dass meistens das Ergebnis weiterhin “angemessen” ist und den Erwartungen entspricht.

Will ich in Objective-C jedoch ein Java-ähnliches Verhalten, kann ich einfach ein Assert-Makro erstellen. Dieses prüft den eine Objektreferenz und wirft eine NullPointerException, sollte die Referenz uninitialisiert sein.

Damit habe ich in Objective-C die Möglichkeit, beide Strategien zu nutzen. In der Regel nutze ich die entspannte nil Behandlung durch die Runtime, ist es aber irgendwo unabdingbar notwendig eine initialisierte Referenz vorzufinden, stelle ich das mit einer Assertion sicher.

Im Gegenzug aber ist der entspannte Umgang mit null Referenzen in Java nicht umsetzbar. Es können für viele Klassen dezidierte NULL Instanzen definiert werden. Deren Implementierung liefert jeweils für jeden Methodenaufruf entweder null Werte oder adäquate NULL Instanzen zurück. Das strenge Typsystem in Java macht es dabei jedoch notwendig, diese Sonderbehandlung in jede Methode jeder betroffenen Klasse einzubauen. Meist hat dies Auswirkungen auf die Klassenhierarchien. Anstelle einfacher Containerklassen verfügt man stattdessen plötzlich über abstrakte Basisklassen mit je einer Implementierung für initialisierte Element und einer zusätzlichen Implementierung für die Darstellung der NULL Instanz.

Der damit verbundene Aufwand für Entwicklung und Wartung ist dabei so hoch, dass ich dann meistens lieber die explizite Behandlung der null Werte implementiere und ein häufiges Auftreten von NullPointerExceptions im Rahmen der Qualitätssicherung akzeptiere.

Rückblickend ist meines Erachtens die Definition einer NullPointerException durch die Laufzeitumgebung eine gute Idee. Die zwangsweise durchgeführt Prüfung auf uninitialisierte Objekt-Referenzen hat aber sicherlich nicht zu einer Steigerung der Softwarequalität geführt. Stattdessen ist die von Testern und Anwendern “gefühlte” Qualität durch diesen Exception Typ gesenkt worden. Eine Meldung “der Knopf funktioniert nicht” ist für einen Anwender weniger schlimm als ein Problem der Art “das Programm stürzt einfach ab.”

In Java möchte ich eine Laufzeitumgebung, die nicht ohne Aufforderung (Assertion oder Pragma Einstellung) eine NullPointerException erzeugt.

Für Objective-C wünsche ich mir eine durch die Laufzeitumgebung eindeutig definierte und allgemein genutzte NullPointerException.

Die Welt ist nicht perfekt. Nirgendwo.

Update: Vielleicht ist mein Gedächtnis nicht mehr zu 100% intakt. Aber auch die Rückfragen bei Kollegen aus dieser Zeit hat keine eindeutige Aussage mehr geliefert. Offenbar war es auch damals schon unproblematisch, mit objc_msgSend() Nachrichten an nil Instanzen zu senden. Probleme ergaben sich damals wahrscheinlich hauptsächlich durch die NULL Pointer auf C-Strukturen, die wesentlich häufiger verwendet wurden als heute.

Weitere Artikel: