Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | Twitter | Lanyrd | Linkedin
 
HOME 

  OVERVIEW

  BY TOPIC
    JAVA
    C++

  BY COLUMN
    EFFECTIVE JAVA
    EFFECTIVE STDLIB

  BY MAGAZINE
    JAVA MAGAZIN
    JAVA SPEKTRUM
    JAVA WORLD
    JAVA SOLUTIONS
    JAVA PRO
    C++ REPORT
    CUJ
    OTHER
 

GENERICS 
LAMBDAS 
IOSTREAMS 
ABOUT 
CONTACT 
Polymorphic Methods and Constructors

Polymorphic Methods and Constructors
Polymorphe Methodenaufrufe und Konstruktoren
Warum man den Aufruf von non-final Methoden während der Initialisierung von Objekten vermeiden sollte

JavaSPEKTRUM, November 2003
Klaus Kreft & Angelika Langer

Dies ist das Manuskript eines Artikels, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im JavaSPEKTRUM erschienen ist.  Die übrigen Artikel dieser Serie sind ebenfalls verfügbar ( click here ).

 
 

Der Aufruf von polymorphen Methoden während der Initialisierungsphase eines Objekts kann zu Überraschungen führen. Deshalb gibt es die Regel: „Man soll keine non-final Methoden im Konstruktor aufrufen.“  In diesem Artikel wollen wir uns ansehen, warum diese Regel sinnvoll ist und wie genau die Initialisierung von Objekten in Java funktioniert.
 
 

Ein problematisches Beispiel

Beginnen wir mit einem Fallbeispiel, an dem man sehen kann, daß die Konstruktion von Objekten in Klassenhierarchien bisweilen zu Überraschungen führen kann. Wir betrachten eine Hierarchie von Klassen, die das Personal einer Firma abbilden: eine Superklasse StaffMember für „normale“ Mitarbeiter und eine abgeleitete Klasse BoardMember für die Mitglieder des Vorstands. Der wesentliche Unterschied besteht darin, daß Mitglieder des Vorstands einen Bonus auf ihre Entlohnung bekommen; deshalb hat die Klasse BoardMember ein zusätzliches Feld. Hier eine verkürzte Form dieser Klassen:

public class StaffMember {
    private Person person;
    private long salary;
    private OrgUnit department;

    public StaffMember(Person p, long s, OrgUnit o) {
      person = p;
      salary = s;
      department = o;
      o.addMember(this);
    }
    public long getCompensation() {
      return salary;
    }
}
public class BoardMember extends StaffMember {
    private long bonus;

    public BoardMember(Person p, long s,long b, OrgUnit o) {
    super(p,s,o);
    bonus = b;
    }
    public long getCompensation() {
       return super.getCompensation()+bonus;
    }
}

Neben den hier gezeigten Klassen gibt es ein Klasse OrgUnit, welche die Abteilungen der Firma repräsentiert. Was diese Klasse genau tut, ist hier unerheblich.  Wichtig ist in unserem Zusammenhang nur, daß jeder Mitarbeiter bei seiner Organisationseinheit registriert wird.  Das geschieht im Konstruktor der Klasse StaffMember über die Methode OrgUnit.addMember(). Hier eine verkürzte Form dieser Klasse OrgUnit:

public class OrgUnit {
 private String name;
 private Set members = new TreeSet();
 private long cost;

 public final void addMember(StaffMember m) {
  members.add(m);
  cost += m.getCompensation();
 }
}

Nun ergibt sich folgender Effekt: wenn man Angestellte und Vorstandsmitglieder konstruiert und den verschiedenen Organisationseinheiten zuordnet, wird man feststellen, daß etwas mit den Gehaltsberechnungen nicht stimmt.  Hier ein Testprogramm:

public final class Corporation {
 private Set units = new HashSet();

 public Corporation()  {
  OrgUnit dep = new OrgUnit("Development");
  units.add(dep);
  new StaffMember(new Person("Helmut", "Hunter"),3000000,dep);
  new StaffMember(new Person("Lorrie", "Lindon"),3000000,dep);
  new StaffMember(new Person("Jeremy", "Jordan"),4000000,dep);

  dep = new OrgUnit("Board");
  units.add(dep);
  new BoardMember(new Person("Vito", "Voracious"),100000000,500000,dep);
  new BoardMember(new Person("Mona", "Malicious"),100000000,300000,dep);
 }
 public static void main(String[] args) {
    Corporation corp = new Corporation();
    System.out.println(corp);
 }
}

