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.

img/Abb_10_20_UML_GeometrischeFigur.svg
Abb. 10-20: Klassenhierarchie mit einer abstrakten Oberklasse

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.

Aus einer abstrakten Klasse werden niemals Objekte erzeugt. Sie dient lediglich dazu Gemeinsamkeiten verwandter Klassen in einer gemeinsamen Oberklasse zu bündeln.

Im UML-Klassendiagramm wird der Name einer abstrakten Klasse entweder kursiv dargestellt oder unterhalb des Namens um die Eigenschaft {abstract} ergänzt.

img/Abb_10_21_UML_AbstrakteKlasse.svg
Abb. 10-21: Modellierung einer abstrakten Klasse im UML-Klassendiagramm

Im Quellcode wird eine abstrakte Klasse durch das Schlüsselwort abstract deklariert.

JAVA
public abstract class AbstrakteKlasse {
}
Abb. 10-22: Deklaration einer abstrakten Klasse in Java
Merke: Abstrakte Klasse

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.

JAVA
public abstract class GeometrischeFigur {
    
    //...

    public abstract double berechneFlaecheninhalt();
 
}
Abb. 10-23: Deklaration der abstrakten Methode berechneFlaecheninhalt(): double
JAVA
public class Rechteck extends GeometrischeFigur {
    
    //...

    public double berechneFlaecheninhalt(){
        return hoehe*breite;
    }

}
Abb. 10-24: Überschreiben der geerbten Methode 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.

img/Abb_10_25_UML_AbstrakteMethode.svg
Abb. 10-25: Modellierung einer abstrakten Methode im UML-Klassendiagramm

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.

JAVA
public abstract class AbstrakteKlasse {

    public abstract void abstrakteMethode();

}
Abb. 10-26: Deklaration einer abstrakten Methode in Java
Merke: Abstrakte Methode
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.

JAVA
public class Konsole {

    public void zeigeFlaecheninhalt(GeometrischeFigur pGeometrischeFigur) {
        System.out.println("Flächeninhalt: " + pGeometrischeFigur.berechneFlaecheninhalt());
    }
    
}
Abb. 10-27: Methode 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.

JAVA
public class Test {

    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);
    }

}
Abb. 10-28: Der Methode zeigeFlaecheninhalt(pGeometrischeFigur: GeometrischeFigur) wir zunächst ein Objekt der Klasse Rechteck übergeben und anschließend ein Objekt der Klasse Kreis.
Aufgabe

Aufgabe 10-7: Geometrische Figuren

Erstellen Sie den Quellcode der Klassen GeometrischeFigur und Dreieck (vgl. Abb. 10-20). Lösung

Lösung
JAVA
import java.awt.Color;

public abstract class GeometrischeFigur {
    
    private Color farbe;
    
    public Color getFarbe() {
        return farbe;
    }

    public void setFarbe(Color farbe) {
        this.farbe = farbe;
    }

    public abstract double berechneFlaecheninhalt();
 
}
Abb. 10-29: Die Klasse GeometrischeFigur (Quellcode)
JAVA
public class Dreieck extends GeometrischeFigur {

    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;
    }

}
Abb. 10-30: Die Klasse Dreieck (Quellcode)
Aufgabe

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.
  1. Entwickeln Sie ein geeignetes UML-Klassendiagramm. Lösung
    Lösung
    img/Abb_10_31_UML_Konto.svg
    Abb. 10-31: UML-Klassendiagramm
  2. Erstellen Sie den Quellcode der von Ihnen modellierten Klassen. Lösung
    Lösung
    JAVA
    public 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)
    JAVA
    public 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)
    JAVA
    public 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)