10.6 Abstrakte Klassen
Manche Oberklassen sind so allgemein gehalten, dass sie lediglich die gemeinsamen Attribute und Methoden ihrer Unterklassen bündeln. Sie selbst werden jedoch nie dazu genutzt eigenständige Objekte zu erzeugen.
Beispiel: Geometrische Figuren
In einem Programm sollen verschiedene geometrische Figuren (Rechtecke, Dreiecke, Kreise) dargestellt werden. Für jede Figur soll eine Farbe festgelegt werden können. Außerdem soll es möglich sein ihren Flächeninhalt zu berechnen.
Die Attribute und Methoden, die allen geometrischen Figuren gemeinsam sind, werden in einer gemeinsamen Oberklasse – GeometrischeFigur
– gebündelt. Da es keine geometrische Figur gibt, die ausschließlich diese Attribute und Methoden besitzt, wird auf Grundlage dieser Klasse niemals ein Objekt erzeugt. Die Klasse GeometrischeFigur
wird daher als abstrakt gekennzeichnet.
Trotzdem ist es möglich, einer Variablen den Datentyp GeometrischeFigur
zuzuweisen. Gemäß dem Substitutionsprinzip kann dieser Variable nämlich jedes Objekt zugewiesen werden, das einer Unterklasse von GeometrischeFigur
angehört. Verfügbar sind dann allerdings nur die öffentlichen Attribute und Methoden, die in dieser Klasse deklariert sind.
Abstrakte Methoden
Beispiel: Geometrische Figuren (Fortsetzung)
Alle von der Oberklasse GeometrischeFigur
abgeleitete Klassen erben von dieser die Methode berechneFlaecheninhalt(): double
. Da die Formel zur Berechnung des Flächeninhalts jedoch von der Art der geometrischen Figur abhängt, ist es notwendig, dass jede Unterklasse diese Methode geeignet überschreibt.
Um sicherzustellen, dass jede Unterklasse dies auch tut, wird die Methode in der Klasse GeometrischeFigur
als abstrakt gekennzeichnet. Dies zwingt jede Unterklasse dazu, die Methode zu überschreiben. Eine abstrakte Methode besitzt daher auch keinen Methodenrumpf.
//...
public abstract double berechneFlaecheninhalt();
}
berechneFlaecheninhalt(): double
//...
public double berechneFlaecheninhalt(){
return hoehe*breite;
}
}
berechneFlaecheninhalt(): double
Eine abstrakte Methode besitzt keinen Methodenrumpf und dementsprechend auch keine Anweisungen. Ihr Zweck liegt ausschließlich darin, jede Unterklasse dazu zu zwingen diese Methoden zu überschreiben. Damit ist sichergestellt, dass alle Unterklassen eine Methode mit dieser Signatur implementieren.
Eine Klasse, die mindestens eine abstrakte Methode enthält, muss selbst als abstrakte Klasse gekennzeichnet werden.
Im UML-Klassendiagramm wird eine abstrakte Methode entweder kursiv dargestellt oder um die Eigenschaft {abstract} ergänzt.
Im Quellcode wird eine abstrakte Methode durch das Schlüsselwort abstract
deklariert. Da die Methode von jeder Unterklasse überschrieben werden muss, entfällt der Methodenrumpf. Stattdessen wird die Deklaration mit einem Strichpunkt abgeschlossen.
public abstract void abstrakteMethode();
}
Beispiel: Geometrische Figuren (Fortsetzung)
Die auf dem Konzept der Vererbung basierende Klassenhierarchie erlaubt es, Funktionalitäten, die alle geometrischen Figuren betreffen, nur ein einziges Mal zu implementieren.
So kann im folgenden Beispiel mit Hilfe der Methode zeigeFlaecheninhalt(pGeometrischeFigur: GeometrischeFigur)
, der Flächeninhalt jeder beliebigen geometrischen Figur auf der Konsole ausgegeben werden.
public void zeigeFlaecheninhalt(GeometrischeFigur pGeometrischeFigur) {
System.out.println("Flächeninhalt: " + pGeometrischeFigur.berechneFlaecheninhalt());
}
}
zeigeFlaecheninhalt(pGeometrischeFigur: GeometrischeFigur)
(Quellcode)
Wären die Klassen Rechteck
, Kreis
und Dreieck
als unabhängige Klassen modelliert worden, die keine gemeinsame Oberklasse besitzen, hätte stattdessen für jede dieser Klassen eine eigene Methode implementiert werden müssen.
Test
Es werden ein Objekt der Klasse Rechteck
und ein Objekt der Klasse Kreis
erzeugt und mit entsprechenden Daten initialisiert. Anschließend werden die Flächeninhalte der beiden Objekte auf der Konsole ausgegeben.
Obwohl es sich um Objekte unterschiedliche Klassen handelt, kann hierzu in beiden Fällen die Methode zeigeFlaecheninhalt(pGeometrischeFigur: GeometrischeFigur)
genutzt werden.
public static void main(String[] args) {
Konsole konsole = new Konsole();
Rechteck rechteck = new Rechteck();
Kreis kreis = new Kreis();
rechteck.setBreite(10);
rechteck.setHoehe(5);
kreis.setRadius(10);
konsole.zeigeFlaecheninhalt(rechteck);
konsole.zeigeFlaecheninhalt(kreis);
}
}
zeigeFlaecheninhalt(pGeometrischeFigur: GeometrischeFigur)
wir zunächst ein Objekt der Klasse Rechteck
übergeben und anschließend ein Objekt der Klasse Kreis
.Aufgabe 10-7: Geometrische Figuren
Erstellen Sie den Quellcode der Klassen GeometrischeFigur
und Dreieck
(vgl. Abb. 10-20).
Lösung
Lösung
public abstract class GeometrischeFigur {
private Color farbe;
public Color getFarbe() {
return farbe;
}
public void setFarbe(Color farbe) {
this.farbe = farbe;
}
public abstract double berechneFlaecheninhalt();
}
GeometrischeFigur
(Quellcode)private double grundlinie;
private double hoehe;
public double getGrundlinie() {
return grundlinie;
}
public void setGrundlinie(double grundlinie) {
this.grundlinie = grundlinie;
}
public double getHoehe() {
return hoehe;
}
public void setHoehe(double hoehe) {
this.hoehe = hoehe;
}
@Override
public double berechneFlaecheninhalt() {
return 0.5*grundlinie*hoehe;
}
}
Dreieck
(Quellcode)Aufgabe 10-8: Bankkonten
Eine Bank bietet ihren Kunden Giro- und Tagesgeldkonten an.
Bei beiden Kontenarten werden der jeweilige Kunde, die Kontonummer, der Saldo und der aktuelle Habenzinssatz gespeichert. Außerdem soll es möglich sein einen bestimmten Betrag einzuzahlen bzw. abzuheben.
Bei Girokonten wird zusätzlich die maximale Höhe des Dispokredits und der dafür fällige Zinssatz gespeichert. Außerdem ist das Abheben eines bestimmten Betrags nur zulässig, solange der eingeräumte Dispokreditrahmen dadurch nicht überschritten wird.
Bei Tagesgeldkonten wird stattdessen gespeichert, in welchem Intervall die Zinszahlung erfolgt (zum Beispiel alle drei, sechs oder zwölf Monate). Außerdem ist das Abheben eines bestimmten Betrags nur zulässig, solange der Saldo dadurch nicht negativ wird.
Anforderungen
- Bei Zinssätzen und Geldbeträgen sollen Nachkommastellen möglich sein.
- Alle Zinssätze werden als positive Zahl gespeichert.
- Die Höhe des Dispokredits soll als positive Zahl gespeichert werden.
- Für das Einzahlen bzw. Abheben sollen jeweils eine eigene Methode erstellt werden. Eine Einzahlung bzw. Abhebung darf nur ausgeführt werden, wenn der übergebene Betrag positiv ist.
- Beim Girokonto darf die Abhebung nur ausgeführt werden, wenn der sich dadurch ergebende neue Saldo den Dispokreditrahmen einhält.
- Beim Tagesgeldkonto darf die Abhebung nur ausgeführt werden, wenn der sich dadurch ergebende neue Saldo nicht negativ ist.
-
Entwickeln Sie ein geeignetes UML-Klassendiagramm.
Lösung
Lösung
Abb. 10-31: UML-Klassendiagramm -
Erstellen Sie den Quellcode der von Ihnen modellierten Klassen.
Lösung
Lösung
JAVApublic abstract class Konto {
private Kunde kunde;
private int kontonummer;
private double saldo;
private double zinssatzHaben;
public Konto(Kunde pKunde, int pKontonummer) {
this.kunde = pKunde;
this.kontonummer = pKontonummer;
}
public Kunde getKunde() {
return kunde;
}
public void setKunde(Kunde pKunde) {
this.kunde = pKunde;
}
public int getKontonummer() {
return kontonummer;
}
public void setKontonummer(int pKontonummer) {
this.kontonummer = pKontonummer;
}
public double getSaldo() {
return saldo;
}
public void setSaldo(double pSaldo) {
this.saldo = pSaldo;
}
public double getZinssatzHaben() {
return zinssatzHaben;
}
public boolean setZinssatzHaben(double pZinssatzHaben) {
boolean ausgefuehrt = false;
if(pZinssatzHaben>0) {
this.zinssatzHaben = pZinssatzHaben;
ausgefuehrt = true;
}
return ausgefuehrt;
}
public boolean einzahlen(double pBetrag) {
boolean ausgefuehrt = false;
if(pBetrag>0) {
saldo = saldo + pBetrag;
ausgefuehrt = true;
}
return ausgefuehrt;
}
public abstract boolean abheben(double pBetrag);
}Abb. 10-32: Abstrakte Klasse Konto
(Quellcode)JAVApublic class Girokonto extends Konto {
private double dispokredit;
private double zinssatzDispokredit;
public Girokonto(Kunde pKunde, int pKontonummer) {
super(pKunde, pKontonummer);
}
public double getDispokredit() {
return dispokredit;
}
public boolean setDispokredit(double pDispokredit) {
boolean ausgefuehrt = false;
if(pDispokredit>=0) {
this.dispokredit = pDispokredit;
ausgefuehrt = true;
}
return ausgefuehrt;
}
public double getZinssatzDispokredit() {
return zinssatzDispokredit;
}
public boolean setZinssatzDispokredit(double pZinssatzDispokredit) {
boolean ausgefuehrt = false;
if(pZinssatzDispokredit>=0) {
this.zinssatzDispokredit = pZinssatzDispokredit;
ausgefuehrt = true;
}
return ausgefuehrt;
}
@Override
public boolean abheben(double pBetrag) {
boolean ausgefuehrt = false;
double neuerSaldo;
if(pBetrag>0) {
neuerSaldo = this.getSaldo()-pBetrag;
if(neuerSaldo>=dispokredit*(-1)) {
this.setSaldo(neuerSaldo);
ausgefuehrt = true;
}
}
return ausgefuehrt;
}
}Abb. 10-33: Klasse Girokonto
(Quellcode)JAVApublic class Tagesgeldkonto extends Konto {
private int zinsintervall;
public Tagesgeldkonto(Kunde pKunde, int pKontonummer) {
super(pKunde, pKontonummer);
}
public int getZinsintervall() {
return zinsintervall;
}
public boolean setZinsintervall(int pMonate) {
boolean ausgefuehrt = false;
if(pMonate>0) {
this.zinsintervall = pMonate;
ausgefuehrt = true;
}
return ausgefuehrt;
}
@Override
public boolean abheben(double pBetrag) {
boolean ausgefuehrt = false;
double neuerSaldo;
if(pBetrag>0) {
neuerSaldo = this.getSaldo()-pBetrag;
if(neuerSaldo>=0) {
this.setSaldo(neuerSaldo);
ausgefuehrt = true;
}
}
return ausgefuehrt;
}
}Abb. 10-34: Klasse Tagesgeldkonto
(Quellcode)