MINT lernen

Übung — AFB III (Verallgemeinern und Reflektieren)

Zehn Aufgaben, bei denen du nicht nur programmierst, sondern Entscheidungen begründest — warum dieser Typ, diese Klasse, diese Liste?

Dein Fortschritt:
0 / 0 Aufgaben
3

Aufgabenblock — AFB III

Begründen statt nur ausführen: Behauptungen widerlegen, Entwürfe beurteilen, Klassen selbst planen und erweitern. Formuliere deine Antwort erst selbst in ganzen Sätzen oder als Code, bevor du die Musterlösung aufklappst — sie zählt dann als erledigt.

A1
Kompiliert heißt fehlerfrei? Mensaplan
AFB III

Tom behauptet: „Java prüft alles vorab mit javac. Ein Programm, das sich ohne Fehlermeldung übersetzen lässt, kann beim Ausführen nicht mehr abstürzen — anders als Python.“

Widerlege Toms Aussage mit einem kurzen, vollständigen Java-Programm zum Mensaplan der Woche. Ordne dann ein, was an seiner Aussage trotzdem stimmt.

Hinweis: Überlege, welche Informationen javac hat und welche erst beim Ausführen feststehen.

Strategie: Ein einziges Gegenbeispiel genügt. Suche einen Fehler, bei dem alle Typen stimmen, aber ein Wert „nicht passt“.Warum so? Der Compiler prüft Syntax und Typen, nicht den Ablauf mit echten Werten.
Lösungsskizze: Array mit 5 Tagen, Schleife mit <= → Index 5 → ArrayIndexOutOfBoundsException. Richtig an Toms Aussage: Typ- und Syntaxfehler findet Java früher als Python.
Musterlösung anzeigen (zählt als erledigt)
public class Mensaplan {
    public static void main(String[] args) {
        String[] tage = {"Mo", "Di", "Mi", "Do", "Fr"};
        for (int i = 0; i <= tage.length; i++) {
            System.out.println(tage[i]);
        }
    }
}

Musterlösung: Das Programm lässt sich ohne Meldung übersetzen, denn Syntax und Typen stimmen. Beim Ausführen gibt es zuerst Mo bis Fr aus und bricht dann ab: Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5. Welcher Index tatsächlich benutzt wird, steht erst zur Laufzeit fest — das kann javac nicht wissen. Toms Aussage ist also falsch.

Richtig ist: Java findet eine ganze Fehlergruppe schon vor dem Start, etwa int x = "drei"; oder ein fehlendes Semikolon. In Python fällt so etwas oft erst auf, wenn die betreffende Zeile läuft. Laufzeitfehler (Index zu groß, null-Zugriff) und Logikfehler bleiben aber in beiden Sprachen möglich — deshalb muss man auch in Java testen.

A2
Den passenden Typ wählen: Schulkiosk
AFB III

Die Schülerfirma baut ein Kassenprogramm für den Kiosk. Gespeichert werden sollen: der Preis eines Artikels, die Anzahl verkaufter Brezeln, ob auf eine Flasche Pfand kommt, der Artikelcode wie 0042 und die Größe eines Bechers (S, M oder L). Ein erster Test liefert Überraschungen:

double summe = 0.0;
summe = summe + 0.1;
summe = summe + 0.2;
System.out.println(summe);          // 0.30000000000000004
System.out.println(summe == 0.3);   // false
int code = 0042;
System.out.println(code);           // 34

Lege für jede der fünf Angaben einen Java-Typ fest und begründe deine Wahl mithilfe der Testausgabe.

Hinweis: Eine führende 0 hat bei int-Literalen in Java eine besondere Bedeutung — sie macht die Zahl zur Oktalzahl.

Strategie: Frage bei jeder Angabe: Wird damit gerechnet? Muss exakt verglichen werden? Kommt es auf jedes Zeichen an?Warum so? Der Typ legt fest, welche Werte und Rechnungen möglich sind — und der Compiler prüft ihn für das ganze Programm.
Lösungsskizze: Preis in Cent als int · Anzahl int · Pfand boolean · Code String · Größe char.
Musterlösung anzeigen (zählt als erledigt)

