Doppelklammer-Initialisierung

Da anonyme Klassen keinen Namen haben, muss für Konstruktoren ein anderer Weg gefunden werden. Hier helfen Exemplarinitialisierungsblöcke, also Blöcke in geschweiften Klammern.

Exemplarinitialisierer gibt es ja eigentlich gar nicht im Bytecode, sondern der Compiler setzt den Programmcode automatisch in jeden Konstruktor. Obwohl anonyme Klassen keinen direkten Konstruktor haben können, gelangt doch über den Exemplarinitialisierer Programmcode in den Konstruktor der Bytecode-Datei.

Dazu ein Beispiel: Die anonyme Klasse ist eine Unterklasse von Point und initialisiert im Konstruktor einen Punkt mit Zufallskoordinaten. Aus diesem speziellen Punkt-Objekt lesen wir dann die Koordinaten wieder aus:

java.awt.Point p = new java.awt.Point() {

{

x = (int)(Math.random() * 1000); y = (int)(Math.random() * 1000);

}

};

System.out.println( p.getLocation() ); // java.awt.Point[…

System.out.println( new java.awt.Point( -1, 0 ) {{

y = (int)(Math.random() * 1000);

}}.getLocation() ); // java.awt.Point[x=-1,y=...]

Sprachlichkeit

Wegen der beiden geschweiften Klammen heißt diese Variante auch Doppelklammer-Initialisierung (engl. Double Brace Initialization).

Die Doppelklammer-Initialisierung ist kompakt, wenn etwas Datenstrukturen oder hierarchische Objekte initialisiert werden sollen.

Beispiel *

Im Folgenden Beispiel erwartet appendText(…) ein Objekt vom Typ HashMap, was durch den Trick direkt initialisiert wird:

String s = new DateTimeFormatterBuilder()

.appendText( ChronoField.AMPM_OF_DAY,

new HashMap<Long, String>() {{ put(0L, “früh”);put(1L,”spät” ); }} )

.toFormatter().format( LocalTime.now() );

System.out.println( s );

Im nächsten Beispiel bauen wir eine geschachtelte Map, das ist ein Assoziativspeicher, der wieder einen anderen Assoziativspeicher enthält:

Map<String,Object> map = new HashMap<String,Object>() {{

put( “name”, “Chris” );

put( “address”, new HashMap<String,Object>() {{

put( “street”, “Feenallee 1″ );

put( “city”, “Elefenberg” );

}} );

}};

Warnung

Die Doppelklammerinitialisierung ist nicht ganz „billig“, da eine Unterklasse aufgebaut wird, also neuer Bytecode generiert wird. Zudem hält die innere Klasse eine Referenz auf die äußere Klasse fest. Des Weiteren kann es Probleme mit equals(…) geben, da wir mit der Doppelklammerinitialisierung eine Unterklasse schaffen, die vielleicht mit equals(…) nicht mehr gültig vergleichen werden kann, denn die Class-Objekte sind jetzt nicht mehr identisch. Das spricht in der Summe eher gegen diese Konstruktion.

Sich selbst mit this übergeben

Möchte sich ein Objekt A einem anderen Objekt B mitteilen, damit B das andere Objekt A „kennt“, so funktioniert das gut mit der this-Referenz. Demonstrieren wir das an einem „Ich bin dein Vater“-Beispiel: Zwei Klasen Luke und Darth repräsentieren zwei Personen, wobei Luke ein Attribut dad für seinen Vater hat:

LukeAndDarth.java, Teil 1
class Luke {
  Darth dad;
}

class Darth {
  void revealTruthTo( Luke son ) {
    son.dad = this;
  }
}

Spannend ist die Methode revealTruthTo(Luke), denn sie setzt beim übergebenen Luke-Objekt das dad-Attribut mit der this-Referenz. Damit kennt Luke seinen Vater, getestet in folgender Klasse:

LukeAndDarth.java, Teil 2
public class LukeAndDarth {
  public static void main( String[] args ) {
    Luke luke = new Luke();
    Darth darth = new Darth();
    System.out.println( luke.dad ); // null
    darth.revealTruthTo( luke );
    System.out.println( luke.dad ); // Darth@15db9742
  }
}

Hinweis: In statischen Methoden steht die this-Referenz nicht zur Verführung, da wir uns in Klassenmethoden nicht auf ein konkretes Objekt beziehen.

toString() für equals-Vergleiche?

Einige kreative Programmierer nutzen die toString()-Repräsentation für Objektvergleiche. Etwas der Richtung: Wenn wir zwei Point-Objekte p und q haben, und p.toString().equals(q.toString()) ist, dann sind beide Punkte eben gleich. Doch ist es hochgradig gefährlich sich auf die Rückgabe von toString() zu verlassen aus mehreren Gründen: Offensichtlich ist, dass toString() nicht unbedingt überschrieben sein muss. Zweitens muss toString() nicht unbedingt alle Elemente repräsentieren und die Ausgabe könnte abgekürzt sein. Drittens können natürlich Objekte equals-gleich sein, auch wenn ihre String-Repräsentation nicht gleich ist, was etwa bei URL-Objekten der Fall ist. Der einzige erlaubte Fall für so eine Konstruktion wäre String/StringBuilder/StringBuffer/CharSequence, wo es ausdrücklich um Zeichenketten geht.

Habe ich Gründe vergessen?

Verwandtschaft von Methode und Konstruktor

Methoden und Konstruktoren besitzen beide Programmcode, haben eine Parameterliste, Modifizierer, können auf Objektvariablen zugreifen und this verwenden – das sind ihre Gemeinsamkeiten. Ein schon erwähnter Unterschied ist, dass Methoden einen Rückgabetyp besitzen (auch wenn er nur void ist), Konstruktoren aber nicht. Zwei weitere Unterschiede betreffen die Syntax und Semantik.

Konstruktoren tragen immer den Namen ihrer Klasse, und da Klassennamen per Konvention großgeschrieben werden, sind auch Konstruktoren immer großgeschrieben – Methoden werden in der Regel immer kleingeschrieben. Und Methoden sind in der Regel Verben, die das Objekt anweisen etwas zu tun, Klassennamen sind Nomen und keine Verben.

Der Programmcode eines Konstruktors wird automatisch nach dem Erzeugen eines Objekts von der JVM genau einmal aufgerufen, und zwar als erstes vor allen anderen Methoden. Methoden lassen sich beliebig oft aufrufen und unterliegen der Kontrolle des Benutzers. Konstruktoren lassen sich später nicht noch einmal auf einem schon existierenden Objekt erneut aufrufen und so ein Objekt reinitialisieren. Der Konstruktor-Aufruf ist implizit und automatisch mit new verbunden und kann nicht getrennt vom new gesehen werden.

Zusammenfassend können wir sagen, dass ein Konstruktor eine Art spezielle Methode zur Initialisierung eines Objektes ist.

JVM-Interna: „Ein Java-Compiler setzt Konstruktoren als void-Methoden um, die „<init>“ heißen“.