Konstruktoren

Konstruktoren

Konstruktoren (constructors)

  • initialisieren Objekte bei der Erzeugung
  • werden nicht vererbt und sind keine Methoden
  • können überladen werden
  • können sich gegenseitig aufrufen und die Konstruktoren der direkten Superklasse rufen

Syntax für den Konstruktor einer Klasse T ist
<visibility> T(<parameter List>) { .... }

Der Aufruf anderer Konstruktoren in derselben Klasse erfolgt mit dem Schlüsselwort this. Konstruktoren von Superklassen können mit super gerufen werden. Es handelt sich aber bei this und super nicht etwa um Objektreferenzen, sondern um eine spezielle Notation, um andere Konstruktoren zu rufen.

Im Class-File, das der Compiler aus der Klasse erzeugt, finden sich die Konstruktoren nicht mit dem Namen, der im Source verwendet wurde wieder, sondern sie haben den speziellen Namen <init>. Auch hieran kann man erkennen, dass Konstruktoren nichts mit Methoden zu tun haben.

Konstruktoren: Beispiel

public class Mitarbeiter {

    public String name;
    public double gehalt;
    public Date geboren;

    public Mitarbeiter(String name, double gehalt,
            Date geboren) {

        this.name = name;
        this.gehalt = gehalt;
        this.geboren = geboren;
    }
}

Konstruktoren werden in UML im selben Bereich wie Methoden notiert, sie werden aber unterstrichen und heißen genauso wie die Klasse.

Der Default-Konstruktor

  • Default-Konstruktor (default constructor) ⇒ Konstruktor ohne Parameter
  • Wenn man in einer Java-Klasse keinen Konstruktor angibt, wird vom Compiler automatisch der Default-Konstruktor mit Sichtbarkeit public erzeugt
  • Sobald man einen weiteren Konstruktor zur Klasse hinzufügt, wird kein Default-Konstruktor mehr automatisch erzeugt

Der Wegfall des Default-Konstruktors beim Hinzufügen weiterer Konstruktoren ist eine häufige Quelle für Fehler im Programm. Vor allem bei Klassen, die Teil eines APIs sind, kann man hierdurch die Kompatibilität brechen, ohne es zu merken. Daher ist es dringend zu empfehlen, den Default-Konstruktor (so gewünscht) immer explizit hinzuschreiben und die Compiler-Magie nicht zu verwenden.

Überladen von Konstruktoren

  • Konstruktoren können wie Methoden überladen werden
  • Die Argumentlisten müssen sich unterscheiden
  • Mit this in der ersten Zeile kann man andere Konstruktoren aufrufen

Beispiele

public Mitarbeiter(String name, double gehalt, Date geboren)
public Mitarbeiter(String name, double gehalt)
public Mitarbeiter(String name)
public class Mitarbeiter {
    public String name;
    public double gehalt;
    public Date geboren;
    public Mitarbeiter(String name, double gehalt, Date geboren) {
        this.name = name;
        this.gehalt = gehalt;
        this.geboren = geboren;
    }

    public Mitarbeiter(String name, double gehalt) {
        this(name, gehalt, null);
    }

    public Mitarbeiter(String name) {
        this(name, 0.0d, null);
    }
}

Mit dem Schlüsselwort this kann ein Konstruktor die Arbeit an einen anderen Konstruktor der Klasse delegieren. Der this-Aufruf muss das erste Statement im Konstruktor sein, später ist er nicht möglich.

Aufruf des Superklassen-Konstruktors

  • Um den Konstruktor der Superklasse aufzurufen, kann man super() verwenden
  • super() muss in der ersten Zeile des Konstruktors angegeben werden
  • Parameter für den Konstruktor der Elternklasse können angegeben werden
  • Macht man keinen expliziten Aufruf, fügt der Compiler einen implizites super() ein
    • Gibt es in der Elternklasse keinen Default-Konstruktor bekommt man einen Compiler-Fehler
    • Sind alle Konstruktoren einer Klasse privat, kann man von ihr nicht ableiten und von außen keine Objekte von ihr erzeugen
public class Manager extends Mitarbeiter {

    public String abteilung;