Hier die Ausgabe, die das Testprogramm produziert, unter der Annahme, daß entsprechende toString()-Methoden implementiert wurden:

corporation.OrgUnit:
name: Development
cost: 100000 USD
 Helmut Hunter ( Development ) [ 30000 USD ] ID: #28904249
 Jeremy Jordan ( Development ) [ 40000 USD ] ID: #26208195
 Lorrie Lindon ( Development ) [ 30000 USD ] ID: #12115735

corporation. OrgUnit:
name: Board
cost: 2000000 USD
 Mona Malicious ( Board ) [ 1003000 USD ] ID: #20876681
 Vito Voracious ( Board ) [ 1005000 USD ] ID: #29240677

Die Vorstandsmitglieder bekommen zwar ihren Gehaltsbonus, aber in den Gesamtkosten der Organisationseinheit ist offenbar nur das Grundgehalt berücksichtigt.  Wie kann das passieren?
 

Problemanalyse

Das Problem entsteht bei der Konstruktion der Vorstandsmitglieder.  Der Konstruktor der Klasse BoardMember ruft zunächst einmal einen Superklassenkonstruktor auf.  Danach wird das Bonus-Feld initialisiert.  Im Superklassenkonstruktor, d.h. im Konstruktor der Klasse StaffMember, werden die Felder der Klasse StaffMember initialisiert und danach wird die addMember()-Methode der Organisationseinheit aufgerufen.  In der addMember()-Methode der Klasse OrgUnit wird das BoardMember-Objekt in den Set der Mitarbeiter eingehängt und die Gesamtkosten der Abteilung werden um das Gehalt des neuen Mitglieds erhöht.

Damit sind ohne jeden Zweifel beim Verlassen des BoardMember-Konstruktors alle Felder des BoardMember-Objekts korrekt initialisiert. Die Methode getCompensation() der Klasse BoardMember wird korrekt das Gehalt inklusive Bonus berechnen. Warum stimmen dann die Gesamtkosten der Organisationseinheit nicht?  Dort wurde doch auch die Methode getCompensation() der Klasse BoardMember aufgerufen.

Das Problem liegt darin, daß die Methode getCompensation() während der Konstruktion des BoardMember-Objekts aufgerufen wird, und zwar zu einem Zeitpunkt, zu dem das BoardMember-Objekt noch gar nicht fertig ist.  Ein Teil des Objekts, nämlich das Bonus-Feld, ist zu jenem Zeitpunkt noch nicht initialisiert und hat noch immer den Wert 0. Die Methode getCompensation() greift auf das noch nicht initialisierte Bonus-Feld zu und berechnet das Gehalt folgerichtig mit Bonus 0.

Um das nachzuvollziehen sehen wir uns einmal im Detail an, wie Objekte in Java konstruiert werden.
 

Objektkonstruktion in Java

Die Konstruktion eines Objekts läuft in Java wie folgt ab:
  1. Speicherbeschaffung. Das Laufzeitsystem beschafft Speicher in der richtigen Menge, d.h. soviel Speicher, wie für die Felder des Objekts gebraucht wird.  Der Speicher ist zunächst einmal völlig uninitialisiert und die Felder haben zufällige Inhalte.
  2. Defaultinitialisierung.  Das Laufzeitsystem initialisiert den beschafften Speicher mit vordefinierten Initialwerten, d.h. alle Felder des Objekts werden mit Defaultwerten belegt.  Die Initialwerte sind abhängig von Typ der Felder und können der Tabelle „Defaultwerte“ entnommen werden.
  3. Superklassenkonstruktor. Ein Konstruktor der Superklasse wird gerufen. Das ist entweder implizit der Defaultkonstruktor oder explizit ein Konstruktor mit Argumenten, der über super(…) angestoßen wird. Hier geht eine Rekursion los, die sukzessive alle Superklassenkonstruktoren bis hinauf zu Object aufruft.  Danach sind alle geerbten Felder initialisiert.
  4. Explizite Initialisierung. Alle Felder, die in der betreffenden Klasse definiert sind und für es eine Initialisierung gibt, bekommen ihre expliziten Initialwerte zugewiesen. Danach sind alle Felder (die geerbten und die eigenen Felder) initialisiert.
  5. Konstruktor-Body. Die eigentlichen Anweisungen des Konstruktors werden ausgeführt.
     
    Type Default Value
    byte (byte) 0
    short (short) 0
    int 0
    long 0L
    float 0.0f
    double 0.0d
    char  '\u0000'
    boolean false
    reference null
    Tabelle 1: Defaultwert
