Abstrakte Klassen in Java
Partielle Implementierungen in Java mit abstrakten Klassen und Methoden definieren, die Unterklassen vervollständigen müssen.
Eine abstrakte Klasse ist eine Klasse, die nicht direkt instanziiert werden kann. Sie existiert, um erweitert zu werden. Sie kann konkrete Methoden (mit Rumpf) und abstrakte Methoden (ohne Rumpf, den die Unterklasse bereitstellen muss) kombinieren — diese Kombination unterscheidet sie sowohl von einer regulären Klasse als auch von einem Interface.
Diese Seite erklärt, wie man eine abstrakte Klasse deklariert, wie Unterklassen sie vervollständigen, warum abstrakte Klassen Zustand halten können, das Template-Method-Muster und die Wahl zwischen einer abstrakten Klasse und einem Interface.
Verwende eine abstrakte Klasse, wenn konkrete Unterklassen Zustand und Infrastruktur teilen müssen, nicht nur einen API-Vertrag. Wenn nur ein Vertrag benötigt wird, ist ein Interface die bessere Wahl. Abstrakte Klassen bauen auf Vererbung und Polymorphismus auf, daher ist es hilfreich, mit diesen zunächst vertraut zu sein.
Deklaration
Füge abstract zum Klassenkopf hinzu. Füge abstract zu jeder Methode hinzu, die keinen Rumpf hat:
public abstract class Shape {
protected final String name;
protected Shape(String name) { this.name = name; }
public abstract double area(); // no body — subclass must provide one
public String describe() { // concrete — inherited as-is
return name + " area=" + area();
}
}Daraus folgt:
new Shape("circle")ist ein Kompilierfehler — abstrakte Klassen können nicht instanziiert werden.- Eine Unterklasse, die nicht alle geerbten abstrakten Methoden implementiert, muss selbst als
abstractdeklariert werden. Es können abstrakte Unterklassen von abstrakten Klassen existieren. - Eine abstrakte Klasse kann einen Konstruktor haben — Unterklassen rufen ihn mit
super(...)auf, genau wie bei einem regulären Elternteil.
Implementierung der abstrakten Methoden
Eine konkrete Unterklasse muss für jede geerbte abstrakte Methode einen Rumpf bereitstellen:
public class Circle extends Shape {
private final double r;
public Circle(double r) {
super("circle");
this.r = r;
}
@Override
public double area() { return Math.PI * r * r; }
}Nun funktioniert new Circle(2), und describe() (geerbt von Shape) ruft über dynamisches Dispatch die area()-Methode der Unterklasse auf.
Abstrakte Klassen können Zustand halten
Dies ist der Hauptgrund, eine abstrakte Klasse einem Interface vorzuziehen. Die Elternklasse kann Felder deklarieren, einen Konstruktor schreiben, der sie initialisiert, und Methoden anbieten, die auf diesem gemeinsamen Zustand operieren:
public abstract class HttpHandler {
private final String path;
protected HttpHandler(String path) { this.path = path; }
public final String path() { return path; }
public abstract Response handle(Request r); // subclass-specific behavior
}Jeder konkrete Handler hat einen path; die Elternklasse speichert und exponiert ihn; jede Unterklasse schreibt nur die anfragespezifische Logik. Interfaces können dies nicht alleine (sie haben keine Instanzfelder).
Kombination aus abstrakten und konkreten Methoden — die Template-Methode
Ein häufiges Muster: Die abstrakte Klasse implementiert den Gesamtablauf als konkrete Methode und lässt die Variationspunkte abstrakt. Unterklassen füllen nur die Teile aus, die sich unterscheiden:
public abstract class Beverage {
// Template — the algorithm, written once.
public final void prepare() {
boilWater();
brew(); // varies
pourIntoCup();
addCondiments(); // varies
}
protected abstract void brew();
protected abstract void addCondiments();
private void boilWater() { System.out.println("boiling water"); }
private void pourIntoCup() { System.out.println("pouring into cup"); }
}
public class Tea extends Beverage {
protected void brew() { System.out.println("steeping tea"); }
protected void addCondiments() { System.out.println("adding lemon"); }
}Tea.prepare() führt das Template der Elternklasse aus, das über Polymorphismus auf Teas brew und addCondiments zurückgreift. Das Hinzufügen einer Coffee-Unterklasse erfordert nur die beiden abstrakten Methoden.
Dies ist das Template-Method-Muster und der bei weitem häufigste Grund, eine abstrakte Klasse zu wählen.
Abstrakt vs. final
abstract und final sind Gegensätze, und der Compiler erzwingt dies:
abstract class— muss abgeleitet werden.final class— darf nicht abgeleitet werden.
Gleiches gilt für Methoden: abstract-Methoden müssen überschrieben werden; final-Methoden dürfen nicht. Beide gleichzeitig anzugeben ist ein Kompilierfehler.
Abstrakte Klasse vs. Interface
| Abstrakte Klasse | Interface | |
|---|---|---|
| Konstruktoren | Ja | Nein |
| Instanzfelder | Ja | Nein (nur public static final-Konstanten) |
| Methodenrümpfe | Ja (beliebig viele) | Ja über default (sparsam einzusetzen) |
| Vererbung | Einfach — nur eine Elternklasse | Mehrfach — viele Interfaces |
| Verwenden wenn | Unterklassen teilen Zustand und Infrastruktur | Unterklassen teilen nur einen API-Vertrag |
Eine praktische Faustregel: Beginne mit einem Interface. Wechsle zu (oder ergänze) eine abstrakte Klasse nur, wenn sich gemeinsamer Code über Implementierungen hinweg ansammelt. Java 8+ default-Methoden auf Interfaces haben einige Einsatzgebiete abstrakter Klassen übernommen, aber der Anwendungsfall mit gemeinsamem veränderlichem Zustand gehört nach wie vor zu abstrakten Klassen.
Abstrakte Methoden dürfen nicht private oder static sein
privatewürde die Methode für Unterklassen unsichtbar machen — sie könnten sie nicht überschreiben, sodass die Abstraktheit nicht durchsetzbar wäre.static-Methoden werden nicht dynamisch dispatcht — sie können nicht überschrieben, nur verborgen werden — daher wäre eine abstrakte statische Methode sinnlos.
Diese beiden Kombinationen sind Kompilierfehler.
Häufige Fehler
abstractbei der Klasse vergessen. Eine Methode ohne Rumpf innerhalb einer nicht-abstrakten Klasse wird nicht kompiliert — der Compiler verlangt, dass die umschließende Klasse ebenfallsabstractist.- Versuch der Instanziierung.
new Shape("x")wird zur Kompilierzeit abgelehnt. Stattdessen eine konkrete Unterklasse instanziieren. - Eine Methode in einer konkreten Unterklasse nicht implementieren. Wenn auch nur eine geerbte abstrakte Methode keinen Rumpf hat, muss die Unterklasse ebenfalls als
abstractdeklariert werden. - Mehrfachvererbung erwarten. Eine Klasse kann nur eine Elternklasse erweitern (abstrakt oder nicht). Wenn mehrere Verträge kombiniert werden müssen, sind Interfaces die richtige Wahl.
Ein ausgearbeitetes Beispiel
Wie geht es weiter
Du hast nun die Variante der Abstraktion kennengelernt, die mit Zustand und gemeinsamem Code kombiniert wird. Die Variante, die beides weglässt — reiner Vertrag, keine Implementierung — ist das Interface. Weiter zu Java-Interfaces.