Was sind innere Klassen?

In Abgrenzung zu sog. Top-Level-Klassen, deren Definition als selbstständige Kompilationseinheit direkt in eigenen *.java-Dateien erfolgt, sind innere Klassen solche, die innerhalb des Codeblocks einer anderen, umschließenden Klasse liegen.

Klassen und Interfaces können in Java grundsätzlich ineinander verschachtelt werden. Der Aufbau des Quelltextes sieht dann prinzipiell wie folgt aus:

// Datei: OuterClass.java
class OuterClass {
    //...
    class InnerClass {
        //...
    }
    //...
}

Die äußere Klasse ist hierbei diejenige, die für die *.java-Datei namensgebend ist, in diesem Fall OuterClass.java.

Die Verwendung von inneren Klassen dient nicht der Erweiterung der sprachlichen Möglichkeiten. Sie ist vielmehr geeignet, das Programmdesign zu optimieren, indem ein enger Bezug zwischen eingeschlossener und einschließender Klasse hergestellt wird.

Es existieren vier verschiedenen Arten innerer Klassen. Sie unterscheiden sich hinsichtlich ihrer Sichtbarkeits-, Zugriffs- und Verwendungsmöglichkeiten. Durch den Compiler werden sie in separaten *.class-Dateien gespeichert. Sie besitzen Bezeichner, bei denen der Name der äußeren Klasse, getrennt durch ein Dollarzeichen ($) von demjenigen der inneren Klasse gefolgt wird. Einzige Ausnahme ist die Benennung von anonymen inneren Klassen, die mangels Bezeichner durchnummeriert werden.

Geschachtelte Top-Level-Klassen

Dieser immer static deklarierte Typ ist ohne Objektbildung der umgebenden Klasse sofort in anderen Klassen verfügbar. Seine Definition kann sich auch in einer anderen inneren Top-Level-Klasse befinden. Der Schachtelungstiefe sind hierbei keine Grenzen gesetzt.
Die Bezeichnung dieses Typs zeigt zudem, dass sich das Verhalten und die Handhabung nur wenig von 'normalen' Top-Level-Klassen unterscheiden. Insbesondere können Objekte ohne eine Referenz auf die äußere Klasse erzeugt werden.

package test;

public class OuterClass {
    static String s1 = "Variable static OuterClass.s1 called";
    String s2 = "Variable OuterClass.s2 called";
    public static class InnerClass {
        public void print() {
            System.out.println("InnerClass#print() called");
            System.out.println(s1);
            System.out.println(s2); // Fehler
        }
    }
}

Wird eine geschachtelte innere Klasse angesprochen, so muss der Packagename und der Name der umgebenden Klasse dem Bezeichner der inneren Klasse, verbunden durch den Punkt-Operator, vorangestellt werden. Eine entsprechende import-Anweisung ist natürlich ebenso zulässig.

import test.OuterClass;

public class Test {
    public static void main(String[] args) {
        OuterClass.InnerClass ai = new OuterClass.InnerClass();
        // alternativ unter Wegfall der import-Anweisung
        // test.OuterClass.InnerClass ai = new test.OuterClass.InnerClass();
        ai.print();
    }
}

Für einen Zugriff auf Variablen der äußeren Klasse aus der inneren heraus müssen diese static deklariert sein, da die Ansprache ja aus einer statischen Umgebung heraus erfolgt.
Möchte man umgekehrt in der äußeren Klasse auf Variablen der inneren zugreifen, so gelten die gängigen Regeln: Der Zugriff kann entweder über ein Objekt der inneren Klasse, oder, bei static-Deklaration der Variablen, mit vorangestelltem Klassenbezeichner ohne Objektbildung stattfinden.
Wie oben bereits erwähnt, versieht der Compiler die *.class-Dateien von geschachtelten Top-Level-Klassen bei der Compilation mit einem Bezeichner, der aus dem Namen der äußeren Klasse und dem der inneren Klasse getrennt durch ein '$' besteht.

Elementklassen

Elementklassen sind 'echte', im Codeblock einer anderen Klasse definierte und beliebig tief schachtelbare Klassen, die nicht static deklariert sind und die keine nur static deklarierten Elemente enthalten dürfen. Konstanten, als static und final deklarierte Variablen, sind jedoch zulässig.
Elementklassen besitzen Zugriff auf alle, auch private oder static deklarierte Elemente der umgebenden Klasse.

public class OuterClass {
    private String s1 = "Variable OuterClass.s1 called";
    static String s2 = "Variable OuterClass.s2 called";
    public class InnerClass {
        static final String s3 = "static und final";  // zulässig
        static String s4 = "nur static";  // Fehler
        public void print() {
            System.out.println(s1);
            System.out.println(s2);
        }
    }
}