Es gibt noch ein paar zusätzliche Details, die wir der Einfachheit halber oben unterschlagen haben:
  • Alternativer Konstruktor. In Phase (3) kann anstelle des Superklassenkonstruktors auch ein anderer Konstruktor derselben Klasse über this(…) aufgerufen werden.  Dieser alternative Konstruktor führt dann seinerseits noch einmal eine Phase (3) durch, d.h. er ruft einen Superklassenkonstruktor oder einen weiteren alternativen Konstruktor derselben Klasse auf. Nach der Phase (3), d.h. wenn all Superklassenkonstruktoren bis hinauf zum Konstruktor von Object ausgeführt worden sind, erfolgt in Phase (4) die explizite Initialisierung der Felder der Klasse und in Phase (5) die Ausführung der Anweisungen im Konstruktor-Body. Damit ist der alternative Konstruktor mit seiner Arbeit fertig und der Kontrollfluß kehrt zum ursprünglichen Konstruktor zurück.  Im ursprünglichen Konstruktor geht es nun nicht mit Phase (4), sondern mit Phase (5) weiter, weil Phase (4),  die explizite Initialisierung der Felder der Klasse, bereits im alternativen Konstruktor erledigt wurde.
  • Instance Initializer. Alle Initialisierungen in Phase (4) werden in der Reihenfolge gemacht, in der sie in der Klassendefinition auftauchen. Dabei werden nicht nur die expliziten Initialisierungen der Felder vorgenommen (die sogenannten „instance variable initializer“), sondern es werden auch die relativ ungebräuchlichen „instance initializer“ ausgeführt (siehe / JLS / §8.8.5.1).  Instance Initializer werden eigentlich nur in anonymen inneren Klassen verwendet, weil anonyme Klassen keinen Namen haben und man deshalb keinen Konstruktor für eine anonyme Klasse schreiben kann. Anstelle eines Defaultkonstruktors schreibt man für eine anonyme Klasse einen Instance Initializer, falls einer benötigt wird.
Betrachten wir ein einfaches Beispiel, an dem man den gesamten Vorgang der Initialisierung studieren kann:

class Elem2D {
  private int x = -1;
  private int y;
  public Elem2D() { y = 1; }
}

class ColoredElem2D extends Elem2D {
  private int color = 0xFF00FF;
}

...
ColoredElem2D elem = new ColoredElem2D();
...

Wenn ein Objekt vom Subtyp ColoredElem2D erzeugt wird, dann erfolgt die Konstruktion in folgenden Schritten:

  • Speicherbeschaffung für alle 3 Felder der Klasse ColoredElem2D sowie für alle von Object geerbten Felder
  • Defaultinitialisierung aller Felder mit 0
  • Aufruf des Defaultkonstruktors von ColoredElem2D
    • Aufruf von super(), d.h. des Defaultkonstruktors von Elem2D
      • Aufruf von super(), d.h. des Defaultkonstruktors von Object
      • Explizite Initialisierung der Felder von Object
      • Konstruktor-Body des Defaultkonstruktors von Object
    • Explizite Initialisierung der Felder von Elem2D , d.h. hier wird x  mit -1 initialisiert
    • Konstruktor-Body des Defaultkonstruktors von Elem2D, d.h. hier wird y mit 1 initialisiert
  • Explizite Initialisierung der Felder von ColoredElem2D, d.h. hier wird color initialisiert
  • Konstruktor-Body des Defaultkonstruktors von ColoredElem2D; ist in diesem Falle leer
Betrachten wir ein komplizierteres Beispiel mit Instance Initializer und dem Aufruf eines alternativen Konstruktors über this(...):

class Elem3D {
  private int x;
  private int y;
  private int z = 3; // instance variable initializer
  { x = z; }    // instance initializer
  public Elem3D()
  { y = (x+z)*2}         // constructor body
}

