Tech Archive Help

10 Java – Klassen

Klassen sind ein wesentlicher Bestandteil der OOP – Objektorientierten Programmierung. Sie sind vergleichbar mit Rezepten oder Blueprints und stellen die grundsätzliche Struktur und den Aufbau von konkret instanziierten Objekten dar – deren Eigenschaften (Objektvariablen) und Funktionalitäten (Objektmethoden).

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.

public class ClassName { // Constants and static or class variables // Instance or object variables // Constructors // Static or class methods // Instance or object methods // Getters and Setters // Inner classes }

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:

10 java classes 1

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.

public class Dog { public String name; public int age; }

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.

public class Dog { public String name; public int age; public Dog(String name, int age) { name = name; age = age; } }

Wurde kein Konstruktor programmiert, wird automatisch der Default-Konstruktor aufgerufen, welcher keine Parameter erwartet. Dieser kann auch optional selber geschrieben werden.

public class Dog { public String name; public int age; public Dog() { } }

Auch mehrere Konstruktoren sind möglich.

public class Dog { public String name; public int age; public Dog(String name, int age) { name = name; age = age; } public Dog(String name) { name = name; age = 0; } }

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.

public class Dog { public String name; public int age; public Dog(String name, int age) { name = name; // <-- here age = age; // <-- here } public Dog(String name) { name = name; // <-- here age = 0; } }

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.

public class Dog { public String name; public int age; public Dog(String name, int age) { this.name = name; this.age = age; } public Dog(String name) { this.name = name; this.age = 0; } }

Ein weiterer Vorteil von this ist, dass die folgenden beiden Konstruktoren miteinander verknüpft werden können. Dadurch wird redundanter Code vermieden.

public class Dog { public String name; public int age; public Dog(String name, int age) { this.name = name; this.age = age; } public Dog(String name) { this(name, 0); } }

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.

public static void print(String text) { System.out.println(text); } public static void main(String[] args) { print("Hello World!"); }

Objektmethoden

Alle weiteren Objektmethoden sollten nach den Klassenmethoden stehen und können nur über ein bestehendes Objekt aufgerufen werden.

public class Dog { public String name; public int age; public Dog(String name, int age) { this.name = name; this.age = age; } public Dog(String name) { this(name, 0); } // Object methods public void eat() { System.out.println(this.name + " is eating!"); } public void bark() { System.out.println(this.name + " barks!"); } }

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).

public class Dog { private String name; // Not visible outside the class private int age; // Not visible outside the class public Dog(String name, int age) { this.name = name; this.age = age; } public Dog(String name) { this(name, 0); } public void eat() { System.out.println(this.name + " is eating!"); } public void bark() { System.out.println(this.name + " barks!"); } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } }

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.

public class Main { static { System.out.println("Class initialization block 1"); } public static void main(String[] args) { System.out.println("Main-Program started..."); } static { System.out.println("Class initialization block 2"); } }
Class initialization block 1 Class initialization block 2 Main-Program started...

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

public class Player { private String name; private Properties properties; public Player(String name) { this.name = name; this.properties = new Properties(1, new String[] {"jump", "run", "swim"}); } public String getName() { return name; } public Properties getProperties() { return properties; } // Inner non-static class public class Properties { private int level; private String[] abilities; public Properties(int level, String[] abilities) { this.level = level; this.abilities = abilities; } public int getLevel() { return level; } public void setLevel(int level) { this.level = level; } public String[] getAbilities() { return abilities; } public void setAbilities(String[] abilities) { this.abilities = abilities; } } }

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

public class Product { private String name; private int quantity; private double price; private Product(Builder builder) { this.name = builder.name; this.quantity = builder.quantity; this.price = builder.price; } // Inner static class public static class Builder { private String name; private int quantity; private double price; public Builder setName(String name) { this.name = name; return this; } public Builder setQuantity(int quantity) { this.quantity = quantity; return this; } public Builder setPrice(double price) { this.price = price; return this; } public Product build() { return new Product(this); } } @Override public String toString() { return "Product [name=" + name + ", quantity=" + quantity + ", price=" + price + "]"; } public static void main(String[] args) { Product product = new Product.Builder() .setName("Widget") .setQuantity(10) .setPrice(19.99) .build(); System.out.println(product); } }

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.

public class EmailSender { public void sendEmail(String recipient, String message) { // Inner local class class EmailConnection { public void connect() { System.out.println("Connection to email server established"); } public void disconnect() { System.out.println("Connection to email server disconnected"); } } EmailConnection connection = new EmailConnection(); connection.connect(); System.out.println("E-Mail sent to " + recipient + " with message: " + message); connection.disconnect(); } public static void main(String[] args) { EmailSender sender = new EmailSender(); sender.sendEmail("example@domain.com", "Hello, this is a test email."); } }

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.

public class Main { public static void main(String[] args) { // Inner anonymous class new Runnable() { @Override public void run() { // Code... } }; } }

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.

public record Point(double x, double y) { }
Point point = new Point(34.5, 23.0); System.out.println(point.x()); // 34.5 System.out.println(point.y()); // 23.0 System.out.println(point); // Point[x=34.5, y=23.0]

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:

javac --enable-preview --release 14 Main.java

Zum Ausführen dient der folgende Befehl:

java --enable-preview Main

Coding Conventions

Last modified: 19 August 2024