Preis: int preisCent (z. B. 70 statt 0,70). Die Testausgabe zeigt: double speichert 0,1 und 0,2 nur ungefähr, die Summe ist nicht exakt 0,3 und der Vergleich mit == scheitert. Bei Geld stört das spätestens beim Kassenabschluss. Mit ganzen Cent rechnet int exakt (10 + 20 == 30 ist true).

Anzahl Brezeln: int — es gibt nur ganze Brezeln. Pfand: boolean — nur ja oder nein. Größe: char — genau ein Zeichen ('S', 'M', 'L'); String ginge auch, erlaubt aber beliebig lange Texte.

Artikelcode: String code = "0042"; Mit dem Code wird nicht gerechnet, und die führenden Nullen gehören dazu. Als int gingen sie verloren — schlimmer noch: 0042 wird als Oktalzahl gelesen und ergibt 34.

A3
Trickaufgabe — Die Schleife ohne Ende
AFB III

Für ein Brettspiel soll eine Figur in Zweierschritten auf Feld 0 zurücklaufen:

for (int feld = 15; feld != 0; feld -= 2) {
    System.out.println("Figur auf Feld " + feld);
}

Das Programm hört nicht mehr auf. Beweise, dass die Schleife mit Startwert 15 nie endet, und verallgemeinere: Für welche Startwerte endet sie? Gib an, für wie viele Startwerte von 1 bis 30 die Schleife endet.

Strategie: Suche eine Eigenschaft von feld, die sich bei feld -= 2 nie ändert.Warum so? Bleibt eine Eigenschaft immer gleich, die 0 aber nicht hat, kann feld nie 0 werden — ganz ohne alle Durchläufe aufzuschreiben.
Lösungsskizze: ungerade − 2 = ungerade; 0 ist gerade → nie erreicht. Gerade Startwerte ≥ 0 erreichen 0. Von 1 bis 30 sind das 2, 4, …, 30.
Musterlösung anzeigen (zählt als erledigt)

Beweis: Der Startwert 15 ist ungerade. Zieht man von einer ungeraden Zahl 2 ab, ist das Ergebnis wieder ungerade. Also ist feld nach jedem Durchlauf ungerade: 15, 13, …, 1, −1, −3, … Die 0 ist gerade und wird daher nie angenommen. Die Bedingung feld != 0 bleibt immer wahr — die Schleife endet nie.

Verallgemeinerung: Die Schleife endet genau dann, wenn der Startwert gerade und nicht negativ ist; dann läuft sie Startwert / 2 Mal. Von 1 bis 30 sind das die 15 geraden Zahlen 2, 4, …, 30.

Der Trick: Die Schleife sieht aus wie ein harmloser Countdown. Robuster ist feld > 0 statt feld != 0 — dann endet sie für jeden Startwert. Übrigens gilt der Beweis sogar, wenn int irgendwann überläuft: Der Wertebereich hat eine gerade Anzahl von Zahlen, die Ungeradheit bleibt erhalten.

A4
Ausgeben oder zurückgeben? Rabattaktion
AFB III

Ein Sportgeschäft gibt Rabatt. Zwei Mitschüler haben dafür eine Methode geschrieben:

// Variante A
public static void zeigeRabatt(double preis) {
    System.out.println("Neuer Preis: " + (preis - preis * 25 / 100));
}

// Variante B
public static double rabattPreis(double preis, int prozent) {
    return preis - preis * prozent / 100;
}

Das Programm soll später auch die Summe eines Einkaufs mit mehreren rabattierten Artikeln berechnen, und im Sommerschlussverkauf gibt es 40 % statt 25 %.

Bewerte beide Varianten nach den Kriterien Wiederverwendbarkeit, Weiterrechnen und Änderbarkeit.

Strategie: Versuche mit jeder Variante, die Summe für eine 40-€-Jacke und ein 18-€-Shirt zu berechnen.Warum so? Ein konkreter Anwendungsfall zeigt sofort, welche Variante an ihre Grenzen stößt.
Lösungsskizze: A: void → kein Wert zum Weiterrechnen, 25 fest eingebaut. B: Rückgabe + Parameter → rabattPreis(40.0, 25) + rabattPreis(18.0, 10).
Musterlösung anzeigen (zählt als erledigt)