class ColoredElem3D extends Elem3D {
  private int color;

  ColoredElem3D(int x, int y, int z, int col) {
    super(x,y,z);
    color = col ;
  }
  ColoredElem3D() {
    this(x,y,z,0xFF00FF);
  }
}

...
ColoredElem3D elem = new ColoredElem3D();
...

Wenn ein Objekt vom Subtyp ColoredElem3D erzeugt wird, dann erfolgt die Konstruktion in folgenden Schritten:

  • Speicherbeschaffung für alle 4 Felder der Klasse ColoredElem3D sowie für alle von Object geerbten Felder
  • Defaultinitialisierung aller Felder mit 0
  • Aufruf des Defaultkonstruktors von ColoredElem3D
    • Aufruf von this(...), d.h. eines alternativen Konstruktors von ColoredElem3D
      • Aufruf von super(...), d.h. eines Konstruktors von Elem3D
        • Aufruf von super(), d.h. des Defaultkonstruktors von Object
        • Explizite Initialisierung der Felder von Object
        • Konstruktor-Body des Defaultkonstruktors von Object
      • Explizite Initialisierung der Felder von Elem3D

      • Es wird zuerst z mit 3 initialisiert, so wie im Initialisierungsausdruck angegeben. Dann wird der Instance Initializer ausgeführt; dabei wird x mit dem Inhalt von z initialisiert.
      • Konstruktor-Body des Defaultkonstruktors von Elem3D, d.h. hier wird y initialisiert
    • Explizite Initialisierung der Felder von ColoredElem3D; in diesem Falle ist nichts zu tun
    • Konstruktor-Body des alternativen Konstruktors von ColoredElem3D, hier wird color initialisiert
  • Konstruktor-Body des Defaultkonstruktors von ColoredElem3D; ist in diesem Falle leer
Wie man sieht, kann die Abfolge der einzelnen Initialisierungen beliebig kompliziert werden.  Wenn man sich bezüglich der Reihenfolge nicht sicher ist, dann empfiehlt es sich, Abhängigkeiten zwischen den Initialisierungsschritten zu vermeiden. In obigem Beispiel wurde zum Zwecke der Veranschaulichung ganz gezielt die Initialisierungsreihenfolge für die Felder ausgenutzt.  Das ist nicht unbedingt zur Nachahmung empfohlen.  Wenn Abhängigkeiten zwischen den Initialisierungen der einzelnen Felder nicht vermieden werden können, dann ist häufig am besten, alle Felder im Konstruktor-Body zu initialisieren.

Kehren wir zu unserem Ausgangspunkt zurück. Unser Problem in  der Fallstudie stammt vom Aufruf einer polymorphen Methode während der Konstruktion.  Sehen wir uns also an, wie polymorphe Methoden im Konstruktionsprozeß behandelt werden.
 

Aufruf polymorpher Methoden während der Konstruktion

Während der Konstruktion können Methoden aufgerufen werden. Dabei ist es auch erlaubt, nicht-statische Methoden der eigenen Klasse aufzurufen. Von Interesse für unser Problem sind dabei diejenigen Methoden eine Klasse, die in Subklassen redefiniert werden können, d.h. alle nicht-statischen Methoden, die weder private noch final sind. Solche redefinierbaren Methoden können in einem Superklassenkonstruktor aufgerufen werden, und das ist auch nicht weiter problematisch, solange nur Superklassenobjekte erzeugt werden.  Wenn ein Superklassenobjekt konstruiert wird, dann wird während der Konstruktion die Superklassenvariante der fraglichen Methode aufgerufen, und es passiert genau das, was man erwartet.