    public Manager(String name, double gehalt,
            Date geboren, String abteilung) {

        super(name, gehalt, geboren);
        this.abteilung = abteilung;
    }
}

Mit dem Schlüsselwort super kann ein Konstruktor die Arbeit an einen Konstruktor der Superklasse delegieren. Der super-Aufruf muss das erste Statement im Konstruktor sein, später ist er nicht möglich.

Aufrufhierarchie von Konstruktoren

Möglichkeiten für Konstruktoren

  • Man hat explizit ein this() geschrieben und delegiert damit an einen anderen Konstruktor derselben Klasse
  • Man hat explizit ein super() geschrieben und delegiert an einen Konstruktor der Superklasse
  • Man hat nichts geschrieben und der Compiler hat automatisch ein super() erzeugt
  • Mindestens ein Konstruktor enthält kein this(), da man andernfalls einen Kreis bilden würde

Was folgt daraus?

public class A extends Object {
    protected int i;

    public A(int i) {
        this.i = i;
    }
}

public class B extends A {
    protected int k;
    public B(int k) {
        this(k, 0);
    }
    public B(int k, int i) {
        super(i);
        this.k = k;
    }
}

Die Regeln für super() und this() führen dazu, dass bei jeder Objekterzeugung die gesamte Vererbungshierarchie hinauf alle Konstruktoren bis hin zur Klasse Object gerufen werden, d. h. es wird sichergestellt, dass alle Objekte korrekt initialisiert wurden.

  • Die Aufrufe der Konstruktoren der Superklassen erfolgen bevor der eigene Konstruktor ausgeführt wird, da this() und super() als erste Zeile im Konstruktor stehen müssen
  • Die geerbten Variablen werden vor den eigenen Variablen initialisiert und bevor eigene Anweisungen durchgeführt werden können

Ein verbreiteter Denkfehler ist es anzunehmen, dass durch die Vererbung bei der Erzeugung eines Objekts der Subklasse, Objekte der Superklasse angelegt werden. Dem ist nicht so. Tatsächlich wird nur ein einziges Objekt, nämlich vom Typ der Subklasse, im Speicher alloziert. Dieses enthält Speicherplatz für seine eigenen und alle geerbten Variablen. Die komplexe Aufrufhierarchie der Konstruktoren dient nur dazu, diese Variablen korrekt zu initialisieren, d. h. den Code auszuführen, der zu den Variablen gehört.

Copy-Konstruktor

Häufig steht man vor der Aufgabe, Objekte mit wiederkehrenden Eigenschaften zu erzeugen oder Objekte zu kreieren, die sich nur in Details voneinander unterscheiden. Hier bietet es sich an, einen speziellen Konstruktor vorzusehen, der Objekte vom eigenen Typ nimmt und daraus ein neues Objekt erzeugt.

Copy-Konstruktor

  • initialisiert das Objekt basierend auf einem anderen Objekt
  • ergibt nur als zusätzlicher Konstruktor Sinn
public class Mitarbeiter {
    ...

    // Copy-Konstruktor
    public Mitarbeiter(Mitarbeiter other) {
        this(name, gehalt, geboren);
    }
}

Mithilfe des Copy-Konstruktors kann man ausgehend von einem Objekt, das als Vorlage dient (Prototyp-Objekt), weitere Objekte erzeugen und diese dann nachträglich anpassen.