Weiterrechnen: Variante A gibt den Preis nur aus. Weil sie void ist, kommt kein Wert zurück — eine Summe ist unmöglich, double s = zeigeRabatt(40.0); übersetzt nicht. Variante B liefert den Preis mit return: rabattPreis(40.0, 25) + rabattPreis(18.0, 10) ergibt 30,0 + 16,2 = 46,2.

Änderbarkeit: In A ist 25 % fest eingebaut; für 40 % müsste man die Methode ändern oder kopieren. B bekommt den Prozentsatz als Parameter und deckt jede Aktion ab.

Wiederverwendbarkeit: B trennt Rechnen und Ausgeben: Das Ergebnis kann auf dem Bildschirm, auf dem Kassenbon oder in einer weiteren Rechnung landen. A ist nur für genau eine Ausgabe brauchbar.

Urteil: Variante B ist klar besser. A ist höchstens als kleine Hilfsmethode sinnvoll, die ihrerseits rabattPreis aufruft.

A5
Klasse entwerfen: Kletterhalle
AFB III

Eine Kletterhalle möchte ihre Routen verwalten: Jede Route hat einen Namen und einen Schwierigkeitsgrad von 1 bis 11. Die Halle will zählen, wie oft eine Route geklettert wurde. Routen können wegen Umbau gesperrt und wieder freigegeben werden; eine gesperrte Route kann nicht geklettert werden. Ab Grad 7 gilt eine Route als „schwer“.

Entwirf die Klasse Route als Klassenkarte und setze sie in Java um. Achte darauf, dass kein Objekt einen ungültigen Grad oder einen unsinnigen Zähler bekommen kann.

Strategie: Substantive im Text → Attribute, Verben → Methoden, Fragen („ist schwer?“) → Methoden mit Rückgabe boolean.Warum so? So entsteht der Entwurf systematisch aus der Anforderung und du vergisst keine Eigenschaft.
Lösungsskizze: Attribute name, grad, begehungen, gesperrt (alle privat). Grad nur über einen prüfenden Setter; begehungen ohne Setter, nur begehen() erhöht ihn; begehen() meldet mit boolean, ob es geklappt hat.
Musterlösung anzeigen (zählt als erledigt)
Route
  • - name: Zeichenkette
  • - grad: Ganzzahl
  • - begehungen: Ganzzahl
  • - gesperrt: Wahrheitswert
  • c Route(name: Zeichenkette, grad: Ganzzahl)
  • + setGrad(grad: Ganzzahl)
  • + getGrad(): Ganzzahl
  • + begehen(): Wahrheitswert
  • + sperren()
  • + freigeben()
  • + getBegehungen(): Ganzzahl
  • + istSchwer(): Wahrheitswert
public class Route {
    private String name;
    private int grad;
    private int begehungen;
    private boolean gesperrt;

    public Route(String name, int grad) {
        this.name = name;
        setGrad(grad);
        begehungen = 0;
        gesperrt = false;
    }

    public void setGrad(int grad) {
        if (grad >= 1 && grad <= 11) {
            this.grad = grad;
        }
    }

    public int getGrad() {
        return grad;
    }

    public boolean begehen() {
        if (gesperrt) {
            return false;
        }
        begehungen = begehungen + 1;
        return true;
    }

    public void sperren() {
        gesperrt = true;
    }

    public void freigeben() {
        gesperrt = false;
    }

    public int getBegehungen() {
        return begehungen;
    }

    public boolean istSchwer() {
        return grad >= 7;
    }
}

Begründung: Alle Attribute sind privat. Der Konstruktor nutzt den Setter, damit auch schon beim Erzeugen die Grenzen gelten (bei ungültigem Grad bleibt 0 stehen — eine Variante wäre ein Ersatzwert wie 1). Für begehungen gibt es bewusst keinen Setter: Der Zähler kann nur durch echtes Klettern wachsen. „Schwer“ wird aus grad berechnet statt gespeichert — so können die beiden Angaben nie widersprüchlich werden.

A6
Wo ist der Standardkonstruktor? Spielstand
AFB III

Für ein Brettspiel-Turnier speichert die Klasse Spielstand Spieler und Punkte:

public class Spielstand {
    private String spieler;
    private int punkte;

    public Spielstand(String spieler) {
        this.spieler = spieler;
    }
}

Emma will für Gäste ohne Namen new Spielstand() schreiben und sagt: „Einen Konstruktor ohne Parameter brauche ich nicht zu schreiben. Den legt Java doch automatisch an — deshalb funktionierte new bei meiner Klasse ohne Konstruktor ja auch.“

Nimm Stellung zu Emmas Aussage und schlage eine Lösung für die Gäste vor.

Strategie: Trenne zwei Fälle: eine Klasse ganz ohne eigenen Konstruktor und eine Klasse mit mindestens einem eigenen Konstruktor.Warum so? Emmas Beobachtung stimmt für den ersten Fall — die Frage ist, ob sie auf den zweiten übertragbar ist.
Lösungsskizze: Standardkonstruktor nur, wenn es keinen eigenen gibt → Übersetzungsfehler. Abhilfe: Konstruktor überladen, z. B. Spielstand() mit spieler = "Gast".
Musterlösung anzeigen (zählt als erledigt)

Stellungnahme: Emmas Aussage stimmt nur teilweise. Java legt den parameterlosen Standardkonstruktor nur an, wenn die Klasse überhaupt keinen eigenen Konstruktor hat. Da Spielstand(String spieler) existiert, gibt es keinen Standardkonstruktor, und new Spielstand() wird nicht übersetzt:

error: constructor Spielstand in class Spielstand cannot be applied to given types;
  required: String
  found:    no arguments

Das ist sinnvoll: Wer einen Konstruktor mit Parameter schreibt, sagt damit „ohne Namen soll kein Spielstand entstehen“. Java soll diese Absicht nicht heimlich umgehen.

Lösung: Den Konstruktor überladen:

public Spielstand() {
    spieler = "Gast";
    punkte = 0;
}

Damit gibt es beide Wege, und Gäste haben einen lesbaren Namen statt null.

Spielstand
  • - spieler: Zeichenkette
  • - punkte: Ganzzahl
  • c Spielstand(spieler: Zeichenkette)
Bisheriger Entwurf — nur ein Konstruktor
A7
Drei Entwürfe vergleichen: Bonuskarte im Café
AFB III

Ein Café vergibt pro Getränk einen Stempel. Bei 10 Stempeln gibt es ein Gratisgetränk, danach beginnt die Karte wieder bei 0. Drei Entwürfe liegen vor:

  1. public int stempel; — sonst nichts.
  2. private int stempel; mit getStempel() und setStempel(int stempel) ohne Prüfung.
  3. private int stempel; mit getStempel(), stempeln() (höchstens bis 10) und einloesen(): boolean (nur bei genau 10, setzt dann auf 0).

Beurteile die drei Entwürfe. Gehe darauf ein, ob Getter und Setter allein schon „Kapselung“ bedeuten.

Strategie: Versuche bei jedem Entwurf zu schummeln: Kann man von außen 99 Stempel oder −3 Stempel eintragen?Warum so? Kapselung ist nur so gut wie der Schutz, den sie gegen ungültige Zustände bietet.
Lösungsskizze: A: gar kein Schutz. B: private, aber der Setter lässt alles durch → Schutz nur zum Schein. C: Methoden bilden die echten Regeln des Cafés ab → beste Lösung.
Musterlösung anzeigen (zählt als erledigt)

Entwurf A bietet keinen Schutz: karte.stempel = 99; ist erlaubt. Jede Stelle im Programm müsste die Café-Regeln selbst kennen.

Entwurf B wirkt gekapselt, ist es aber kaum: karte.setStempel(99); oder setStempel(-3) gehen genauso durch. Getter und Setter allein sind noch keine Kapselung — entscheidend ist, dass die Methoden die Regeln durchsetzen.

Entwurf C bildet die Regeln des Cafés ab: Stempel wachsen nur um 1, höchstens bis 10; das Einlösen klappt nur bei voller Karte und setzt sie zurück. Einen ungültigen Zustand kann man von außen nicht herstellen.

public void stempeln() {
    if (stempel < 10) {
        stempel = stempel + 1;
    }
}

