Abstrakte Klassen und Interfaces
https://bildung.social/@oerinformatik/
https://oer-informatik.de/uml-klassendiagramm-interface-abstraktion
tl/dr; (ca. 8 min Lesezeit): Ein großes Problem bei Objekt- und Klassenbeziehung ist die starke Kopplung unterschiedlicher Klassen aneinander. Der Austausch von Verhalten ist so nur mit großem Aufwand möglich. Das Konzept der Abstraktion bietet hier in Form von abstrakten Klassen und Interfaces die Möglichkeit, Komponenten lose zu koppeln. (Zuletzt geändert am 08.04.2025)
Dieser Text ist ein Teil der Infotext-Serie zu UML-Klassendiagrammen:
Abstrakte Klassen
Wozu abstrakte Klassen dienen, soll an folgendem Beispiel erläutert werden: es soll das gesamte Vermögen, das in Aktien, auf Konten und als Festgeld angelegt ist, jederzeit ausgewertet werden können. Dazu sollen die Klassen Konto
, Aktien
und Festgeld
von einer gemeinsamen Elternklasse Geldanlage
erben, in der festgelegt ist, dass wir über die Methode getAnlagenwert()
immer den aktuellen Zeitwert der Geldanlage erhalten.

Da das Guthaben zum Zeitwert beim Konto
schlicht über den Kontostand, beim Festgeld
über die Verzinsung der Einlage und bei einer Aktie
über den aktuellen Börsenwert ermittelt werden muss, kann die Methode getAnlagenwert()
nicht sinnvoll zentral in Geldanlage
implementiert werden. Wir können verdeutlichen, dass jede Unterklasse eine eigene Implementierung benötigt, in dem wir den Methodenrumpf leer lassen und lediglich die Signatur (also Name + Parameter) und der Rückgabewert festlegen: derlei Methoden nennt man abstrakt.
Durch die fehlende Implementierung von getAnlagenwert()
ist das Verhalten einer Geldanlage
nicht vollständig definiert - es können keine konkreten Instanzen (Objekte) von Geldanlage
gebildet werden. Klassen wie Geldanlage
, aus denen keine Instanzen gebildet werden dürfen oder sollen, werden auch als abstrakt bezeichnet.
Im UML-Klassendiagramm werden abstrakte Klassen und Methoden durch kursive Schreibweise gekennzeichnet:

Abstrakte Klassen dürfen auch Implementierungen enthalten (siehe Getter/Setter + Attribut name
).
Die einzelnen Kindklassen müssen alle abstrakten Methode (hier: getAnlagenwert()
) implementieren, damit sie selbst konkrete Klassen sind, die instanziiert werden können:

Die kursive Schreibweise kann schnell übersehen werden, daher ist es ratsam, zusätzlich das Schlüsselwort {abstract}
als constraint in geschweiften Klammern unterhalb des Klassennamens zu notieren.

In der Regel werden zwar nur Klassen mit abstrakten Methoden selbst als abstrakt dargestellt, theoretisch kann jedoch jede Klasse als abstrakt gekennzeichnet werden, auch wenn sie instanziierbar wäre. Umgekehrt gilt jedoch:
Sobald eine Klasse über mindestens eine abstrakte Methode verfügt, muss sie als abstrakt gekennzeichnet werden. Das gilt auch für Klassen, die abstrakte Methoden erben: im folgenden Beispiel ist Konto
abstrakt, da es die abstrakte Methode getAnlagewert()
erbt und nicht selbst implementiert. Erst von den Kindklassen PrivatKonto
und Geschäftskonto
können Instanzen (Objekte) gebildet werden, da sie die abstrakte Methode implementieren.