Der Superklassenkonstruktor wird aber nicht nur für die Konstruktion von Superklassenobjekten verwendet, sondern wird auch während der Konstruktion von Subklassenobjekten aufgerufen. Wenn man sich die Abfolge der Aktionen während der Konstruktion eines Subklassenobjekts noch einmal anschaut, dann stellt man fest, daß entweder explizit über super(...) oder implizit vom Compiler ein Konstruktor der Superklasse angestoßen wird. Dann ergibt sich eine interessante Situation, wenn dieser Superklassenkonstruktor eine redefinierte Methode aufruft: dem Compiler stehen für den Aufruf sowohl die Variante der Methode aus der Superklasse als auch die Variante der Methode aus der Subklasse zur Verfügung.  Er entscheidet, welche der beiden Methoden gerufen wird, abhängig vom Typ des Objekts, auf dem die Methode angestoßen wird.  Während der Konstruktion eines Subklassenobjekts wird er also die Subklassenvariante der redefinierten Methode aufrufen. Die Subklassenvariante greift unter Umständen auf subklassenspezifische Felder zu. Nun ist die Frage: in welchem Zustand sind diese Subklassenfelder zum Zeitpunkt des Aufrufs? Schließlich befinden wird uns gerade mitten im Prozeß der Initialisierung des Subklassenobjekts. Sind alle Felder schon ordnungsgemäß initialisiert, wenn die Subklassenvariante der Methode gerufen wird, oder nicht? Oder läßt sich das vielleicht gar nicht vorhersagen?

Ob die möglicherweise benutzen Felder des Subklassenobjekts bereits ordnungsgemäß initialisiert sind, hängt davon ab, zu welchem Zeitpunkt während der Konstruktion die fragliche Methode aufgerufen wird. Sehen wir uns an, was da genau passiert. Betrachten wir dazu ein einfaches Beispiel:

class Elem2D {
  private int x = -1;
  private int y;
  public Elem2D() { y = calculateY(); }
  protected int calculateY() { return 2*(x+1) ; }
}

class Elem3D extends Elem2D {
  private int z = 1;
  protected int calculateY() { return 2*(x+z); }
}

...
Elem3D elem = new Elem3D();
...

Wenn ein Objekt vom Subtyp Elem3D erzeugt wird, dann erfolgt die Konstruktion wie bereits zuvor erläutert in folgenden Schritten:

  • Speicherbeschaffung für alle Felder
  • Defaultinitialisierung aller Felder mit 0
  • Aufruf des Defaultkonstruktors von Elem3D
    • Aufruf von super(), d.h. des Defaultkonstruktors von Elem2D
      • Aufruf von super(), d.h. des Defaultkonstruktors von Object
      • Explizite Initialisierung der Felder von Object
      • Konstruktor-Body des Defaultkonstruktors von Object
    • Explizite Initialisierung der Felder von Elem2D , d.h. hier wird x  mit -1 initialisiert
    • Konstruktor-Body des Defaultkonstruktors von Elem2D, d.h. hier wird y mit dem Ergebnis von  calculateY() initialisiert
  • Explizite Initialisierung der Felder von Elem3D, d.h. hier wird z mit 1 initialisiert
  • Konstruktor-Body des Defaultkonstruktors von Elem3D; ist in diesem Falle leer
Die Methode calculateY(), die hier gerufen wird, ist die Variante aus der Subklasse Elem3D.  Diese greift auf das Feld z zu, das aber zu diesem Zeitpunkt noch nicht explizit initialisiert wurde und noch immer den Wert 0 hat.  Die Berechnung wird daher ein vermutlich unerwartetes Ergebnis liefern. Das liegt daran, daß die Methode auf einem halb initialisierten Objekt aufgerufen wird.  Das läßt sich auch gar nicht verhindern, weil die Methode calculateY() aus dem Superklassenkonstruktor heraus aufgerufen wird, und der Superklassenkonstruktor läuft immer als erstes ab, lange bevor die Felder der Subklasse initialisiert werden.  Es handelt sich also um einen systematischen, reproduzierbaren Effekt.  Der Ablauf der Initialisierung von Objekten ist in Java genau so wie oben beschrieben festgelegt und das bedeutet, daß überschriebene Methoden, die aus Superklassenkonstruktoren heraus aufgerufen werden, auf halbfertigen Subklassenobjekten arbeiten.

Die Überraschung rührt daher, daß die Verwendung von halbfertigen Objekten nicht den Konventionen der Objektorientierung entspricht. Die Grundidee in der Objektorientierung ist eigentlich, daß jede Methode eines Objekts das Objekt von einem konsistenten Zustand in einen anderen konsistenten Zustand überführt.  Der Konstruktor hat dabei die besondere Aufgabe, den ersten konsistenten Zustand herzustellen. Alle Methode, außer dem Konstruktor, können immer davon ausgehen, daß sie auf einem konsistenten Objekt aufgerufen werden. Das genau ist aber in unserem Beispiel nicht der Fall.  Die Methode calculateY() der Klasse Elem2D wird auf einem inkonsistenten, halbfertigen Objekt aufgerufen. Darauf ist die Methode nicht vorbereitet und das Ergebnis dürfte überraschend oder gar fehlerhaft, sein.  Was kann man in solchen Fällen tun?