public boolean einloesen() {
    if (stempel == 10) {
        stempel = 0;
        return true;
    }
    return false;
}

Urteil: C ist die beste Lösung, B nur scheinbar besser als A.

A8
Array oder ArrayList? Schwimmkurs
AFB III

Ein Schwimmverein plant die Anmeldung für einen Kurs: Der Kurs hat genau 12 Bahnenplätze, die fest nummeriert sind (Platz 0 bis 11). Wer keinen Platz bekommt, landet auf einer Warteliste, deren Länge niemand vorher kennt. Wird ein Platz frei, rückt die erste Person der Warteliste nach. Für eine Statistik soll außerdem das Alter aller Angemeldeten gespeichert werden. Ein erster Versuch lautet ArrayList<int> alter = new ArrayList<>();

Diskutiere für die Kursplätze, die Warteliste und die Altersangaben, ob ein Array oder eine ArrayList besser passt, und komm zu einem begründeten Ergebnis.

Strategie: Frage jeweils: Steht die Anzahl fest? Wird oft vorne entfernt oder hinten angefügt? Haben die Positionen eine feste Bedeutung?Warum so? Genau darin unterscheiden sich Array (feste Länge, feste Plätze) und ArrayList (wächst und schrumpft, rückt nach).
Lösungsskizze: Plätze → Array mit 12 Fächern (null = frei). Warteliste → ArrayList (add, remove(0)). Alter → ArrayList<Integer>, denn int ist als Typparameter nicht erlaubt.
Musterlösung anzeigen (zählt als erledigt)

Kursplätze: Für ein Array spricht, dass es genau 12 Plätze mit fester Nummer gibt: String[] plaetze = new String[12]; Ein freier Platz ist null, und niemand „rutscht“ auf einen anderen Platz, wenn jemand abspringt. Eine ArrayList würde beim Entfernen nachrücken — die Platznummern würden sich ändern. → Array.

Warteliste: Die Länge ist unbekannt und ändert sich ständig; neue Personen kommen hinten dazu (add), die erste rückt nach (get(0), remove(0)). Mit einem Array müsste man die Größe raten und alles von Hand verschieben. → ArrayList<String>.

Altersangaben: Die Anzahl wächst mit jeder Anmeldung, also ArrayList. Der erste Versuch übersetzt aber nicht: error: unexpected type … required: reference, found: int. Richtig ist ArrayList<Integer>; Java wandelt int beim add automatisch um.

Ergebnis: Es gibt nicht „die“ bessere Struktur. Feste Anzahl mit fester Bedeutung der Plätze → Array; veränderliche Anzahl → ArrayList.

A9
Ist-ein oder hat-ein? Zoo-App
AFB III

Für eine Zoo-App schlägt eine Gruppe drei Vererbungen vor. Tier hat die Methoden beschreibung() und kannFliegen().

  1. public class Vogel extends Tier
  2. public class Pinguin extends Vogel — mit überschriebenem kannFliegen(), das false liefert
  3. public class Gehege extends Tier — „weil im Gehege ja Tiere leben“

Außerdem meint jemand: „@Override können wir weglassen, das ist nur Deko.“ Beim Testen stand in Pinguin versehentlich public String beschreibug().

Erörtere die drei Vorschläge und die Aussage zu @Override.

Strategie: Bilde bei jeder Vererbung den Satz „Ein … ist ein …“. Klingt er falsch, passt eher eine Beziehung „hat“ (Attribut bzw. Liste).Warum so? Vererbung bedeutet, dass die Unterklasse alles von der Oberklasse erbt — auch Methoden, die gar nicht passen.
Lösungsskizze: 1 ✓ · 2 ✓, aber Überschreiben nötig · 3 ✗ → Gehege hat eine ArrayList<Tier>. @Override macht aus dem Tippfehler einen Compilerfehler.
Musterlösung anzeigen (zählt als erledigt)

1. Vogel extends Tier: „Ein Vogel ist ein Tier“ stimmt. Vogel erbt beschreibung() und kannFliegen() und kann Eigenes ergänzen. Sinnvoll.

2. Pinguin extends Vogel: „Ein Pinguin ist ein Vogel“ stimmt biologisch. Nachteil: Er erbt ein kannFliegen(), das für Vögel true liefert; darum muss er es überschreiben. Das ist genau der Zweck des Überschreibens — vertretbar.

