9.5 Anonyme innere Klassen
Anonyme Klassen gehen noch einen Schritt weiter als lokale Klassen: Auch sie sind innere Klassen, haben aber keinen Namen und erzeugen immer automatisch ein Objekt; Klassendeklaration und Objekterzeugung sind zu einem Sprachkonstrukt verbunden. Die allgemeine Notation ist folgende:
new KlasseOderSchnittstelle() { /* Eigenschaften der inneren Klasse */ }
In dem Block geschweifter Klammern lassen sich nun Methoden und Attribute deklarieren oder Methoden überschreiben. Hinter new steht der Name einer Klasse oder Schnittstelle:
new Klassenname(Optionale Argumente) { … }: Steht hinter new ein Klassentyp, dann ist die anonyme Klasse eine Unterklasse von Klassenname. Es lassen sich mögliche Argumente für den Konstruktor der Basisklasse angeben (das ist zum Beispiel dann nötig, wenn die Oberklasse keinen parameterlosen Konstruktor deklariert).
new Schnittstellenname() { … }: Steht hinter new der Name einer Schnittstelle, dann erbt die anonyme Klasse von Object und implementiert die Schnittstelle Schnittstellenname. Implementiert sie nicht die Operationen der Schnittstelle, ist das ein Fehler. Wir hätten nichts davon, denn dann hätten wir eine abstrakte innere Klasse, von der sich kein Objekt erzeugen lässt.
Für anonyme innere Klassen gilt die Einschränkung, dass keine zusätzlichen extends- oder implements-Angaben möglich sind. Ebenso sind keine eigenen Konstruktoren möglich (wohl aber Exemplarinitialisierer), und nur Objektmethoden und finale statische Variablen sind erlaubt.
9.5.1 Nutzung einer anonymen inneren Klasse für den Timer
In Listing 9.6 haben wir für den Timer extra eine neue lokale Klasse deklariert, aber genau genommen haben wir diese nur einmal nutzen müssen, nämlich um ein Exemplar zu bilden und scheduleAtFixedRate(…) übergeben zu können. Das ist ein perfektes Szenario für anonyme innere Klassen. Aus
class SportReminderTask extends TimerTask {
@Override public void run() { ... }
}
new Timer().scheduleAtFixedRate( new SportReminderTask(), ... );
wird:
new Timer().scheduleAtFixedRate( new TimerTask() {
@Override public void run() {
System.out.println( "Los, ..." );
}
},
0 /* ms delay */,
1000 /* ms period */);
Im Kern ist es also eine Umwandlung von new SportReminderTask() in new TimerTask() { … }. Von dem Klassennamen SportReminderTask ist nichts mehr zu sehen, das Objekt ist anonym.
[»] Hinweis
Eine anonyme Klasse kann Methoden der Oberklasse überschreiben, Operationen aus Schnittstellen implementieren und sogar neue Eigenschaften anbieten:
String s = new Object() {
String quote( String s ) {
return String.format( "'%s'", s );
}
}.quote( "Cora" );
System.out.println( s ); // 'Cora'
Der neu deklarierte anonyme Typ hat eine Methode quote(String), die direkt aufgerufen werden kann. Ohne diesen direkten Aufruf ist die quote(…)-Methode aber unsichtbar, denn der Typ ist ja anonym, und so sind nur die Methoden der Oberklasse (bei uns Object) bzw. der Schnittstelle bekannt. (Wir lassen die Tatsache außen vor, dass eine Anwendung mit Reflection auf die Methoden zugreifen kann.)
9.5.2 Umsetzung innerer anonymer Klassen *
Auch für innere anonyme Klassen erzeugt der Compiler eine normale Klassendatei. Wir haben gesehen, dass der Java-Compiler bei einer »normalen« geschachtelten Klasse die Notation ÄußereKlasse$InnereKlasse wählt. Das klappt bei anonymen inneren Klassen natürlich nicht mehr, da uns der Name der inneren Klasse fehlt. Der Compiler wählt daher folgende Notation für Klassennamen: InnerToStringDate$1. Falls es mehr als eine innere Klasse gibt, folgen $2, $3 usw.
Ausnahmen in inneren anonymen Klassen
In einem Stack-Trace taucht der generierte Klassenname auf, wenn es eine Ausnahme gab. Ist etwa die Deklaration eingebettet in eine main(…)-Methode der Klasse T:
new Object() { String nuro() { throw new IllegalStateException(); } }.nuro();
So folgt bei der Ausführung:
Exception in thread "main" java.lang.IllegalStateException
at T$1.nuro(T.java:6)
at T.main(T.java:6)
9.5.3 Konstruktoren innerer anonymer Klassen
Der Compiler setzt anonyme Klassen in normale Klassendateien um. Jede Klasse kann einen eigenen Konstruktor deklarieren, und auch für anonyme Klassen sollte das möglich sein, um Initialisierungscode dort hineinzusetzen.
Wir wollen eine innere Klasse schreiben, die eine Unterklasse von java.awt.Point ist. Sie soll die toString()-Methode überschreiben:
Point p = new Point( 10, 12 ) {
@Override public String toString() {
return "(" + x + "," + y + ")";
}
};
System.out.println( p ); // (10,12)
Die anonyme Unterklasse wird also durch den normalen Konstruktor von Point initialisiert.
Exemplarinitialisierungsblöcke bei inneren anonymen Klassen
Da aber anonyme Klassen keinen Namen haben, muss für Konstruktoren ein anderer Weg gefunden werden. Hier helfen Exemplarinitialisierungsblöcke, also Blöcke in geschweiften Klammern direkt innerhalb einer Klasse, die wir schon in Kapitel 6, »Eigene Klassen schreiben«, vorgestellt haben. 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 Klammern heißt diese Variante auch Doppelklammer-Initialisierung (engl. double brace initialization).
Die Doppelklammer-Initialisierung ist kompakt, wenn etwa Datenstrukturen oder hierarchische Objekte initialisiert werden sollen.
[zB] Beispiel *
Im folgenden Beispiel erwartet appendText(…) ein Objekt vom Typ HashMap, das 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. Diese Map enthält an einem Punkt wieder einen anderen Assoziativspeicher:
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 Doppelklammer-Initialisierung ist nicht ganz »billig«, da für die Unterklasse eine zusätzliche Klassendatei im Dateisystem 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 Doppelklammer-Initialisierung eine Unterklasse schaffen, die vielleicht mit equals(…) nicht mehr gültig verglichen werden kann, denn die Class-Objekte sind jetzt nicht mehr identisch. Das spricht in der Summe eher gegen diese Konstruktion. Der Typ Map bietet mit den statischen of(…)/entry(…)-Methoden eine bessere Möglichkeit.
Gar nicht super() *
Innerhalb eines »anonymen Konstruktors« kann kein super(…) verwendet werden, um den Konstruktor der Oberklasse aufzurufen. Dies liegt daran, dass automatisch ein super(…) in den Initialisierungsblock eingesetzt wird. Die Parameter für die gewünschte Variante des (überladenen) Oberklassen-Konstruktors werden am Anfang der Deklaration der anonymen Klasse angegeben. Dies zeigt das folgende Beispiel:
System.out.println( new java.awt.Point( -1, 0 ) {{
y = (int)(Math.random() * 1000);
}}.getLocation() ); // java.awt.Point[x=-1,y=...]
[zB] Beispiel
Wir initialisieren ein Objekt BigDecimal, das beliebig große Ganzzahlen aufnehmen kann. Im Konstruktor der anonymen Unterklasse geben wir anschließend den Wert mit der geerbten toString()-Methode aus:
new java.math.BigDecimal( "12345678901234567890" ) {{
System.out.println( toString() );
}};