Java Innere Klassen
Nicht-statische innere Klassen in Java definieren, die eine implizite Referenz auf eine Instanz der umgebenden Klasse halten.
Eine innere Klasse ist eine nicht-statische verschachtelte Klasse — eine, die innerhalb einer anderen Klasse ohne den static-Modifikator deklariert wird. Das entscheidende Merkmal: Jede Instanz einer inneren Klasse ist an eine Instanz der umgebenden Klasse gebunden und trägt eine implizite Referenz auf diese. Aus der inneren Klasse heraus können Sie die Felder der äußeren Instanz lesen und schreiben sowie deren Methoden aufrufen, als wären es Ihre eigenen.
Dies macht innere Klassen zum richtigen Werkzeug, wenn eine kleine, sekundäre Klasse eng in den Zustand einer anderen Klasse eingebunden sein muss — am bekanntesten sind Iteratoren, die über die internen Daten ihres Containers iterieren.
Diese Seite erklärt, wie man eine innere Klasse deklariert, wie man eine erstellt (man braucht immer eine äußere Instanz), den Outer.this-Qualifizierer, den klassischen Iterator-Anwendungsfall, die Memory-Leak-Falle, die mit der impliziten Referenz verbunden ist, und wie man zwischen einer inneren Klasse und einer static-verschachtelten Klasse entscheidet.
Eine innere Klasse deklarieren
Einfach static aus der Deklaration einer verschachtelten Klasse weglassen:
public class Outer {
private int x = 1;
class Inner { // no static — inner class
int get() { return x; } // reads Outer's x directly
}
}Inner hat keine eigenen Felder, und doch gibt get() 1 zurück. Das bloße x wird über die implizite Referenz zu Outer.this.x aufgelöst.
Eine Instanz erstellen
Da jede Instanz einer inneren Klasse an eine äußere gebunden ist, benötigen Sie eine äußere Instanz, um eine innere zu erstellen. Es gibt zwei Möglichkeiten:
Outer o = new Outer();
Outer.Inner i = o.new Inner(); // bind explicitly to o…oder innerhalb einer nicht-statischen Methode von Outer:
public class Outer {
void demo() {
Inner i = new Inner(); // implicitly bound to this
}
}Die o.new Inner()-Syntax ist selten und überraschend — in den meisten Fällen werden Instanzen innerer Klassen innerhalb der Methoden der äußeren Klasse erstellt, wo die Bindung implizit ist.
Outer.this — bei Namenskollisionen zur äußeren Klasse durchgreifen
Wenn die innere Klasse ein Feld mit demselben Namen wie eines in der äußeren Klasse deklariert, überschattet das innere das äußere. Um auf das äußere zuzugreifen, qualifizieren Sie es mit Outer.this:
public class Outer {
int x = 1;
class Inner {
int x = 2;
void demo() {
System.out.println(x); // 2 — Inner's x
System.out.println(this.x); // 2 — Inner's x
System.out.println(Outer.this.x); // 1 — Outer's x
}
}
}this in einer inneren Klasse bezieht sich auf die innere Instanz; Outer.this bezieht sich auf die umgebende äußere Instanz.
Der klassische Anwendungsfall — Iteratoren
Der klassische Grund für innere Klassen ist die Implementierung eines Iterators über die privaten Interna eines Containers:
public class IntList {
private int[] data;
private int size;
// ... constructors, add, ...
public Iterator<Integer> iterator() {
return new InnerIterator();
}
private class InnerIterator implements Iterator<Integer> {
private int i = 0;
public boolean hasNext() { return i < size; }
public Integer next() { return data[i++]; }
}
}InnerIterator greift direkt über die implizite äußere Referenz auf data und size zu. Kein Setter, kein Accessor nötig — die innere Klasse ist Teil der Implementierung von IntList.
Beachten Sie private class InnerIterator. Von außen sehen Aufrufer nur das öffentliche Iterator<Integer>-Interface; sie wissen nicht, dass InnerIterator existiert. Das ist der Kapselungsvorteil der Verschachtelung.
Innere Klassen halten eine Referenz — und halten die äußere am Leben
Eine subtile Falle. Solange eine Instanz einer inneren Klasse erreichbar ist, kann die JVM die äußere Instanz, an die sie gebunden ist, nicht durch Garbage Collection freigeben. Wenn eine Instanz einer inneren Klasse an langlebigen Code zurückgegeben wird (z. B. wenn ein Listener irgendwo installiert wird), können ganze Objektgraphen länger als erwartet am Leben bleiben.
public class Window {
Listener installListener() {
return new Listener(); // returned to whoever calls this
}
class Listener { ... } // holds a Window reference forever
}Wenn installListener() in einer statischen Registry gespeichert wird, lebt das Window so lange, bis die Registry geleert wird. Die Lösung besteht meist darin, die verschachtelte Klasse static zu machen und benötigte Daten explizit zu übergeben, um die implizite Referenz zu unterbrechen.
Dies ist der häufigste Grund, warum Teams standardmäßig zu static-verschachtelten Klassen greifen und nur dann zu inneren Klassen wechseln, wenn sie die Bindung ausdrücklich benötigen.
Statische Member in inneren Klassen
In den meisten Jahren der Java-Geschichte durften innere Klassen keine static-Member (static-Felder, -Methoden oder verschachtelte Klassen) deklarieren. Java 16 hat dies gelockert — innere Klassen können jetzt statische Member haben. Wenn Sie sich jedoch dabei ertappen, solche zu wünschen, ist das oft ein Hinweis, dass die Klasse selbst static sein möchte.
static vs. innere Klasse — die Entscheidung
Eine nützliche Regel: Machen Sie sie static, es sei denn, Sie benötigen aktiv die äußere Referenz.
- Statisch verschachtelte Klasse: einfacher, leichter, hält die äußere nicht am Leben.
- Innere Klasse: bequemer syntaktischer Zucker über den Zugriff auf
outerInstance.field, wenn die Beziehung echte Bedeutung hat.
Wenn Sie immer nur Outer.this.field verwenden, nehmen Sie einfach ein Outer-Objekt als Konstruktorparameter und machen die Klasse statisch.
Ein ausgearbeitetes Beispiel
Was kommt als Nächstes
Die nächste Art verschachtelter Klassen ist die inline, einmalige Variante: anonyme Klassen, die für schnelle Subklassen-und-Instanziierung-in-einem-Ausdruck-Situationen verwendet werden. Weiter zu anonymen Klassen.