3. Gehege extends Tier: „Ein Gehege ist ein Tier“ ist falsch. Das Gehege würde kannFliegen() erben, bekäme aber keine Möglichkeit, mehrere Tiere zu speichern. Richtig ist eine Beziehung „hat“: private ArrayList<Tier> tiere; in Gehege. Abgelehnt.

@Override: Für das Weglassen spricht nur, dass der Code auch ohne läuft. Dagegen spricht der Test: Ohne Annotation wäre beschreibug() einfach eine neue Methode, und der Pinguin würde still die Beschreibung von Tier liefern. Mit @Override meldet javac sofort method does not override or implement a method from a supertype. Die Annotation ist also eine geprüfte Absichtserklärung — beibehalten.

A10
Projekt erweitern: Spieleverleih der SV
AFB III

Die SV verleiht in der Pause Brettspiele. Die Klasse Spieleverleih kann schon Spiele aufnehmen, suchen und ausleihen:

public Brettspiel suche(String titel) {
    for (Brettspiel b : spiele) {
        if (b.getTitel().equals(titel)) {
            return b;
        }
    }
    return null;
}

public boolean ausleihen(String titel) {
    Brettspiel b = suche(titel);
    if (b == null || b.istVerliehen()) {
        return false;
    }
    b.setVerliehen(true);
    return true;
}

Erweitere die Klasse um zurueckgeben(titel): Wahrheitswert und anzahlVerliehen(): Ganzzahl. Lege außerdem mindestens vier Testfälle mit erwartetem Ergebnis fest, die auch Sonderfälle abdecken.

Strategie: Baue zurueckgeben spiegelbildlich zu ausleihen und nutze suche wieder. Für die Testfälle: Was kann alles schiefgehen?Warum so? Vorhandene, getestete Methoden wiederzuverwenden spart Fehler — und Testfälle für Sonderfälle finden genau die Stellen, an denen neue Methoden brechen.
Lösungsskizze: false, wenn das Spiel fehlt oder gar nicht verliehen ist; sonst zurücksetzen und true. anzahlVerliehen zählt mit for-each. Tests: nicht verliehenes Spiel zurückgeben, ausleihen und zurückgeben, zweimal zurückgeben, unbekannter Titel.
Musterlösung anzeigen (zählt als erledigt)
public boolean zurueckgeben(String titel) {
    Brettspiel b = suche(titel);
    if (b == null || !b.istVerliehen()) {
        return false;
    }
    b.setVerliehen(false);
    return true;
}

public int anzahlVerliehen() {
    int anzahl = 0;
    for (Brettspiel b : spiele) {
        if (b.istVerliehen()) {
            anzahl++;
        }
    }
    return anzahl;
}

Testfälle (Bestand: „Carcassonne“, „Azul“, beide im Regal):

  1. zurueckgeben("Azul") → false (war gar nicht verliehen)
  2. ausleihen("Azul") → true, danach anzahlVerliehen() → 1
  3. zurueckgeben("Azul") → true; ein zweites Mal → false
  4. zurueckgeben("Monopoly") → false (unbekannter Titel, kein Absturz); anzahlVerliehen() → 0

Die Null-Prüfung verhindert eine NullPointerException bei unbekannten Titeln. Die Rückgabe boolean erlaubt es der Oberfläche, eine passende Meldung anzuzeigen. Alle vier Testfälle liefern mit dem Code oben genau die erwarteten Werte.

Spieleverleih
  • - spiele: Liste vom Typ Brettspiel
  • c Spieleverleih()
  • + hinzufuegen(b: Brettspiel)
  • + suche(titel: Zeichenkette): Brettspiel
  • + ausleihen(titel: Zeichenkette): Wahrheitswert
Brettspiel
  • - titel: Zeichenkette
  • - verliehen: Wahrheitswert
  • c Brettspiel(titel: Zeichenkette)
  • + getTitel(): Zeichenkette
  • + istVerliehen(): Wahrheitswert
  • + setVerliehen(verliehen: Wahrheitswert)
Bisheriger Stand: ein Spieleverleih hat viele Brettspiele