Theoretisch könnte man die fragliche Methode darauf vorbereiten, mit inkonsistenten Objekte fertig zu werden.  Das macht aber nur in seltenen Ausnahmefällen Sinn.  Bestenfalls könnte man eine Konsistenzprüfung machen und eine Exception werfen, wenn das Objekt inkonsistent ist. Das hätte aber zur Folge, daß jede Konstruktion von Subklassenobjekten grundsätzlich fehlschlägt. Keine gute Idee.

Aus diesen Überlegungen ergibt sich die Empfehlung, die in vielen Büchern zu finden ist (siehe / VER /, / HAG /, / BLO /):

Man soll niemals überschreibbare Methoden in einem Konstruktor aufrufen .
Die Regel ist sinnvoll und sollte auf jeden Fall beherzigt werden.  Haben wir diese Regel in unserem problematischen Beispiel der Organisationseinheit mit ihren Mitarbeitern verletzt? Kehren wir zur Fallstudie zurück.

Zurück zum problematischen Beispiel

Sehen wir uns die Konstruktoren aus unserer Fallstudie noch einmal an.

public class StaffMember {
    private Person person = null;
    private long salary = 0;
    private OrgUnit department = null;

    public StaffMember(Person p, long s, OrgUnit o) {
      person = p;
      salary = s;
      department = o;
      o.addMember(this);
    }
    public long getCompensation() {
      return salary;
    }
}
public class BoardMember extends StaffMember {
    private long bonus = 0;

    public BoardMember(Person p, long s,long b, OrgUnit o) {
    super(p,s,o);
    bonus = b;
    }
    public long getCompensation() {
       return super.getCompensation()+bonus;
    }
}

Wenn ein Objekt vom Subtyp BoardMember erzeugt wird, dann erfolgt die Konstruktion wie bereits zuvor erläutert in folgenden Schritten:

  • Speicherbeschaffung für alle Felder
  • Defaultinitialisierung aller Felder mit 0 bzw. null
  • Aufruf des Konstruktors von BoardMember
    • Aufruf von super(...), d.h. eines Konstruktors von StaffMember
      • Aufruf von super(), d.h. des Defaultkonstruktors von Object
      • Explizite Initialisierung der Felder von Object
      • Konstruktor-Body des Defaultkonstruktors von Object
    • Explizite Initialisierung der Felder von StaffMember; entfällt in diesem Fall
    • Konstruktor-Body des Konstruktors von StaffMember, d.h. hier werden person, salary und department  mit ihren eigentlichen Initialwerten initialisiert und es wird die Methode OrgUnit.addMember() gerufen
  • Explizite Initialisierung der Felder von BoardMember; entfällt in diesem Fall
  • Konstruktor-Body des Defaultkonstruktors von BoardMember; hier wird bonus mit seinem eigentlichen Initialwert initialisiert
Eigentlich haben wir nirgendwo gegen die Regel verstoßen, daß überschreibbare Methoden nicht in Konstruktoren aufgerufen werden sollen.  Die einzige Methode, die im Konstruktor gerufen wird, ist die Methode addMember() der Klasse OrgUnit. Aber diese Methode ist final und damit nicht überschreibbar. Wo ist das Problem? Hier zur Erinnerung noch einmal die Klasse OrgUnit:

public class OrgUnit {
 private String name;
 private Set members = new TreeSet();
 private long cost;

 public final void addMember(StaffMember m) {
  members.add(m);
  cost += m.getCompensation();
 }
}

Das Problem kommt daher, daß die Regel „Man soll niemals überschreibbare Methoden in einem Konstruktor aufrufen.“ etwas verkürzt ist. Eigentlich müßte es heißen: „Man soll niemals überschreibbare Methoden der eigenen Klasse in einem Konstruktor aufrufen – auch nicht indirekt.“ In unserer Fallstudie haben wird die this-Referenz als Argument an eine Methode einer anderen Klasse übergeben, die die this-Referenz verwendet hat, um eine überschreibbare Methode unserer Klasse aufzurufen. Wir haben also auf Umwegen eine überschreibbare Methode unserer eigenen Klasse während der Konstruktion aufgerufen, und das führt dann zu Problemen.

