java\08_OOP_Entwurfsmuster\ (Start mit java Aufgabe_OOP_NN_k.java)
Die Klasse Paketfach ist der Bauplan (Datentyp): Sie legt fest, welche Attribute (Fachnummer, Größe, Inhalt, Abholcode) und welche Methoden (einlegen, entnehmen, istFrei …) jedes Paketfach hat. Ein Objekt ist ein konkretes Paketfach, das zur Laufzeit mit new erzeugt wird und eigene Attributwerte hat, z. B. Fach 7 der Station Hafenstraße, Größe "M", belegt mit Sendung KT-1001. Für die Station gibt es also eine Klasse, aber 24 Paketfach-Objekte.
Die Attribute eines Paketfachs (fachNr, groesse, inhalt, abholcode) werden nach außen verborgen (Sichtbarkeit private). Andere Objekte, z. B. die Steuerung der Paketstation, können den Zustand nur über die öffentlichen Methoden lesen oder ändern (getGroesse(), istFrei(), einlegen, entnehmen). Niemand kann von außen direkt inhalt überschreiben oder den Abholcode auslesen.
Vorteile (zwei davon):
Der Konstruktor wird beim Erzeugen eines Objekts (new) automatisch aufgerufen und initialisiert das neue Objekt, d. h. er setzt die Anfangswerte der Attribute.
| Nr. | Ausgabe | Begründung |
|---|---|---|
| (1) | 1000 | Ahrens | 4.0 kg | e3 = e1 kopiert nur die Referenz. e3.setGewicht(4.0) ändert also dasselbe Objekt, auf das e1 zeigt. |
| (2) | 1001 | Bode | 1.0 kg | Zweiter Konstruktor ruft this("Bode", 1.0) auf. setGewicht(40.0) wird abgelehnt (über 31,5 kg), das Gewicht bleibt 1.0. |
| (3) | true | e1 und e3 verweisen auf dasselbe Objekt. |
| (4) | false | e4 ist ein eigenes Objekt (Nummer 1002) – andere Identität. |
| (5) | true | equals vergleicht Empfänger und Gewicht: beide "Ahrens" und 4.0. |
| (6) | 4 | Es wurden vier Objekte erzeugt (1000 bis 1003). Auch e5 zählt: Die Nummer wird vergeben, bevor das ungültige Gewicht 0.0 abgelehnt wird. |
Ausgabe · Aufgabe_OOP_01_2.java
1000 | Ahrens | 4.0 kg 1001 | Bode | 1.0 kg true false true 4
e5 (3 ist falsch). Die genaue Darstellung der Zahl (4.0 oder 4) ist unerheblich.== vergleicht bei Objekten die Referenzen (Identität): e1 und e4 sind zwei verschiedene Objekte im Speicher, daher false. Die Klasse Etikett überschreibt equals so, dass inhaltlich verglichen wird (Empfänger und Gewicht) – beide Objekte haben gleiche Werte, daher true.
naechsteNr gehört als statisches Attribut zur Klasse und existiert nur einmal für alle Etiketten. Jeder Konstruktoraufruf liest und erhöht denselben Zähler – so entstehen fortlaufende, eindeutige Nummern, und die statische Methode getAnzahl() kann ohne Objekt aufgerufen werden.
Als normales Attribut hätte jedes Objekt eine eigene Kopie mit dem Startwert 1000: Alle Etiketten bekämen die Nummer 1000, die Nummern wären nicht mehr eindeutig. Außerdem dürfte die statische Methode getAnzahl() nicht mehr auf das Attribut zugreifen (Compilerfehler).
Datei: java\08_OOP_Entwurfsmuster\Aufgabe_OOP_01_2.java (enthält zusätzlich ein Beispiel für die NullPointerException)
Paketfach mit statischem Zähler (unterstrichen) und gerichteter Assoziation zu Sendung- (2 P); statisches Attribut anzahlFaecher unterstrichen oder mit {static} (1 P); beide Konstruktoren (1 P); Methoden getFachNr, getGroesse, istFrei, einlegen, entnehmen mit vollständiger Signatur (1 P); getAnzahlFaecher statisch und rang privat (1 P); Beziehung zu Sendung (1 P).inhalt nur als Rollenname an der Assoziation statt im Attributfach; Multiplizität 1 statt 0..1 (der Belegsatz kennt nur 1 und *); Aggregation (ein Fach „enthält“ eine Sendung). Keine Komposition: Die Sendung existiert unabhängig vom Fach weiter. {final} ist nicht gefordert.Erklärung: Die Anzahl aller Fächer soll ohne Paketfach-Objekt abrufbar sein – das verlangt ein statisches Attribut und eine statische Methode. Fachnummer und Größe sind nach der Erzeugung unveränderlich, also gibt es nur Getter und keine Setter. rang ist eine interne Hilfsmethode und deshalb privat.
konstruktor Paketfach(fachNr : integer, groesse : string) this.fachNr := fachNr this.groesse := groesse inhalt := null abholcode := null anzahlFaecher := anzahlFaecher + 1 Ende konstruktor konstruktor Paketfach(fachNr : integer) this(fachNr, "M") Ende konstruktor
this (1 P), Zähler erhöhen (1 P), zweiter Konstruktor ruft this(fachNr, "M") auf (1 P). Die Zuweisung von null ist optional (Standardwert). Wer im zweiten Konstruktor den Code kopiert statt this(…) zu verwenden, verliert den dritten Punkt.methode einlegen(s : Sendung, code : string) : boolean
wenn istFrei() und rang(s.getGroesse()) <= rang(groesse) dann
inhalt := s
abholcode := code
rückgabe true
Ende wenn
rückgabe false
Ende methode
rang mit <= (1 P), Sendung und Code speichern und passende Rückgaben (1 P). istFrei() darf durch inhalt = null ersetzt werden.methode entnehmen(code : string) : Sendung
wenn nicht istFrei() und abholcode = code dann
s : Sendung := inhalt
inhalt := null
abholcode := null
rückgabe s
Ende wenn
rückgabe null
Ende methode
null (1 P).Typischer Fehler: erst inhalt := null und danach rückgabe inhalt – dann wird immer null zurückgegeben. Deshalb die Sendung vorher in einer lokalen Variablen merken. Ohne die Prüfung „Fach belegt“ würde bei leerem Fach null = code verglichen; in Java wäre abholcode.equals(code) dann sogar eine NullPointerException.
Java · Aufgabe_OOP_01_3.java (Klasse Paketfach)
class Paketfach {
private static int anzahlFaecher = 0;
private final int fachNr;
private final String groesse;
private Sendung inhalt;
private String abholcode;
public Paketfach(int fachNr, String groesse) {
this.fachNr = fachNr;
this.groesse = groesse;
this.inhalt = null;
this.abholcode = null;
anzahlFaecher = anzahlFaecher + 1;
}
public Paketfach(int fachNr) {
this(fachNr, "M");
}
public static int getAnzahlFaecher() {
return anzahlFaecher;
}
public int getFachNr() {
return fachNr;
}
public String getGroesse() {
return groesse;
}
public boolean istFrei() {
return inhalt == null;
}
public boolean einlegen(Sendung s, String code) {
if (s != null && istFrei() && rang(s.getGroesse()) <= rang(groesse)) {
inhalt = s;
abholcode = code;
return true;
}
return false;
}
public Sendung entnehmen(String code) {
if (!istFrei() && abholcode.equals(code)) {
Sendung s = inhalt;
inhalt = null;
abholcode = null;
return s;
}
return null;
}
private int rang(String groesse) {
if (groesse.equals("S")) {
return 1;
} else if (groesse.equals("M")) {
return 2;
} else if (groesse.equals("L")) {
return 3;
}
return 0;
}
}
Ausgabe des Testprogramms
Erzeugte Fächer: 3
Fach 2 hat die Größe M
f1.einlegen(KT-1001 [M]) -> false (Sendung zu groß für S)
f2.einlegen(KT-1001 [M]) -> true
f2.einlegen(KT-1003 [S]) -> false (Fach belegt)
f3.einlegen(KT-1002 [L]) -> true
f1.einlegen(KT-1003 [S]) -> true
f2.entnehmen("000000") -> null (falscher Code)
f2.entnehmen("482913") -> KT-1001
f2.istFrei() -> true
f2.entnehmen("482913") -> null (Fach leer)
| Aussage | trifft zu auf | Begründung |
|---|---|---|
1. Mit new direkt Objekte erzeugen | keines | Weder abstrakte Klassen noch Interfaces sind instanziierbar. |
| 2. Attribute mit Werten je Objekt | abstrakte Klasse | Interfaces haben nur Konstanten. |
| 3. Eine Klasse kann mehrere davon erweitern/implementieren | Interface | Nur eine Oberklasse (extends), beliebig viele Interfaces (implements). |
| 4. Als Datentyp verwendbar | beide | z. B. List<Fahrzeug> oder Kuehlbar k |
5. Konstruktor, Aufruf mit super(…) | abstrakte Klasse | Interfaces haben keinen Konstruktor. |
| 6. Methoden ohne Rumpf, die implementiert werden müssen | beide | abstrakte Methoden bzw. Interface-Methoden |
Entscheidung: Interface Druckbar.
Abholschein erbt bereits von Dokument. Java erlaubt nur eine Oberklasse – eine abstrakte Klasse Druckbar könnte Abholschein nicht zusätzlich erweitern. Interfaces kann eine Klasse dagegen beliebig viele implementieren.List<Druckbar> arbeiten und kennt die konkreten Klassen nicht (lose Kopplung).Abholschein falsch (0 P für die Entscheidung).Fahrzeug LR-07 angelegt Lastenrad LR-07 bereit Fahrzeug KT-02 angelegt Kuehlung auf 20 Grad
Erklärung: Jeder Unterklassen-Konstruktor ruft als erste Anweisung super(…) auf. Der Konstruktor von Fahrzeug läuft deshalb vollständig durch (erste Zeile), erst danach der Rest des Unterklassen-Konstruktors. Beim Kühltransporter wird temperatur erst nach dem super-Aufruf auf 20 gesetzt.
| Zeile | Fehler | Korrektur |
|---|---|---|
| 1 | EScooter ist nicht abstrakt, implementiert aber die abstrakte Methode kostenProKm() nicht. | public double kostenProKm() { return 0.03; } ergänzen |
| 2/3 | Kein Aufruf des Oberklassen-Konstruktors. Das automatische super() scheitert, weil Fahrzeug keinen parameterlosen Konstruktor hat. | erste Anweisung super(kennung, 35); (Zeile 3 entfällt) |
| 8 | info() ist in Fahrzeug final und darf nicht überschrieben werden. | Methode entfernen oder unter anderem Namen anlegen, z. B. beschreibung() (ohne @Override) |
| 9 | kennung ist in Fahrzeug private und in der Unterklasse nicht sichtbar. | getKennung() verwenden (oder Attribut protected machen) |
| 14 | Fahrzeug ist abstrakt und kann nicht mit new instanziiert werden. | eine konkrete Unterklasse erzeugen, z. B. Fahrzeug reserve = new Lastenrad("RES-1"); |
Meldungen von javac 21 für die fehlerhafte Fassung (gekürzt)
Fehler: Fahrzeug ist abstrakt, kann nicht instanziiert werden
Fehler: EScooter ist nicht abstrakt und setzt die abstrakte Methode kostenProKm() in Fahrzeug nicht außer Kraft
Fehler: Konstruktor Fahrzeug in Klasse Fahrzeug kann nicht auf die angegebenen Typen angewendet werden.
Erforderlich: String,int Ermittelt: keine Argumente
Fehler: info() in EScooter kann nicht info() in Fahrzeug außer Kraft setzen. Außer Kraft gesetzte Methode ist final
Fehler: kennung hat private-Zugriff in Fahrzeug
5 Fehler
Java · korrigierte Klasse (Aufgabe_OOP_02_2.java)
class EScooter extends Fahrzeug {
public EScooter(String kennung) {
super(kennung, 35); // Fehler 1 behoben
System.out.println("E-Scooter bereit");
}
@Override
public double kostenProKm() { // Fehler 2 behoben
return 0.03;
}
public String beschreibung() { // Fehler 3 behoben (info() ist final)
return "E-Scooter " + getKennung(); // Fehler 4 behoben (kennung ist private)
}
}
// Fehler 5: new Fahrzeug(...) ist nicht erlaubt -> konkrete Unterklasse instanziieren
Kuehltransporter erbt von Fahrzeug (ein Kühltransporter ist ein Fahrzeug) und realisiert Kuehlbar. Ein Objekt einer Klasse ist mit allen Oberklassen und allen implementierten Interfaces typkompatibel (Upcast). Über die Fahrzeug-Variable sind dann nur die Fahrzeug-Methoden aufrufbar, über die Kuehlbar-Variable nur kuehlen.
Ausgabe des gesamten Testprogramms
--- a) Konstruktorverkettung --- Fahrzeug LR-07 angelegt Lastenrad LR-07 bereit Fahrzeug KT-02 angelegt Kuehlung auf 20 Grad --- b) korrigierte Klasse EScooter --- Fahrzeug ES-11 angelegt E-Scooter bereit ES-11 (35 km), Kosten/km: 0.03 E-Scooter ES-11 Fahrzeug RES-1 angelegt Lastenrad RES-1 bereit RES-1 (60 km) --- c) ein Objekt, zwei Typen --- Fahrzeug KT-03 angelegt Kuehlung auf 20 Grad KT-03 (220 km) / kuehlen(-18): true f1 instanceof Kuehlbar: false, f2 instanceof Kuehlbar: true
Versicherbar nur für versicherbare SendungsartenPostsendung abstrakt gekennzeichnet und ohne typ (1 P); gemeinsame Attribute, Konstruktor und Getter nur in der Oberklasse (1 P); drei abstrakte Methoden kursiv bzw. {abstract} (1 P); drei Unterklassen mit Vererbungspfeil (Dreieck an Postsendung) (2 P); überschreibende Methoden in allen Unterklassen (1 P); Konstruktoren der Unterklassen (1 P); Interface Versicherbar mit Methode (1 P); Realisierung nur durch Paket und Expresssendung, gestrichelt mit Dreieck (1 P).# statt - mit Gettern; istStationsfaehig() als normale Methode in Postsendung (Rückgabe false), die nur Paket überschreibt. Nicht richtig: berechneVersicherung zusätzlich in Postsendung – dann hätte auch Brief diese Methode, und das Interface wäre überflüssig.klasse Paket erbt von Postsendung implementiert Versicherbar
konstruktor Paket(sendungsNr : string, gewicht : double)
super(sendungsNr, gewicht)
Ende konstruktor
methode berechnePorto() : double
wenn getGewicht() <= 5.0 dann
rückgabe 5.49
Ende wenn
rückgabe 8.99
Ende methode
methode maxGewicht() : double
rückgabe 31.5
Ende methode
methode istStationsfaehig() : boolean
rückgabe true
Ende methode
methode berechneVersicherung(warenwert : double) : double
betrag : double := warenwert * 0.01
wenn betrag < 2.00 dann
betrag := 2.00
Ende wenn
rückgabe betrag
Ende methode
Ende klasse
super (1 P); berechnePorto mit Grenze „bis einschließlich 5 kg“ (1 P); maxGewicht und istStationsfaehig (1 P); berechneVersicherung mit Mindestbetrag 2,00 (1½ P). Gleichwertig: Zugriff auf ein protected-Attribut gewicht statt getGewicht().Java · Aufgabe_OOP_02_3.java (Klasse Paket)
class Paket extends Postsendung implements Versicherbar {
public Paket(String sendungsNr, double gewicht) {
super(sendungsNr, gewicht);
}
@Override
public double berechnePorto() {
if (getGewicht() <= 5.0) {
return 5.49;
}
return 8.99;
}
@Override
public double maxGewicht() {
return 31.5;
}
@Override
public boolean istStationsfaehig() {
return true;
}
@Override
public double berechneVersicherung(double warenwert) {
double betrag = warenwert * 0.01;
if (betrag < 2.00) {
betrag = 2.00;
}
return betrag;
}
}
Ausgabe des Testprogramms (alle Sendungsarten, polymorph über List<Postsendung>)
Nr Art Gewicht Porto max.kg Station B-1 Brief 0,02 0,95 1,0 nein B-2 Brief 0,40 1,80 1,0 nein P-1 Paket 3,00 5,49 31,5 ja P-2 Paket 12,00 8,99 31,5 ja E-1 Expresssendung 2,50 14,90 20,0 nein Versicherung (nur Objekte, die Versicherbar realisieren): P-1 Warenwert 150,00 -> Versicherung 2,00 P-2 Warenwert 600,00 -> Versicherung 6,00 E-1 Warenwert 400,00 -> Versicherung 2,00 Gegenprobe mit der bisherigen Klasse (Fallunterscheidung nach typ): E-1 alt: Porto 14,90, Versicherung(400) 2,00
Java erlaubt bei Klassen nur Einfachvererbung. Würde eine Klasse von zwei Klassen erben, die beide eine Methode mit gleicher Signatur implementieren oder gleichnamige Attribute haben, wäre nicht eindeutig, welche Implementierung bzw. welches Attribut gilt; erben beide Klassen außerdem von einer gemeinsamen Oberklasse, wären deren Attribute doppelt vorhanden (Diamond-Problem).
Ein Interface enthält dagegen keine Instanzattribute und (in der klassischen Form) nur Methodensignaturen ohne Implementierung. Paket implementiert berechneVersicherung selbst – es gibt genau eine Implementierung und damit keinen Konflikt. Deshalb darf eine Klasse eine Oberklasse haben (Postsendung, „ist ein“) und zusätzlich beliebig viele Interfaces realisieren (Versicherbar, „kann versichert werden“).
Polymorphie (Vielgestaltigkeit) bedeutet, dass eine Variable vom Typ einer Oberklasse oder eines Interfaces auf Objekte verschiedener Unterklassen verweisen kann. Derselbe Methodenaufruf führt je nach Objekt zu unterschiedlichem Verhalten, weil die Unterklassen die Methode überschreiben. Welche Implementierung ausgeführt wird, wird erst zur Laufzeit anhand der tatsächlichen Klasse des Objekts entschieden (dynamische bzw. späte Bindung). Beispiel: Zustellung z := new Expresszustellung() – der Aufruf z.gebuehr() führt die Methode von Expresszustellung aus.
| Nr. | Ergebnis | Begründung |
|---|---|---|
| 1 | Überladen | gleiche Klasse, gleicher Name, unterschiedliche Parameterliste (integer bzw. string) |
| 2 | Überschreiben | Unterklasse definiert die geerbte Methode mit gleicher Signatur neu und ersetzt ihre Implementierung |
| 3 | Überladen | andere Parameterliste (zusätzlicher Parameter). Die geerbte planeTour(km) bleibt im Kuehltransporter unverändert erhalten; es kommt nur eine weitere Variante hinzu. |
| Nr. | Ausgabe | Begründung |
|---|---|---|
| (1) | Express | b zeigt auf eine Expresszustellung → deren überschriebene beschreibung() (dynamische Bindung). |
| (2) | Express: 10.0 EUR | quittung() ist geerbt; darin werden beschreibung() und gebuehr() dynamisch gebunden → Express-Versionen; gebuehr() = super.gebuehr() + 6.00 = 4.00 + 6.00. |
| (3) | Nacht-Express: 10.0 EUR | Nachtzustellung überschreibt nur beschreibung(): "Nacht-" + super.beschreibung() (= "Express"). gebuehr() wird von Expresszustellung geerbt. |
| (4) | Zustellung: 4.0 EUR | reines Zustellung-Objekt |
| (5) | Standardbuchung | Überladen wird beim Übersetzen nach dem statischen Typ des Arguments ausgewählt: b ist als Zustellung deklariert → buche(Zustellung). |
| (6) | Expressbuchung | statischer Typ von d ist Expresszustellung → die spezifischere Variante buche(Expresszustellung). |
Ausgabe · Aufgabe_OOP_03_2.java
Express Express: 10.0 EUR Nacht-Express: 10.0 EUR Zustellung: 4.0 EUR Standardbuchung Expressbuchung
Der Compiler prüft anhand des statischen Typs, welche Methoden aufgerufen werden dürfen. b ist als Zustellung deklariert, und Zustellung hat keine Methode zeitfenster() – dass das Objekt zur Laufzeit eine Expresszustellung ist, spielt für den Compiler keine Rolle. Lösung: Typ prüfen und explizit umwandeln (Downcast):
if (b instanceof Expresszustellung) {
Expresszustellung e = (Expresszustellung) b;
System.out.println(e.zeitfenster());
}
instanceof und Cast. Ohne instanceof-Prüfung ½ P. Auch akzeptiert: Variable gleich als Expresszustellung deklarieren.Übersetzen: Der Compiler akzeptiert die Anweisung, weil Expresszustellung eine Unterklasse von Zustellung ist und ein Downcast grundsätzlich möglich ist. Ausführen: a verweist auf ein reines Zustellung-Objekt, das keine Expresszustellung ist. Die Umwandlung schlägt fehl, es wird eine ClassCastException ausgelöst (ohne Behandlung bricht das Programm ab). Vermeiden lässt sich das mit einer Prüfung durch instanceof.
ClassCastException: class Zustellung cannot be cast to class Expresszustellung (Zustellung and Expresszustellung are in unnamed module of loader 'app') a instanceof Expresszustellung: false
ClassCastException zur Laufzeit.Datei: java\08_OOP_Entwurfsmuster\Aufgabe_OOP_03_2.java – Die Datei enthält die Klassen aus Anlage 7, den Programmausschnitt der Aufgabe sowie b) und c) zum Ausprobieren.
Eine generische Klasse hat einen Typparameter (hier T), der als Platzhalter für den Datentyp der Elemente steht. Der konkrete Typ wird erst bei der Verwendung festgelegt, z. B. Ablage<Lagerposten> oder später Ablage<Rollcontainer>; die Klasse wird nur einmal programmiert.
Vorteil gegenüber Object: Typsicherheit beim Übersetzen – in eine Ablage<Lagerposten> lässt der Compiler nur Lagerposten hinein, beim Herauslesen ist kein Cast nötig, und Typfehler fallen nicht erst zur Laufzeit (ClassCastException) auf. (Auch akzeptiert: Wiederverwendbarkeit ohne Codekopie, bessere Lesbarkeit.)
konstruktor Ablage(bezeichnung : string, kapazitaet : integer)
this.bezeichnung := bezeichnung
this.kapazitaet := kapazitaet
elemente := new List<T>()
Ende konstruktor
methode ablegen(element : T) : boolean
wenn elemente.size() < kapazitaet und nicht elemente.contains(element) dann
elemente.add(element)
rückgabe true
Ende wenn
rückgabe false
Ende methode
< (1 P); Prüfung mit contains (½ P); add und beide Rückgaben (1 P). Typischer Fehler: Liste im Konstruktor nicht erzeugen – dann führt das erste ablegen zu einer NullPointerException.methode faelligePosten(ablage : Ablage<Lagerposten>, heute : Datum) : List<Lagerposten>
ergebnis : List<Lagerposten> := new List<Lagerposten>()
posten : List<Lagerposten> := ablage.getElemente()
zähle i von 0 bis posten.size() - 1
p : Lagerposten := posten.get(i)
wenn p.istFaellig(heute) dann
ergebnis.add(p)
Ende wenn
Ende zähle
rückgabe ergebnis
Ende methode
getElemente() mit richtigen Grenzen (1 P); Aufruf von istFaellig(heute) ohne Fallunterscheidung nach der Klasse (1 P); add und Rückgabe (1 P). Eine Lösung, die mit instanceof die drei Regeln selbst nachprogrammiert, verfehlt den Sinn der Aufgabe (höchstens 2 P).Testdaten: R-101 (Eingang 20.09. + 14 Tage = 04.10. ≤ 10.10.), Z-201 (freigegeben) und L-301 (Abholtermin = heute) sind fällig; R-102 erst ab 15.10., Z-202 nicht freigegeben, L-302 erst am 12.10.
Polymorphie durch dynamische (späte) Bindung. Lagerposten deklariert istFaellig als abstrakte Methode, und jede Unterklasse (Retoure, Zollposten, Lagerauftrag) überschreibt sie mit ihrer eigenen Regel. Die Variable p hat den statischen Typ Lagerposten; welche Implementierung ausgeführt wird, entscheidet die Laufzeitumgebung erst beim Aufruf anhand der tatsächlichen Klasse des Objekts (dynamischer Typ).
methode maximum(v : Vergleicher<T>) : T
wenn elemente.size() = 0 dann
rückgabe null
Ende wenn
max : T := elemente.get(0)
zähle i von 1 bis elemente.size() - 1
wenn v.vergleiche(elemente.get(i), max) > 0 dann
max := elemente.get(i)
Ende wenn
Ende zähle
rückgabe max
Ende methode
null (1 P); Startwert = erstes Element (1 P); Vergleich über v.vergleiche in richtiger Richtung (> 0 heißt „größer als das bisherige Maximum“) (1 P); Ersetzen und Rückgabe (1 P). Ein Startwert wie 0 oder null ist falsch, weil T ein beliebiger Typ ist und mit null nicht verglichen werden kann.Java · Aufgabe_OOP_03_3.java (Ablage<T>, Vergleicher<T>, Lagerverwaltung)
class Ablage<T> {
private String bezeichnung;
private int kapazitaet;
private List<T> elemente;
public Ablage(String bezeichnung, int kapazitaet) {
this.bezeichnung = bezeichnung;
this.kapazitaet = kapazitaet;
this.elemente = new ArrayList<>();
}
public boolean ablegen(T element) {
if (elemente.size() < kapazitaet && !elemente.contains(element)) {
elemente.add(element);
return true;
}
return false;
}
public int anzahl() {
return elemente.size();
}
public List<T> getElemente() {
return elemente;
}
public T maximum(Vergleicher<T> v) {
if (elemente.size() == 0) {
return null;
}
T max = elemente.get(0);
for (int i = 1; i < elemente.size(); i++) {
if (v.vergleiche(elemente.get(i), max) > 0) {
max = elemente.get(i);
}
}
return max;
}
}
interface Vergleicher<T> {
int vergleiche(T a, T b);
}
class Lagerverwaltung {
public List<Lagerposten> faelligePosten(Ablage<Lagerposten> ablage, Datum heute) {
List<Lagerposten> ergebnis = new ArrayList<>();
List<Lagerposten> posten = ablage.getElemente();
for (int i = 0; i < posten.size(); i++) {
Lagerposten p = posten.get(i);
if (p.istFaellig(heute)) { // dynamische Bindung
ergebnis.add(p);
}
}
return ergebnis;
}
}
Ausgabe des Testprogramms (Testdaten aus Anlage 8, heute = 10.10.2026)
ablegen(R-101) -> true ablegen(R-102) -> true ablegen(Z-201) -> true ablegen(Z-202) -> true ablegen(L-301) -> true ablegen(L-302) -> true ablegen(R-101) erneut -> false (schon enthalten) ablegen(R-103) -> false (Kapazität 6 erreicht) anzahl() = 6 c) faelligePosten(regalA, 10.10.2026): R-101 Z-201 L-301 e) maximum(GewichtVergleicher) = L-301 (18.0 kg) Ablage<String>.maximum(nach Länge) = "Zoll ruft zurück" maximum einer leeren Ablage = null
Die letzten Zeilen zeigen die Wiederverwendung: dieselbe Klasse als Ablage<String>, der Vergleicher als Lambda-Ausdruck, und null bei leerer Ablage.
| Entwurfsmuster | Kategorie |
|---|---|
| Singleton | Erzeugungsmuster |
| Fassade | Strukturmuster |
| Strategie | Verhaltensmuster |
| Fabrikmethode | Erzeugungsmuster |
Das Singleton-Muster stellt sicher, dass es von der Klasse Tarifkonfiguration genau ein Objekt gibt, und bietet über die statische Methode getInstanz() einen globalen Zugriffspunkt darauf. So wird die Datei tarife.properties nur einmal gelesen, und alle Programmteile verwenden garantiert dieselben Tarifwerte.
| Zeile | Fehler | Korrektur |
|---|---|---|
| 2 | instanz ist ein Instanzattribut – die einzige Instanz muss aber zur Klasse gehören, damit die statische Methode darauf zugreifen kann. | private static Tarifkonfiguration instanz = null; |
| 6 | Der Konstruktor ist public – jede Klasse kann mit new beliebig viele Objekte erzeugen. | private Tarifkonfiguration() { … } |
| 11 | getInstanz() ist nicht statisch – man bräuchte bereits ein Objekt, um an das Objekt zu kommen. | public static Tarifkonfiguration getInstanz() |
| 12 | Bei jedem Aufruf wird ein neues Objekt erzeugt und die Datei erneut gelesen. | if (instanz == null) { instanz = new Tarifkonfiguration(); } |
- instanz : Tarifkonfiguration {static}“ bzw. „wenn instanz = null dann instanz := new Tarifkonfiguration()“.Java · Aufgabe_OOP_04_2.java (korrigierte Klasse, threadsicher)
class Tarifkonfiguration {
private static Tarifkonfiguration instanz = null; // Fehler 1: static
private double grundpreisPaket;
private double kgZuschlag;
private Tarifkonfiguration() { // Fehler 2: private
grundpreisPaket = ladeWert("paket.grundpreis");
kgZuschlag = ladeWert("paket.kgzuschlag");
}
public static synchronized Tarifkonfiguration getInstanz() { // Fehler 3: static
if (instanz == null) { // Fehler 4: nur einmal erzeugen
instanz = new Tarifkonfiguration();
}
return instanz;
}
public double getGrundpreisPaket() {
return grundpreisPaket;
}
public double getKgZuschlag() {
return kgZuschlag;
}
// ladeWert(...) wie bisher
}
Problem: Rufen zwei Threads getInstanz() gleichzeitig zum ersten Mal auf, können beide die Bedingung instanz == null als erfüllt sehen, bevor einer von ihnen das neue Objekt zugewiesen hat (Race Condition). Dann werden zwei Objekte erzeugt, die Datei wird zweimal gelesen, und Programmteile arbeiten eventuell mit unterschiedlichen Instanzen – das Singleton ist verletzt.
Lösung (eine genügt): getInstanz() als synchronized deklarieren (nur ein Thread gleichzeitig in der Methode); oder die Instanz gleich beim Laden der Klasse erzeugen (private static final Tarifkonfiguration INSTANZ = new Tarifkonfiguration();); oder in Java ein enum mit genau einem Wert verwenden.
Ausgabe des Testprogramms (10 Threads rufen gleichzeitig getInstanz() auf)
t1 == t2: true Grundpreis Paket: 4,20 EUR, Zuschlag je kg: 0,35 EUR Verschiedene Instanzen bei 10 Threads: 1 Anzahl Ladevorgänge der Datei: 1
Zustellservice und andere Aufrufer kennen nur die abstrakte Klasse Benachrichtigung, nicht die konkreten Unterklassen. Sie bleiben bei Erweiterungen unverändert und sind leichter testbar.Benachrichtigung abstrakt mit # ziel : string und Konstruktor (1½ P); abstrakte Methoden senden und kostenProNachricht gekennzeichnet (1 P); statische Fabrikmethode mit beiden Parametern und Rückgabetyp Benachrichtigung, unterstrichen oder {static} (1½ P); drei Unterklassen mit Vererbungspfeil (1 P); Konstruktor und überschreibende Methoden in den Unterklassen (1 P); Zustellservice mit Methode und Beziehung zu Benachrichtigung (Abhängigkeit oder gerichtete Assoziation) (1 P). Zusätzliche Abhängigkeiten «create» von der Fabrikmethode zu den Unterklassen sind richtig, aber nicht gefordert.methode erzeuge(kanal : string, ziel : string) : Benachrichtigung {static}
wenn kanal = "EMAIL" dann
rückgabe new EmailBenachrichtigung(ziel)
sonst wenn kanal = "SMS" dann
rückgabe new SmsBenachrichtigung(ziel)
sonst wenn kanal = "PUSH" dann
rückgabe new PushBenachrichtigung(ziel)
Ende wenn
rückgabe null // unbekannter Kanal
Ende methode
// Verwendung im Zustellservice – keine Fallunterscheidung mehr:
methode informiereEmpfaenger(k : Kunde, text : string) : double
b : Benachrichtigung := Benachrichtigung.erzeuge(k.getKanal(), k.getKontakt())
wenn b = null dann
rückgabe 0.0
Ende wenn
b.senden(text)
rückgabe b.kostenProNachricht()
Ende methode
ziel, Rückgabe), 1 P für null bei unbekanntem Kanal. Die Methode informiereEmpfaenger ist nicht gefordert und dient nur der Veranschaulichung.Java · Aufgabe_OOP_04_3.java
abstract class Benachrichtigung {
protected String ziel;
public Benachrichtigung(String ziel) {
this.ziel = ziel;
}
public abstract boolean senden(String text);
public abstract double kostenProNachricht();
public static Benachrichtigung erzeuge(String kanal, String ziel) {
if (kanal.equals("EMAIL")) {
return new EmailBenachrichtigung(ziel);
} else if (kanal.equals("SMS")) {
return new SmsBenachrichtigung(ziel);
} else if (kanal.equals("PUSH")) {
return new PushBenachrichtigung(ziel);
}
return null;
}
}
class Zustellservice {
/** Liefert die Kosten der Benachrichtigung (0, wenn keine gesendet wurde). */
public double informiereEmpfaenger(Kunde k, String text) {
Benachrichtigung b = Benachrichtigung.erzeuge(k.getKanal(), k.getKontakt());
if (b == null) {
System.out.println(" Kanal " + k.getKanal() + " von " + k.getName() + " unbekannt - keine Nachricht");
return 0.0;
}
b.senden(text);
return b.kostenProNachricht();
}
}
Ausgabe (vier Kunden, der Kanal "FAX" ist unbekannt)
E-Mail an ahrens@example.org: 183 Zeichen SMS an +49 151 0000001: 160 Zeichen (gekürzt auf max. 160) Push an Gerät geraet-7f3a: 183 Zeichen Kanal FAX von Dahl unbekannt - keine Nachricht Kosten der Benachrichtigungen: 0,07 EUR
Ein Konstruktor mit allen Angaben hätte viele, überwiegend optionale Parameter: Aufrufe wie new Etikett("KT-1", null, 0.0, null, "Garage", null) sind unlesbar und fehleranfällig (Reihenfolge, viele null-Werte), oder man bräuchte sehr viele überladene Konstruktoren. Ein Builder setzt nur die benötigten Angaben Schritt für Schritt über sprechende Methoden (.ablageort("Garage").wunschtag(…)) und erzeugt mit build() ein vollständiges, geprüftes Etikett. Der Code wird lesbarer, flexibler, und die Prüfung der Angaben steht an einer Stelle.
| Nr. | Muster | Begründung |
|---|---|---|
| 1 | Adapter | Eine vorhandene, nicht änderbare Klasse (SpeditionApi) wird über eine Adapterklasse an die erwartete Schnittstelle Statusquelle angepasst; der Adapter ruft holeStatusXml auf und übersetzt das Ergebnis. |
| 2 | Fassade | Eine Klasse bietet eine einfache Methode einliefern(…) und koordiniert intern die fünf Teilsysteme. |
| 3 | Decorator | Jede Zusatzleistung umhüllt die Sendung, hat dieselbe Schnittstelle und erhöht den Preis; Kombinationen entstehen durch Verschachteln statt durch Unterklassen. |
Paketauftrag kennt nur das Interface; drei austauschbare Strategien+ preis(gewicht : double) : double im Interface (1 P); Realisierung durch alle drei Tarifklassen, gestrichelt mit Dreieck am Interface (2 P, je fehlender Realisierung −1 P); gerichtete Assoziation Paketauftrag → Preisstrategie (1 P; Aggregation oder ungerichtete Assoziation ebenfalls akzeptiert).// Klasse Privattarif
methode preis(gewicht : double) : double
wenn gewicht <= 2.0 dann
rückgabe 4.99
sonst wenn gewicht <= 10.0 dann
rückgabe 7.49
Ende wenn
rückgabe 11.99
Ende methode
// Klasse Paketauftrag
methode berechnePreis() : double
rückgabe strategie.preis(gewicht)
Ende methode
preis: Grenzen 2 und 10 kg jeweils einschließlich (2 P), drei richtige Rückgaben (1 P); berechnePreis: Delegation an das Strategie-Objekt ohne Fallunterscheidung (1 P).Java · Aufgabe_OOP_05_2.java
interface Preisstrategie {
double preis(double gewicht);
}
class Privattarif implements Preisstrategie {
@Override
public double preis(double gewicht) {
if (gewicht <= 2.0) {
return 4.99;
} else if (gewicht <= 10.0) {
return 7.49;
}
return 11.99;
}
}
class Geschaeftstarif implements Preisstrategie {
@Override
public double preis(double gewicht) {
return 3.20 + 0.25 * gewicht;
}
}
class Aktionstarif implements Preisstrategie {
@Override
public double preis(double gewicht) {
return 3.99;
}
}
class Paketauftrag {
private double gewicht;
private Preisstrategie strategie;
public Paketauftrag(double gewicht, Preisstrategie strategie) {
this.gewicht = gewicht;
this.strategie = strategie;
}
public void setStrategie(Preisstrategie strategie) {
this.strategie = strategie;
}
public double berechnePreis() {
return strategie.preis(gewicht);
}
}
Ausgabe (Preise je Tarif, Wechsel zur Laufzeit)
Gewicht Privattarif Geschaeftstarif Aktionstarif 1,5 kg 4,99 3,58 3,99 2,0 kg 4,99 3,70 3,99 6,0 kg 7,49 4,70 3,99 14,0 kg 11,99 6,70 3,99 Strategie zur Laufzeit wechseln: 6 kg, Privattarif: 7,49 EUR 6 kg, Aktionstarif: 3,99 EUR
Mit dem Strategie-Muster kann der Tarif eines bestehenden Auftrags zur Laufzeit gewechselt werden (setStrategie), z. B. wenn ein Kunde ein Geschäftskonto eröffnet. Bei Unterklassen müsste ein neues Objekt einer anderen Klasse erzeugt und alle Daten müssten übertragen werden. Außerdem bleiben die Tarife unabhängig von Paketauftrag: Neue Tarife erfordern keine Änderung am Auftrag, und es entsteht keine „Klassenexplosion“, wenn weitere Merkmale (z. B. Expressaufträge) mit allen Tarifen kombiniert werden müssen.
Funktionsweise: Ein Subject (hier Sendungsstatus) verwaltet eine Liste angemeldeter Observer. Interessierte Objekte (KundenApp, Hallenanzeige) melden sich mit addObserver an. Ändert sich der Zustand des Subjects, ruft es notifyObservers() auf; diese Methode ruft bei jedem angemeldeten Observer update() auf, und jeder Observer aktualisiert sich daraufhin selbst.
Begründung: Eine Statusänderung muss sofort und automatisch an mehrere, unterschiedliche Anzeigen weitergegeben werden. Weil das Subject nur das Interface Observer kennt, können später weitere Anzeigen (Kundenportal) ergänzt werden, ohne Sendungsstatus zu ändern – genau das fordert Anlage 12. Anzeigen müssen den Status nicht ständig abfragen (kein Polling).
quelle für die Pull-VarianteSendungsstatus → Subject (1 P); Realisierung KundenApp und Hallenanzeige → Observer (1 P); gerichtete Assoziation (oder Aggregation) Sendungsstatus → Observer mit * (1 P); Assoziation von KundenApp und Hallenanzeige zu Sendungsstatus (Attribut quelle) (1 P); ergänzte Methoden addObserver, removeObserver, notifyObservers in Sendungsstatus und update() in beiden Anzeigen (1 P).Sendungsstatus hängt nur vom Interface ab, nicht von den konkreten Anzeigeklassen. Änderungen an KundenApp oder Hallenanzeige wirken sich nicht auf Sendungsstatus aus.Observer und melden sich an – Sendungsstatus muss weder geändert noch neu getestet werden.List<Observer> kann beliebige, verschiedene Observer enthalten; sie lassen sich zur Laufzeit an- und abmelden. Sendungsstatus ist ohne die Anzeigen test- und wiederverwendbar.setStatus("In Zustellung") vom Client an status (1 P); Selbstaufruf notifyObservers() mit eigenem Aktivierungsbalken (1 P); update() an app (1 P); getStatus() von app an status mit Antwort (1 P); anzeigen() als Selbstaufruf von app (1 P); danach update() an halle (1 P); getStatus() mit Antwort und anzeigen() bei halle (1 P); Rückantworten von update und setStatus bis zum Client (1 P).loop-Fragment „[für alle Observer]“ um einen update-Aufruf, wenn erkennbar ist, dass beide Observer benachrichtigt werden; fehlende geschachtelte Aktivierungsbalken bei getStatus kosten keine Punkte.// Klasse Sendungsstatus
methode setStatus(status : string) : void
this.status := status
notifyObservers()
Ende methode
methode notifyObservers() : void
zähle i von 0 bis observers.size() - 1
observers.get(i).update()
Ende zähle
Ende methode
// Klasse KundenApp
methode update() : void
letzteMeldung := quelle.getStatus()
anzeigen()
Ende methode
setStatus: Status speichern und danach notifyObservers() aufrufen (1 P); notifyObservers: Schleife über alle Observer mit richtigen Grenzen (1 P) und Aufruf von update() (1 P); update: Status über quelle.getStatus() holen und anzeigen() aufrufen (1 P).Java · Aufgabe_OOP_05_3.java (Interfaces, Subject, Observer)
interface Subject {
void addObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}
interface Observer {
void update();
}
class Sendungsstatus implements Subject {
private String sendungsNr;
private String status;
private List<Observer> observers;
public Sendungsstatus(String sendungsNr) {
this.sendungsNr = sendungsNr;
this.status = "Angekündigt";
this.observers = new ArrayList<>();
}
public String getSendungsNr() {
return sendungsNr;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
notifyObservers();
}
@Override
public void addObserver(Observer o) {
observers.add(o);
}
@Override
public void removeObserver(Observer o) {
observers.remove(o);
}
@Override
public void notifyObservers() {
for (int i = 0; i < observers.size(); i++) {
observers.get(i).update();
}
}
}
class KundenApp implements Observer {
private Sendungsstatus quelle;
private String letzteMeldung;
public KundenApp(Sendungsstatus quelle) {
this.quelle = quelle;
quelle.addObserver(this);
}
@Override
public void update() {
letzteMeldung = quelle.getStatus(); // Pull: Daten selbst abholen
anzeigen();
}
public void anzeigen() {
System.out.println(" KundenApp: " + letzteMeldung);
}
public String getLetzteMeldung() {
return letzteMeldung;
}
}
class Hallenanzeige implements Observer {
private Sendungsstatus quelle;
private int hallenNr;
private String zeile;
public Hallenanzeige(Sendungsstatus quelle, int hallenNr) {
this.quelle = quelle;
this.hallenNr = hallenNr;
quelle.addObserver(this);
}
@Override
public void update() {
zeile = quelle.getStatus(); // Pull
anzeigen();
}
public void anzeigen() {
System.out.println(" Hallenanzeige " + hallenNr + ": " + zeile);
}
}
Ausgabe (zwei Statusänderungen, Abmelden der Hallenanzeige, dritte Änderung, Push-Vergleich)
KundenApp: In Zustellung Hallenanzeige 3: In Zustellung KundenApp: In Paketstation eingelegt Hallenanzeige 3: In Paketstation eingelegt -- Hallenanzeige meldet sich ab -- KundenApp: Abgeholt Letzte Meldung in der App: Abgeholt -- f) Push-Variante zum Vergleich -- Push-Observer erhält: KT-4712 -> Im Sortierzentrum
Bei der Push-Variante schickt das Subject die geänderten Daten gleich mit: Die Methode erhält Parameter, z. B. update(sendungsNr : string, status : string), und notifyObservers() ruft observers.get(i).update(sendungsNr, status) auf. Der Observer muss den Status nicht mehr mit getStatus() abholen (der Rückaufruf im Sequenzdiagramm entfällt) und braucht keine Referenz quelle mehr. Nachteil: Alle Observer erhalten alle Daten, und bei zusätzlichen Daten ändert sich die Schnittstelle.
getStatus() bzw. der Referenz auf das Subject.interface PushObserver {
void update(String sendungsNr, String status); // Daten werden mitgeschickt
}
class StatusQuellePush {
private String sendungsNr;
private String status;
private List<PushObserver> observers = new ArrayList<>();
public StatusQuellePush(String sendungsNr) {
this.sendungsNr = sendungsNr;
}
public void addObserver(PushObserver o) {
observers.add(o);
}
public void setStatus(String status) {
this.status = status;
for (int i = 0; i < observers.size(); i++) {
observers.get(i).update(sendungsNr, status);
}
}
}