10 Java – Klassen
Klassen sind ein wesentlicher Bestandteil der OOP – Objektorientierten Programmierung. Sie sind vergleichbar mit Rezepten oder Blueprints und stellen die grundlegende Struktur und den Aufbau von konkret instanziierten Objekten dar – deren Eigenschaften (Objektvariablen) und Funktionalitäten (Objektmethoden). Jede Klasse, die in Java erstellt wird, erbt automatisch von der Klasse Object
.
Alle Variablen und Methoden, die in Java deklariert werden, sind von solch einer Klassendefinition ummantelt – unabhängig davon, ob sie zu einem Objekt gehören oder nicht. Der Name der Datei muss zudem genauso heißen, wie der Name der Klasse, die sich innerhalb der Datei befindet.
Klassenaufbau
Der Aufbau einer Klasse ist nicht in Stein gemeißelt. Er kann sich von Unternehmen zu Unternehmen unterscheiden. Eine weit verbreitete Anordnung sieht folgendermaßen aus.
Klassenvariablen
Klassenvariablen oder auch statische Variablen werden mit dem Keyword static
gebildet und sollten ganz oben innerhalb einer Klassendefinition deklariert werden. Sie existieren unabhängig von einem Objekt und werden meist mit dem Keyword final
deklariert. Variablen mit dem Keyword final
werden Konstanten genannt, da sich deren Wert nach Initialisierung nicht mehr ändern kann.
Konstanten
Zwei Beispiel-Konstanten aus der Math
-Klasse:
Objektvariablen
Objekt- oder Instanzvariablen werden unterhalb der Klassenvariablen aber oberhalb von Konstruktoren deklariert. Diese Variablen gehören immer zu einem konkreten Objekt und dienen als Eigenschaften des Objekts wie beispielsweise der Name oder die Farbe eines Tiers. Objektvariablen können nur über ein bestehendes Objekt angesprochen werden.
Instanzvariablen können sowohl aus primitiven Datentypen als auch aus Referenzdatentypen wie z.B. Strings bestehen.
Die Initialisierung dieser Variablen übernimmt in den meisten Fällen der Konstruktor.
Konstruktoren
Eine spezielle Art der Methode ist der Konstruktor. Er besitzt keinen Rückgabewert – nicht mal das Keyword void
. Zudem muss der Konstruktor denselben Namen wie die Klasse besitzen. Er ist für das eigentliche Initialisieren der Objekte verantwortlich.
Beim Erstellen eines Objekts wird der Konstruktor aufgerufen. Dabei können ihm Werte mit übergeben werden, mit denen die Objektvariablen des jeweiligen Objekts initialisiert werden.
Wurde kein Konstruktor programmiert, wird automatisch der Default-Konstruktor aufgerufen, welcher keine Parameter erwartet. Dieser kann auch optional selber geschrieben werden.
Auch mehrere Konstruktoren sind möglich.
Verdeckte Variablen
Werden in Codeblöcken neue Variablennamen eingeführt, können diese nicht noch einmal angelegt werden. Heißt die lokale Variable einer Methode genauso wie eine Objektvariable, spricht man auch von einer verdeckten oder shadowed Variablen.
An sich besteht in solchen Fällen kein Problem. Der Compiler wird dann standardmäßig die lokale Variable nutzen. Durch dieses Vorgehen können später neu hinzugefügte Objektvariablen keinen schon bestehenden Code zerstören.
Eine kleine Komplikation besteht nur, wenn die Objektvariable mit einer lokalen Variable genutzt werden soll. In diesem Fall wird der Wert der lokalen Variable verwendet und erneut der lokalen Variable zugewiesen.
Dafür gibt es jedoch eine Lösung.
Das Keyword this
Natürlich können bei solchen Problemen die lokalen oder Objektvariablen umbenannt werden. Müssen diese an mehreren Stellen geändert werden, kann der Aufwand jedoch etwas größer werden. Möchte man die beiden Variablen also gerne gleich benennen, kann das Keyword this
Abhilfe schaffen.
Das Schlüsselwort this
funktioniert ähnlich wie eine Referenzvariable, indem es auf ein bestimmtes Objekt verweist – allerdings in einem spezifischen Kontext. Anders ausgedrückt: Ein Objekt verweist mit this
auf sich selbst. this
bezieht sich auf genau das Objekt, das gerade verwendet wird bzw. in dessen Kontext (Klasse) man sich aktuell befindet.
Ein weiterer Vorteil von this
ist, dass die folgenden beiden Konstruktoren miteinander verknüpft werden können. Dadurch wird redundanter Code vermieden.
Der untere Konstruktor ruft nun den oberen Konstruktor mit dem ergänzenden Wert auf.
Klassenmethoden
Im vorherigen Kapitel 9 – Methoden gab es bereits viele Beispiele zu Klassenmethoden. Klassenmethoden brauchen wie Klassenvariablen kein bestimmtes Objekt um auf diese zugreifen zu können. Sie werden mit dem Keyword static
gebildet und werden über den Klassennamen angesprochen.
Objektmethoden
Alle weiteren Objektmethoden sollten nach den Klassenmethoden stehen und können nur über ein bestehendes Objekt aufgerufen werden.
Getter- & Setter-Methoden
Getter und Getter dienen dazu, Objektvariablen abzugreifen (get-Methoden) oder deren Werte zu überschreiben bzw. sie zu initialisieren (set-Methoden). Sie können für jede Objektvariable angelegt werden. Durch diese Methoden kann somit festgelegt werden, ob überhaupt auf die Variablen zugegriffen werden kann, wenn diese beispielsweise als private
deklariert wurden (siehe Kapitel 12 – Modifizierer und Zugriffsrechte).
Klasseninitialisierer
Wird eine Klasse vom ClassLoader
in die Runtime Environment geladen, werden zunächst die statischen Blöcke von oben nach unten ausgeführt. Dies tritt auf, sobald eine Klassenvariable oder Klassenmethode zum ersten Mal aufgerufen oder eine Instanz der Klasse erstellt wird.
Da die Initialisierung einer Klasse nur ein einziges Mal pro Programmausführung stattfindet, können dadurch auch Klassenvariablen initialisiert werden.
Zudem ermöglichen Klasseninitialisierer die Ausführung von komplexer Initialisierungslogik, die über einfache Zuweisungen hinausgeht, z. B. das Einlesen von Konfigurationsdateien oder das Einrichten von sonstigen statischen Ressourcen.
Innere Klassen
Innere Klassen werden innerhalb einer anderen Klasse definiert. Diese inneren Klassen haben Zugriff auf die Mitglieder der äußeren Klasse, einschließlich mit private
deklarierten Methoden und Variablen und bieten eine Möglichkeit, logische Beziehungen zwischen Klassen zu modellieren. Es gibt verschiedene Arten von inneren Klassen und jede hat ihren eigenen Anwendungsfall.
Nicht-statische innere Klassen
Wenn eine innere Klasse keine static
-Deklaration hat, handelt es sich um eine nicht-statische innere Klasse. Sie werden innerhalb von einer anderen Klasse definiert, aber außerhalb von Methoden, Konstruktoren und Anweisungsblöcken (z.B. Klasseninitialisierer).
Nicht-statische innere Klasse haben Zugriff auf alle Mitglieder (statische und nicht-statische Variablen und Methoden) der äußeren Klasse. Ein Objekt der inneren Klasse ist an ein Objekt der äußeren Klasse gebunden
Nicht-statische innere Klassen sind nützlich, wenn die innere Klasse stark von der äußeren Klasse abhängt und auf deren Objektvariablen und -methoden zugreifen muss. Ein klassisches Beispiel ist ein Iterator
, der als innere Klasse in einer Collection wie List
oder Set
implementiert ist.
Sie können verwendet werden, um Utility-Klassen zu erstellen, die nur von der äußeren Klasse verwendet werden. Diese Hilfsklassen müssen keine eigenständigen Klassen sein und haben typischerweise außerhalb der äußeren Klasse keinen Sinn (z.B. Player
-Klasse ↔ Properties
-Klasse).
Wenn eine Klasse sehr groß ist, können nicht-statische innere Klassen verwendet werden, um zusammenhängende Logik in einer abgeschlossenen Einheit zu kapseln, ohne die äußere Klasse zu überladen.
Statische innere Klassen
Wenn eine innere Klasse mit dem Schlüsselwort static
deklariert ist, handelt es sich um eine statische innere Klasse. Auch sie werden innerhalb von einer anderen Klasse definiert, aber außerhalb von Methoden, Konstruktoren und Anweisungsblöcken (z.B. Klasseninitialisierer).
Eine statische innere Klasse hat Zugriff auf Klassenvariablen und -methoden, aber nicht auf die Objektvariablen und -methoden der äußeren Klasse. Ein Objekt der inneren Klasse ist nicht an ein Objekt der äußeren Klasse gebunden
Statische innere Klassen eignen sich gut für Utility-Klassen, die Klassenmethoden oder Konstanten enthalten und keine Verbindung zum Objekt der äußeren Klasse benötigen.
Dies wird häufig verwendet, um verschiedene Implementierungen eines Interfaces oder einer abstrakten Klasse zu kapseln. Sie können aber auch dazu verwendet werden, um die Implementierung von Algorithmen oder Datenstrukturen zu kapseln, die nur innerhalb der äußeren Klasse verwendet werden sollen.
Eine statische innere Klasse wird, wie das obige Beispiel zeigt, auch häufig für ein Builder-Pattern verwendet. Bei dieser Methode wird eine statische innere Klasse verwendet, um die Konstruktion eines komplexen Objekts zu kapseln. Die statische innere Klasse fungiert als Builder
, der die verschiedenen Teile des Objekts schrittweise konfiguriert und schließlich das fertige Objekt erstellt.
Lokale innere Klassen
Lokale innere Klassen werden innerhalb einer Methode order eines Anweisungsblocks definiert. Sie können auf lokale Variablen der Methode zugreifen, sofern diese final
oder effektiv final
sind und sind nur innerhalb des Bereichs sichtbar, in dem sie definiert sind.
Lokale innere Klassen sind ideal, wenn eine Klasse nur innerhalb einer Methode oder eines Anweisungsblocks verwendet werden soll. Besonders, wenn diese Klasse auf die lokalen Variablen der Methode zugreifen muss.
In ereignisgesteuerten Programmiermodellen (Event-driven Programming), wie in GUI-Anwendungen, können lokale innere Klassen verwendet werden, um die Ereignisbehandlungslogik in einer Methode zu kapseln.
Sie eignen sich auch zur Implementierung von vorübergehenden Datenstrukturen oder Logiken, die nur in einem kleinen Kontext benötigt werden und außerhalb der Methode keine Bedeutung haben.
Anonyme Klassen
Diese Klassen haben keinen Namen und werden gleichzeitig deklariert und initialisiert. Sie werden häufig verwendet, um Schnittstellen oder abstrakte Klassen zu implementieren, wenn nur eine einzelne Instanz benötigt wird.
Zudem finden sie häufig in GUI-Umgebungen Anwendung, um Event-Listener zu erstellen, ohne dafür eine benannte Klasse zu definieren und eignen sich für Aufgaben, die nur einmal ausgeführt werden und für die keine Wiederverwendbarkeit notwendig ist.
Records
Records wurden mit Java 14 vorläufig eingeführt und mit dem Feedback der Community in Java 15 und 16 weiterentwickelt und verbessert, bis sie in Java 16 offiziell als Standfunktion etabliert wurden.
Sie bieten eine kompakte Möglichkeit, unveränderliche Datenklassen zu definieren. Records sind im Wesentlichen eine Abkürzung für die Erstellung von Klassen, die hauptsächlich zur Speicherung von Daten verwendet werden. Im Gegensatz zu herkömmlichen Klassen nehmen sie dem Entwickler viel Boilerplate-Code ab, da sie automatisch Konstruktoren, Getter-Methoden, equals()
, hashCode()
und toString()
-Methoden generieren.
Records sind unveränderlich, was in vielen Fällen von Vorteil ist, aber es schränkt auch ihre Verwendung ein, wenn veränderbare Daten benötigt werden.
Aktivierung von Records
Für Versionen vor Java 14 sind Records nicht als Sprachfunktion verfügbar.
Für die Versionen zwischen Java 14 und 16 das Flag
--enable-preview
verwendet werden, um Records zu verwenden.Für Versionen ab Java 16 und höher sind Records eine Standardfunktion und das Flag
--enable-preview
ist für die Verwendung von Records nicht mehr erforderlich.
Um eine .java
-Datei vor Java 16 mit dem Preview Feature zu kompilieren, muss folgender Befehl verwendet werden:
Zum Ausführen dient der folgende Befehl:
Coding Conventions
Klassen sollten nach der PascalCase-Notation benannt werden.
Jede Art von Methode – siehe Kapitel 9 – Methoden: Coding-Conventions.
Jeder Art von Variable (außer Konstanten) – siehe Kapitel 3 – Variablen: Coding Conventions.
Konstruktoren müssen genau wie die Klasse benannt werden.
Getter & Setter sollten mit
get
bzw. mitset
beginnen.Getter für Booleans sollten mit
is
beginnen. Alternativ kann auchhas
verwendet werden.Konstanten sollten nach der SCREAMING_SNAKE_CASE-Notation geschrieben werden.