Abstrakte Klassen
Die Klasse Form aus der letzten Lektion hatte ein Problem: flaeche() lieferte 0.0. Das ist keine Antwort, sondern eine Notlüge – eine „allgemeine Form“ hat schlicht keinen Flächeninhalt.
Schlimmer noch: Wer eine neue Unterklasse schreibt und das Überschreiben vergisst, bekommt keinen Fehler, sondern lautlos falsche Ergebnisse.
Die Lösung
Eine abstrakte Klasse kann nicht instanziiert werden – man kann kein Objekt von ihr erzeugen. Sie dient nur als gemeinsame Oberklasse.
Eine abstrakte Methode hat keinen Rumpf. Sie legt nur die Signatur fest und verpflichtet jede nicht-abstrakte Unterklasse, sie zu überschreiben.
a) Entferne im Hauptprogramm die Schrägstriche vor Form f = new Form("irgendwas"); und lies die Fehlermeldung.
b) Kommentiere in Rechteck die Methode flaeche() aus. Was meldet die IDE jetzt?
c) Beurteile: Was ist an beiden Fehlermeldungen besser als am Verhalten der nicht-abstrakten Fassung aus der letzten Lektion?
Auflösung
a) Die IDE lehnt es ab, ein Objekt einer abstrakten Klasse zu erzeugen.
b) Die IDE verlangt, dass Rechteck die abstrakte Methode flaeche() implementiert – sonst müsste Rechteck selbst als abstract gekennzeichnet werden.
c) Beide Fehler treten schon beim Übersetzen auf, nicht erst zur Laufzeit. In der alten Fassung hätte eine vergessene flaeche()-Methode lautlos 0.0 geliefert – und die Gesamtfläche wäre falsch gewesen, ohne dass irgendetwas darauf hinweist.
Ein Fehler, den die Sprache dir abnimmt, ist ein Fehler, den du nicht suchen musst.
| abstrakte Klasse | normale Klasse | |
|---|---|---|
| Objekte erzeugbar? | nein | ja |
| darf abstrakte Methoden haben? | ja | nein |
| darf normale Methoden haben? | ja | ja |
| darf Attribute und Konstruktoren haben? | ja | ja |
Im Diagramm wird der Name einer abstrakten Klasse und einer abstrakten Methode kursiv geschrieben.
Der Konstruktor bleibt sinnvoll, obwohl man keine Objekte erzeugen kann: Die Unterklassen rufen ihn mit super(...) auf.
Wann abstrakt, wann konkret?
Entscheide für jede Klasse, ob sie abstrakt sein sollte. Begründe mit der Frage: Kann es davon ein sinnvolles einzelnes Objekt geben?
a) Fahrzeug mit Unterklassen Auto, Fahrrad, LKW
b) Rechteck mit Unterklasse Quadrat
c) Mitarbeiter mit Unterklassen Angestellter, Honorarkraft
d) Konto mit Unterklassen Girokonto, Sparkonto
Auflösung
a) abstrakt. Es gibt kein Fahrzeug, das kein Auto, Fahrrad oder LKW ist.
b) konkret. Ein Rechteck ist ein vollwertiges Objekt für sich – nicht jedes Rechteck ist ein Quadrat.
c) abstrakt. Jede Person in einer Firma ist entweder angestellt oder Honorarkraft. „Mitarbeiter“ allein legt kein Gehalt fest.
d) Das ist Auslegungssache. Wer ein einfaches Guthabenkonto ohne Sonderregeln anbieten will, macht Konto konkret. Wer möchte, dass jedes Konto eine der beiden Sorten ist, macht es abstrakt.
Solche Entscheidungen zu begründen ist genau das, was mit objektorientierte Modellierungen beurteilen gemeint ist.
Aufgabe 1: Die Firma
Eine Firma beschäftigt Angestellte (festes Monatsgehalt nach Gehaltsstufe) und Honorarkräfte (Stundenlohn mal geleistete Stunden).
Setze die Hierarchie so um, dass alle Tests grün werden.
classDiagram
class Mitarbeiter {
<<abstract>>
#String name
+Mitarbeiter(String pName)
+String getName()
+double berechneGehalt()*
+String infozeile()
}
class Angestellter {
-int stufe
+Angestellter(String pName, int pStufe)
+double berechneGehalt()
}
class Honorarkraft {
-double stunden
-double stundensatz
+Honorarkraft(String pName, double pStunden, double pSatz)
+double berechneGehalt()
}
Mitarbeiter <|-- Angestellter
Mitarbeiter <|-- Honorarkraft
Tipp 1: Warum kann das Feld beide Sorten aufnehmen?
Weil beide Mitarbeiter sind. Ein Mitarbeiter[] nimmt jedes Objekt auf, dessen Klasse von Mitarbeiter erbt.
Und beim Aufruf team[i].berechneGehalt() greift die dynamische Bindung: Bei einer Angestellten rechnet Java mit der Gehaltsstufe, bei einer Honorarkraft mit den Stunden. Die Klasse Firma muss die beiden Sorten überhaupt nicht kennen.
Tipp 2: bestbezahlt
Das Muster aus der Einführungsphase: Merke dir den Index der bisher bestbezahlten Person, nicht den Betrag – sonst kommst du am Ende nicht an den Namen.
Und denk an den Sonderfall anzahl == 0.
Aufgabe 2: Die Eisdiele
Eine Eisdiele verkauft zwei Sorten von Bechern:
- Standardbecher zu festen Preisen: klein 6 €, mittel 10 €, groß 15 €.
- Wunschbecher, bei denen man die Kugeln selbst wählt: 1,20 € pro Kugel.
a) Modelliere die Hierarchie. Welche Klasse ist abstrakt, welche konkret? Was steht in der Oberklasse?
b) Zeichne das Implementationsdiagramm.
c) Setze es um und schreibe eine Testklasse mit mindestens sechs Testfällen – darunter mindestens zwei Sonderfälle.
Tipp: Was gehört in die Oberklasse?
Alles, was für jeden Becher gilt: eine Bezeichnung, vielleicht der Name der Sorte, und die abstrakte Methode preis().
Die Frage, aus der man die Antwort ableitet, ist immer dieselbe: Was ist bei allen gleich, was ist bei jedem anders? Was anders ist, wird abstrakt.
Zusatzaufgabe
Erinnerst du dich an das Projekt „Unsere kleine Stadt“? Dort gab es die Erweiterungsidee, alle Objekte zwischen Tag und Nacht umzuschalten – und die Frage, wo diese Methode eigentlich hingehört.
Jetzt kannst du sie beantworten.
a) Entwirf eine abstrakte Klasse Stadtobjekt, von der Haus, Baum und Wolke erben. Welche Methoden sind abstrakt, welche nicht?
b) Setze es um. Die Stadt soll in einer Schleife über ein Stadtobjekt[] laufen und alle auf Nacht umschalten.
c) Beurteile: Wie viele Stellen musst du ändern, wenn eine vierte Objektart dazukommt?