Elementklassen existieren nicht unabhängig von ihrer umgebenden Klasse. Jedes ihrer Objekte ist einem der äußeren Klasse zugeordnet. Die *.class-Datei besteht auch hier aus dem Namen der äußeren Klasse und demjenigen der inneren getrennt durch ein '$'.
Bei der Objektbildung einer Elementklasse ist auf die etwas ungewöhnliche Schreibweise zu achten, bei der das Objekt der äußeren Klasse durch den Punktoperator mit dem Konstruktoraufruf durch new der inneren Klasse verbunden wird.

OuterClass oc = new OuterClass();
OuterClass.InnerClass ic = oc.new InnerClass();
// liefert oc$ic.class

Befinden sich in äußerer und innerer Klasse Elemente mit gleichen Bezeichnern, so werden diejenigen der äußeren Klasse in der inneren überdeckt. Um in solchen Fällen dennoch auf die Elemente der äußeren Klasse zugreifen zu können, verwendet man das Schlüsselwort this in Kombination mit dem äußeren Klassennamen:

public class OuterClass {
    private String s1 = "Variable OuterClass.s1 called";
    public class InnerClass {
        private String s1 = "Variable InnerClass.s1 called";
        public void print() {
            System.out.println(OuterClass.this.s1); // Variable OuterClass.s1 called
        }
    }
}

Lokale Klassen

Unter lokalen Klassen versteht man solche, die in Anweisungsblöcken von Methoden, Konstruktoren, etc. einer äußeren Klasse definiert sind. Lokale Interfaces sind nicht möglich. Auch Zugriffsmodifikatoren sind bei der Klassendeklaration nicht zulässig. Klassenmethoden und statische Variablen dürfen in lokalen Klassen nicht vorhanden sein, static und final deklarierte Konstanten allerdings sehr wohl.

public class OuterClass {
    String s1 = "Variable OuterClass.s1 called";
    public void print() {
        String s2 = "Variable s2 in OuterClass.print() called";
        class InnerClass {
            String s3 = "Variable InnerClass.s3 called";
            InnerClass() {
                System.out.println(s1); // Variable OuterClass.s1 called
                System.out.println(s2); // Variable s2 in OuterClass.print() called
                System.out.println(s3); // Variable InnerClass.s3 called
            }
        }
        new InnerClass();
        System.out.println(s3); // Fehler
    }

    public static void main(String[] args) {
        new OuterClass().print();
    }
}

Lokale innere Klassen haben direkten Zugriff auf die Variablen der äußeren Klasse, umgekehrt jedoch ist ein Zugriff auf die Variablen der inneren Klasse nur mit einem Objekt dieser möglich.

Anonyme innere Klassen

Anonyme Klassen sind innere Klassen, von denen immer ein Objekt gebildet wird, die jedoch keinen eigenen Bezeichner besitzen. Aus diesem Grund werden sie vom Compiler auch in durchnummerierten *.class-Dateien abgelegt, wobei dem Namen der äußeren Klasse, getrennt durch ein Dollar-Zeichen, die laufende Nummer der in der Klasse enthaltenden inneren anonymen Klassen folgt. Dies gilt auch bei stärkerer Schachtelung. So werden beim u.a. Beispiel vom Compiler drei *.class-Dateien erzeugt: AnonClass.class, AnonClass$1.class, und AnonClass$1$1.class.

Innere anonyme Klassen verhalten sich wie lokale Klassen. Sie dürfen darüber hinaus weder extends- noch implements-Angaben und keinen Konstruktor enthalten. Sie erweitern entweder eine bestehende Klasse oder sie erben von Object und implementieren ein Interface. Häufig findet man letztere Variante bei der Ereignis-Behandlung.

import java.awt.BorderLayout;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

public class AnonClass {
    public AnonClass() {
        init();
    }
    
    private void init() {
        JButton butt = new JButton("klick");
        butt.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                System.out.println(new Rectangle(40, 20) {
                    public String toString(){
                        return "Masse des Rechtecks "  + getWidth() + "x" + getHeight();
                    };
                });
            }
        });
        JFrame frame = new JFrame("Anon. Class Beispiel");
        frame.add(butt, BorderLayout.CENTER);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(200, 200);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new AnonClass());
    }
}

Das Beispiel demonstriert beide Varianten. Eine anonyme Klasse wird der Methode addActionListener() als Parameter übergeben. Man erkennt hier, dass nach new das Interface ActionListener angesprochen und im sich anschließenden Block dessen Methode actionPerformed() implementiert wird.
Im Codeblock der Methode wird eine Consolenausgabe mit System.out.println() erzeugt. Der Methode println() wird hierbei wiederum eine anonyme von Rectangle abgeleitete Klasse als Parameter übergeben, in der toString() überschrieben wird.

Ausgegeben wird also die String-Repräsentation des automatisch erzeugten Rectangle-Objektes, das wiederum durch Klick auf den Button und das damit verbundene Event-Handling ausgeführt wird.

Wenn Ihnen javabeginners.de gefällt, freue ich mich über eine Spende an diese gemeinnützigen Organisationen.