Außerdem gibt die Methode getCompensation() die this-Referenz an die Methode TreeSet.add() weiter.  Was macht die add()-Methode der TreeSet-Klasse eigentlich mit unserem unfertigen Objekt?  Sie wird die compareTo()-Methode unserer Subklasse rufen, die wiederum das noch nicht initialisierte Bonus-Feld für den Vergleich heranziehen wird, und auch das wird zu Fehlern führen, die vielleicht nicht sofort, sondern erst irgendwann später auffallen werden.

Was macht man also, wenn man eine solche problematische Situation entdeckt hat? Leider gibt es meistens keine einfache Lösung, die ohne ein Redesign der Klassenhierarchie und der an der Konstruktion beteiligten Methoden auskäme. Es empfiehlt sich also, bereits beim Design einer non-final-Klasse darauf zu achten, daß keine überschreibbaren Methoden der eigenen Klasse direkt oder indirekt aus dem Konstruktor heraus aufgerufen werden.  Wenn man in Versuchung ist, es dennoch zu tun, dann sollte man sein  Design sofort noch einmal überdenken.

Nun ist die Weitergabe der this-Referenz während der Konstruktion durchaus gängige Praxis, typischerweise im Zusammenhang mit Registrierungen, die während der Konstruktion durchgeführt werden.  In dynamischen System kommt es häufiger vor, daß sich Objekte während der Konstruktion bei einer anderen logischen Einheit im System anmelden, damit sie später unter bestimmten Umständen zurückgerufen werden; das bezeichnet man als Callback-Registrierung. Dabei wird, wie in unserem Beispiel, die this-Referenz auf ein noch unfertiges Objekt zur Registrierung weiter gegeben.

Callback-Registrierung während der Konstruktion ist solange unproblematisch, wie keine Vererbung im Spiel ist. Dann kann man die Registrierung als letzte Anweisung im Konstruktor-Body machen, also dann, wenn das Objekt bereits fertig initialisiert ist. Sobald Subklassen existieren, kann es unter Umständen nicht mehr gewährleistet werden, daß die Registrierung erst ganz am Ende statt findet.  Dann wird in der Tat ein halbfertiges Objekt registriert.  Solange die this-Referenz nur herumgereicht und irgendwo gespeichert wird, ist das ungefährlich. Man muß aber darauf achten, daß die Methode, die die this-Referenz auf ein noch unfertiges Objekt bekommt, diese Referenz nicht zum Aufruf von Methoden oder zum Zugriff auf die Felder des Objekts verwendet.  Bei einer typischen Callback-Registierung ist das auch nicht der Fall.  Methoden des registrierten Objekts werden typischerweise erst sehr viel später, nach Beendigung der Konstruktion, gerufen.

In unserem Beispiel haben wir genau das mißachtet. Wir haben die this-Referenz auf ein noch unfertiges Objekt an die Registrierungsmethode OrgUnit.addMember() übergeben und die hat sofort eine Methode des halbfertigen Objekts aufgerufen, was zu den erläuterten Problemen geführt hat.
 

Zusammenfassung

Konstruktoren arbeiten während der Konstruktion auf unfertigen Objekten und sollten deshalb äußerst vorsichtig sein beim Zugriff auf möglicherweise noch nicht initialisierte Teile des Objekts.  Problematisch ist es  insbesondere, wenn Superklassenkonstruktoren überschreibbare Methoden (d.h. non-final oder non-private Methoden) der eigenen Klasse direkt oder indirekt aufrufen.  Die Superklassenkonstruktoren werden relativ früh während der Objekterzeugung gerufen und finden daher ein unfertiges Objekt vor.  Wenn sie überschriebene Methoden aufrufen, dann greifen diese Methoden unter Umständen die auf die noch nicht initialisierten Felder des unfertigen Objekts zugreifen. Das ist normalerweise ein Problem, das man nur vermeiden kann, indem man während der Objekterzeugung weder direkt noch indirekt überschreibbare Methoden aufruft.
 