Alle Klassen, die nicht instanziiert werden können oder sollen, werden abstrakt genannt. Alle Klassen, die instanziiert werden können oder sollen, werden konkrete Klassen genannt.
Abstrakte Klassen können alle Member und Eigenschaften enthalten, die auch konkrete Klassen enthalten können. Von Klassen, die als abstrakt gekennzeichnet wurden, können keine Instanzen gebildet werden (unabhängig davon, ob diese abstrakte Methoden enthalten).
Als Java-Quelltext sieht die abstrakte Klasse Geldanlage
und die konkreten Kindklassen Festgeld
und Aktie
beispielsweise so aus:
package de.oerinformatik.crm;
import java.time.LocalDate;
import java.time.Period;
public abstract class Geldanlage {
private String name;
public Geldanlage(String name){
this.setName(name);
}
public String getName(){
return this.name;
}
public void setName(String name){
this.name = name;
}
public abstract double getAnlagewert();
}
class Aktie extends Geldanlage{
private int gesamtanzahl = 0;
private double kurswert = 0;
public Aktie(String name){
super(name);
}
public double getAnlagewert(){
return this.gesamtanzahl * this.kurswert;
}
public void kaufeAktie(int anzahl){
gesamtanzahl += anzahl;
}
public boolean verkaufeAktie(int anzahl){
if (anzahl>=gesamtanzahl){
gesamtanzahl -= anzahl;
return true;
}
return false;
}
public void setKurswert(double kurswert){
this.kurswert = kurswert;
}
}
class Festgeld extends Geldanlage{
private double betrag = 0.0;
private LocalDate startdatum;
private LocalDate enddatum;
private double zins;
public Festgeld(double betrag, LocalDate startdatum, LocalDate enddatum, double zins){
super("Festgeld");
this.betrag = betrag;
this.enddatum = enddatum;
this.startdatum = startdatum;
this.zins = zins;
}
public double getAnlagewert(){
Period dauer = Period.between(startdatum, LocalDate.now());
System.out.println("Dauer des Investments: "+dauer.toString());
int days = dauer.getDays();
System.out.println("Dauer in Tagen: "+Integer.toString(days));
double virtuellesKapital = this.betrag * Math.pow((1 + this.zins),(days/360.0));
System.out.println("Virtuelles Kapital: "+Double.toString(virtuellesKapital));
return virtuellesKapital;
}
}
Interfaces
Das Konzept der Abstraktion wird mit Interfaces weiterentwickelt: Interfaces stellen abstrakte Strukturen dar, die im engeren Sinn komplett frei von Implementierung sind. Im Kern legen Interfaces lediglich Methodensignaturen und Rückgabewerte fest.
Interfaces stellen Verträge zwischen Klassen dar, die festschreiben, welche Methoden eine Klasse implementieren muss. Beispielsweise legt das Interface Verzinsbar
fest, dass alle Klassen, die es realisieren (implementieren), eine Methode verzinse()
anbieten müssen. Greift man auf eine Klasse zu, die gegen das Interface implementiert wurde, kann man sich also sicher sein, dass diese Methode existiert.

verzinse()
implementiert werden mussInterfaces werden im UML-Diagramm wie Klassen dargestellt, die mit dem Stereotyp <<Interface>>
oberhalb des Namens notiert werden. Da die Struktur abstrakt ist, wird Name und Methode i.d.R. kursiv dargestellt.
Interfaces existieren nicht in allen Programmiersprachen (daher fehlen hier z.B. Python-Beispiele). In Java sieht das Interface beispielsweise so aus:
Klassen, die die abstrakten des Interfaces implementieren, werden mit einem “Implementierungspfeil” gekennzeichnet - mit einer nicht ausgefüllten Pfeilspitze (wie bei Vererbung) in Richtung Interface, jedoch statt der durchgezogenen Linie der Vererbung mit einer gestrichelten Linie:

SparKonto
implementiert Verzinsbar
(und somit verzinse()
)Sofern die Klassen die abstrakten Methoden eines Interfaces nicht selbst implementieren, sind die Klassen selbst abstrakt. Im folgenden Beispiel implementiert die Klasse SparAnlage
das Interface Verzinsbar
unvollständig (verzinse()
bleibt abstrakt) und ist daher selbst abstrakt.

SparAnlage
implementiert die Methode verzinse()
des Interfaces nicht, bleibt also abstraktDie Grenzen zwischen Interfaces und abstrakten Klassen sind in den unterschiedlichen Programmiersprachen unterschiedlich stark ausgeprägt:
In Java gelten beispielsweise folgende Regeln:
Interfaces können statische Attribute enthalten
Interfaces können default-Implementierungen enthalten
Eine Klasse, die das Interface Verzinsbar
implementiert und ein Verhalten für die darin definierte abstrakte Methode festlegt, sieht in Java beispielsweise so aus:
interface Verzinsbar{
public abstract void verzinse();
}
class SparKonto implements Verzinsbar{
private double zinssatz;
private double kontostand;
public SparKonto(double zinssatz, double kontostand){
this.zinssatz = zinssatz;
this.kontostand = kontostand;
}
public void verzinse(){
this.kontostand = this.kontostand * (1+zinssatz);
}
}
Benutzen von Interfaces und Implementieren von Interfaces: Dependency Inversion Principle
Interfaces werden zur losen Kopplung zwischen Klassen genutzt. Häufig hängen Klassen über Assoziationen direkt von der Implementierung anderer Klassen ab. Im folgenden Beispiel benötigt die Klasse Kunde
ein Objekt vom Typ Konto
, um einen Artikel zahlen zu können:

Änderungen an Konto
können somit unmittelbar dazu führen, dass sich auch die Implementierung in Kunde
ändern muss. Das ist insbesondere dann nicht wünschenswert, wenn sich beide in unterschiedlichen Modulen befinden (da Kunde
Konto
nutzt wäre es z.B. denkbar, dass sich dieses in einem Modul höheren Abstraktionsniveaus befindet).
An die Stelle der direkten Abhängigkeit tritt ein Vertrag zwischen Kunde
und Konto
: Die Abhängigkeit besteht darin, dass es die Methoden abbuchen()
und einzahlen()
geben muss: Kunde
will diese Nutzen, und das Objekt, das Kunde
nutzt, muss diese Methoden bereitstellen:

In diesem Diagramm ist die Abhängigkeit zwischen Kunde
und Zahlbar
ganz allgemein als <<use>>
angegeben - Kunde nutzt das Interface. Man darf natürlich auch genauer spezifizieren, wenn bekannt ist, welcher Art die Abhängigkeit ist (ob es sich also um eine Assoziation, Aggreagrion oder Komposition handelt). In unserem Beispiel wäre also die Darstellung einer Assoziation zwischen Kunde
und Zahlbar
auch korrekt (und etwas präziser):

Das Interface sieht in Java so aus:
interface Zahlbar{
public abstract boolean auszahlen(double betrag);
public abstract boolean einzahlen(double betrag);
}
Die Klasse Konto müsste die beiden Methoden implentieren:
public class Konto implements Zahlbar{
private double kontostand;
//...
public boolean einzahlen(double betrag) {
kontostand = kontostand + betrag;
return true;
}
public boolean auszahlen(double betrag) {
if (kontostand - betrag > 0){
kontostand = kontostand - betrag;
return true;
}else{
return false;
}
}
In der Klasse Kunde wird eine Instanz genutzt, die das Interface Zahlbar
implementiert (also z.B. ein Konto
):
public boolean bezahleRechnung(Rechnung rechnung, Zahlbar konto){
double kosten = rechnung.getRechnungsbetrag();
return konto.auszahlen(kosten);
}
Aufgerufen würde das ganze dann etwa so:
Solange das Interface unverändert bleibt, können die jeweiligen Implementierungen geändert, ja sogar getauscht werden: Kunde hängt nur noch von der abstrakten Struktur Zahlbar
ab, aber nicht mehr von einer konkreten Implementierung. Realisiert werden kann diese Abhängigkeit auch über andere Implementierungen von Zahlbar
.

Kunde nutzt ein Objekt vom Typ Zahlbar
, um einen Artikel zu bezahlen (siehe Auszug aus der Methode bezahleRechnung()
).
Lollipop- / Ball-and-Socket-Notation
Wenn der Aufbau des Interfaces nicht so wichtig ist, wird häufig die Lollipop- (oder Ball and Socket-) Notation gewählt, die nur die Schnittstelle benennt, die Member des Interface aber nicht zeigt:

Bei Kunde ist mit der stilisierten Buchse gekennzeichnet, dass ein Zahlbar
-Objekt benötigt wird. Konto
stellt dieses Objekt zur Verfügung, war durch den stilisierten Stecker dargestellt wird.
Java-Beispiele und Gesamtübersicht (als Big Picture)
Der Quelltext für eine Implementierung der OOP-Beispiele findet sich im Gitlab-Repository unter src/app/ ( java | python )
. Das Gesamtprojekt sieht etwa so aus:

Links und weitere Informationen
Hinweis zur Nachnutzung als Open Educational Resource (OER)
Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Abstrakte Klassen und Interfaces” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/uml-klassendiagramm-interface-abstraktion veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/uml/umlklasse. Stand: 08.04.2025.
[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]