Finalizer sind keine Destruktoren

  • In anderen objektorientierten Sprachen (z. B. C++) stellen Destruktoren das Gegenstück zu den Konstruktoren dar
  • Java kennt wegen der automatischen Speicherverwaltung keine Destruktoren, da die Objekte automatisch zerstört werden
  • Java kennt nur die sog. Finalizer
    • Alle Klassen erben von Object eine Methode namens `finalize()
    • Sie wird aufgerufen, bevor ein Objekt im Rahmen der Garbage-Collection endgültig zerstört wird
  • Man benötigt im Allgemeinen keine Finalizer
  • Wegen ihrer unangenehmen Nebenwirkungen sollte man daher sie vermeiden

C++ braucht zwingend das Konzept der Destruktoren, da die Speicherverwaltung von den Programmierer:innen erledigt werden muss. Daher muss er eine Möglichkeit haben, den Zustand seines Objekts zu löschen und allozierten Speicher selbst wieder freigeben zu können. Da in Java die Speicherverwaltung vollkommen automatisch erfolgt, braucht man nicht manuell in die Freigabe des Speichers einzugreifen. In einigen sehr seltenen Fällen möchte man aber sichergehen, dass bestimmte Aufräumarbeiten durchgeführt werden bevor ein Objekt endgültig zerstört wird, z. B. weil eine kritische Ressource (Datenbankverbindung, Datei, Socket etc.) vom Objekt gehalten wird. Für solche Fälle kann man Finalizer verwenden. Ansonsten sollte man sie aber unbedingt vermeiden, da sie einige sehr unerwünschte Eigenschaften haben:

  • Es wird nicht garantiert, dass sie überhaupt aufgerufen werden
  • Es wird nicht garantiert, wann sie aufgerufen werden
  • Finalizer verlangsamen erheblich die Freigabe des Speichers, da die zu finalisierenden Objekte in einer Warteschlange gehalten und einzeln abgearbeitet werden. Das kann zum einen die Gesamtperformance beeinträchtigen und zum anderen auch zu Speicherknappheit führen, wenn der Speicher nicht schnell genug freigegeben wird.

Zusammenfassend kann man sagen: Die Anwendungsfälle für Finalizer sind so selten, dass man sie besser überhaupt nicht verwendet oder aber sehr genau weiß, was man tut.

Details zu Finalizern findet sich in Joshua Block, Effective Java, 2nd Edition, S. 27ff.

Unveränderliche Objekte

Unveränderliche Objekte (immutable objects)

  • Der Zustand des Objekts kann nach der Erzeugung nicht mehr verändert werden
  • Nur der Konstruktor setzt den Zustand, keine andere Methode verändert ihn
  • Vorteil
    • Es ist irrelevant, ob zwei Referenzen auf zwei gleiche oder ein identisches Objekt zeigen
    • Einige Optimierungen möglich
  • Nachteil
    • Handhabung ist komplizierter
    • Performance kann leiden

Das wohl prominenteste Beispiel für ein unveränderliches Objekt in Java ist die Klasse String. Nachdem ein String einmal angelegt wurde, kann er nicht mehr verändert werden. Viele Methoden auf String, wie z. B. toUpper() liefern einen neuen String zurück (s = s.toUpper()) und verändern niemals den Inhalt des vorhandenen Strings. Darauf muss man beim Umgang mit Strings (und anderen unveränderlichen Objekten) natürlich achten, da ein Aufruf einer Methode deren Rückgabewert ignoriert wird (s.toUpper()) einfach wirkungslos bleibt. Weiterhin muss man sich darüber im Klaren sein, dass immutable Objekte in einigen Fällen zu einem signifikanten Performanceproblem führen können, in anderen aber die Performance erheblich steigern können (z. B. über das Fliegengewicht-Pattern).

Durch die Verwendung von unveränderlichen Objekten ist es für den Verwender nicht mehr relevant, ob zwei Referenzen auf zwei gleiche (im Sinne von equals()) oder ein und dasselbe Objekt (im Sinne von ==) zeigen. Hierdurch sind viele Optimierungen möglich, die bei veränderbaren Objekten nicht durchgeführt werden könnten. Das Ersetzen zweier gleicher und unveränderlicher Objekte durch ein einziges wird als interning bezeichnet.

Für unveränderliche Objekte ergibt ein Copy-Konstruktor nur begrenzt Sinn, weil man die Kopien nicht mehr verändern kann. Deswegen erweitert man die Idee der Getter- und Setter-Methoden um ein weiteres Konzept für unveränderliche Objekte.

Wither-Methoden

  • Syntax: Typ withXXX(Typ xxx)
  • werden bei unveränderlichen Objekten eingesetzt
  • liefern ein neues Objekt zurück, das in dem angegeben Attribut geändert wurde
public class City {

    private final String name;
    private final int population;

    public City(String name, int population) {
        this.name = name;
        this.population = population;
    }

    public City withPopulation(int pop) {
        return new City(this.name, pop);
    }

    public City withName(String n) {
        return new City(n, this.population);
    }
}

Copyright © 2025 Thomas Smits