Leserzuschrift

Ganz offensichtlich ist das ganze Problem keineswegs akademischer Natur, wie der folgende Leserbrief zeigt:
 
Leserbrief
 
Hallo Herr Kreft,

ihr Artikel in "Java Spektrum" 6/2003 hat mir sehr gut gefallen, weil er über eine wirklich üble "Falle" aufklärt, in die auch professionelle Entwickler tappen. Deshalb hier noch ein ergänzendes Beispiel für genau diesen Fehler, welches seit mehreren releases in Suns SDK schlummert, und zwar in der Swing-API.

In den ersten Swing-releases reagierten Instanzen von JInternalFrame nicht auf das event JInternalFrameOpen und waren per default sichtbar. Um ein JInternalFrameOpen-event zu erzwingen, änderte man ab SDK 1.2.1 das default-Verhalten indem man am Ende des default-constructors (quick and very dirty!!!)einen Aufruf der nicht-finalen Methode setVisible( false ) einfügte. Damit waren die Entwickler gezwungen einen expliziten Aufruf setVisible( true ) zu verwenden, in welchem dann das event JInternalFrameOpen generiert wird.

Wenn nun in von JInternalFrame abgeleiteten Klassen die Methode setVisible( boolean ) überschrieben wird, was üblich ist, um aktuelle Parameter zu setzen oder den Frame zu registrieren, schlägt die Falle zu! Es kommt nämlich zu einem Aufruf der Methode setVisible( boolean ) der child-Klasse im super-constructor ohne Initialisierung der Klassenvariablen der child-Klasseninstanz, was mitunter zu wahrhaft seltsamen Ergebnissen und einer aufwendigen Fehlersuche führt. Ein mögliches workaround: Ein  expliziter Aufruf von setVisible( false) der child-Klasse an einer "ungefährlichen" Stelle, wobei es mitunter schwierig ist, eine solche zu finden.

Trotz direkter Meldung an die Swing-Entwickler und Beschreibung des Fehlers in Suns Java-Developer-Forum findet sich dieser Fehler auch noch im SDK 1.4.1 . Vielleicht hilft diese Info ja, ihn ein wenig mehr zu "popularisieren".

Mit freundlichen Grüßen!

Helmut Schwigon

Literaturverweise

 
/KRE/ Die Implementierung von clone(), Teil 1 und 2 
Klaus Kreft & Angelika Langer
JavaSpektrum, September 2002 und November 2002 
http://www.AngelikaLanger.com/Articles/EffectiveJava/05.Clone-Part1/05.Clone-Part1.html
http://www.AngelikaLanger.com/Articles/EffectiveJava/06.Clone-Part2/06.Clone-Part2.html
/KRE1/ Unveränderliche Typen in Java (Teil 1-2) 
Klaus Kreft & Angelika Langer
JavaSpektrum, März + Juli 2003
http://www.AngelikaLanger.com/Articles/EffectiveJava/08.Immutability-Part1/08.Immutability-Part1.html
http://www.AngelikaLanger.com/Articles/EffectiveJava/09.Immutability-Part2/09.Immutability-Part2.html
/KRE2/ final Klassen und final Methoden
Klaus Kreft & Angelika Langer
JavaSpektrum, September 2003
http://www.AngelikaLanger.com/Articles/EffectiveJava/10.NonPolymorphicClasses/10.NonPolymorphicClasses.html
/VER/ The Elements of Java Style
Al Vermeulen, et.al.
Cambridge University Press, 2000
Rule 81, p. 70
/HAG/  Practical Java 
Peter Haggar
Addison Wesley, 2000
Praxis 68, p. 239
/BLO/ Effective Java 
Joshua Bloch
Addison Wesley, 2001
Item 15, p. 80
/JLS/ Java Language Specification, 2nd Edition
http://java.sun.com/docs/books/jls/second_edition/html/j.title.doc.html

 
 
 

If you are interested to hear more about this and related topics you might want to check out the following seminar:
Seminar
 
Effective Java - Advanced Java Programming Idioms 
4 day seminar ( open enrollment and on-site)
 
  © Copyright 1995-2008 by Angelika Langer.  All Rights Reserved.    URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/11.PolyMethodsInCtor/11.PolyMethodsInCtor.html  last update: 26 Nov 2008