Java-Freud und Java-Leid

Seit Anfang diesen Jahres bin ich berufstechnisch von C++ auf Java umgeschwenkt. C++ ist ja bekannt dafür, die diversesten Fallstricke zu haben - und Java ist ja irgendwann mal mit dem Vorsatz angetreten, das alles besser zu machen. Unglücklicherweise ist das allenfalls partiell gelungen, denn ich finde allein schon im Arbeitsalltag immer wieder häßliche Fußangeln. So zum Beispiel folgendes Problem beim Aufruf überladener Methoden im Konstruktor:

Ich skizziere mal grob folgendes Szenario:
{syntaxhighlighter brush:java}
abstract class A {
int i;
public A() {
foo();
}
public void foo() {
i = 42;
}
}
class B extends A {
List daten;
public void foo() {
super.foo();
daten = new ArrayList();
daten.add("Foo!"); // Defaultwerte einfügen
}
public int len() {
return daten.size();
}
}

(...)

B b = new B();
System.out.println(b.len());
{/syntaxhighlighter}

Mit C++ wäre die Bauchlandung vorprogrammiert (no pun intended): Im Konstruktor kennt C++ die Vererbungshierarchie noch nicht und würde deshalb nur A.foo() aufrufen - beim Aufruf von b.len() wäre daten nicht initialisiert und es gäbe (aller Voraussicht nach, bei Verwendung von dynamischen Strukturen) einen segfault. Was passiert nun aber bei Java?

Java kennt bereits im Konstruktor die Vererbungshierarchie und ruft folgerichtig B.foo(). Was aber lieferte nun mein Code, der vom Prinzip her dem gezeigten Codeschnipsel entspricht? Ebenfalls eine NullPointerException!

Der Grund: Java verarbeitet nicht zuerst die Initialisierung aller Membervariablen, sondern erledigt auch das in Aufrufreihenfolge der Konstruktoren. Und diese Lautet: Erst Konstruktor der Vaterklasse rufen, dann eigene Variablen initialisieren, dann Rest vom eigenen Konstruktorcode ausführen (sofern vorhanden). Genau genommen arbeitet B.foo() also auf nicht initalisierten Werten (die es bei Java eigentlich nicht geben sollte)? Jedenfalls wird die Initialisierung aus B.foo() zunichte gemacht, da die Aufrufreihenfolge folgendermaßen aussieht:

  • Members von A initialisieren
  • Konstruktor von A ausfüren
  • Dieser ruft foo(), das überladene B.foo() wird ausgeführt
  • Members von B initialisieren, dabei wird daten auf null gesetzt (aua!)
  • Konstruktorcode von B ausführen (ist hier nicht vorhanden)

Fast perfekt, liebe Java-Designer... aber eben nur fast :-(

Edit: Erstaunlicherweise reproduziert der obige Code nicht mein Problem... obwohl ich in meinem Projektcode dieses Problem und im Debugger genau die oben beschriebene Aufrufreihenfolge nachvollziehen konnte. Das rehabilitiert die Java-Designer zwar, löste aber mein konkretes Problem nicht ;-) Hm, ein VM-Bug?