MINT lernen

Übung — AFB II (Zusammenhänge herstellen)

Zehn Aufgaben, bei denen du Java-Programme im Kopf ablaufen lässt, Fehler aufspürst und Klassen selbst schreibst.

Dein Fortschritt:
0 / 0 Aufgaben
2

Aufgabenblock — AFB II

Zehn Aufgaben aus dem ganzen Kapitel: Fehlermeldungen einordnen, Schleifen und Methodenaufrufe verfolgen, aus Klassenkarten Code machen und eine Suche in der Liste bauen — immer in mehreren Schritten, mit drei gestuften Tipps, wenn du nicht weiterkommst.

A1
Compiler oder Laufzeit? Münzsammlung
AFB II

Lina schreibt ein Programm für ihre Münzsammlung. Sie probiert sechs Zeilen aus — jede einzeln in main von Muenzsammlung.java:

  1. int anzahl = 12.5;
  2. boolean selten = "ja";
  3. char land = "M";
  4. double wert = 7 / 2;
  5. String s = "Münzen: " + 5 + 3;
  6. int[] kiste = new int[3]; kiste[3] = 5;

Analysiere die sechs Zeilen: Welche meldet schon javac, welche bricht erst beim Ausführen ab, welche laufen ohne Fehlermeldung — und mit welchem Wert?

Ansatz: Der Compiler prüft Typen: Passt der Wert rechts zum Typ links? Was er nicht wissen kann, sind Werte, die erst beim Ausführen entstehen — etwa welcher Index wirklich benutzt wird.
Typen: 'M' ist ein char, "M" ein String. 12.5 passt nicht verlustfrei in int. 7 / 2 ist erst eine Ganzzahldivision, dann wird erweitert.
Lösung: Compilerfehler in 1, 2, 3; Laufzeitfehler in 6; Zeile 4 ergibt 3.0, Zeile 5 den Text „Münzen: 53“.
Vollständige Lösung

Compilerfehler (3) — das Programm startet gar nicht:

1: error: incompatible types: possible lossy conversion from double to int
2: error: incompatible types: String cannot be converted to boolean
3: error: incompatible types: String cannot be converted to char

Laufzeitfehler (1) — Zeile 6 wird übersetzt, bricht aber beim Ausführen ab: Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3 (erlaubt sind nur 0 bis 2).

Ohne Meldung laufen 4 und 5 — trotzdem liefern sie vermutlich nicht, was Lina wollte: wert = 3.0 (Ganzzahldivision) und s = „Münzen: 53“, weil von links nach rechts erst "Münzen: " + 5 zu einem Text wird und dann die 3 angehängt wird. Solche Logikfehler findet weder javac noch die JVM.

A2
while-Schleife verfolgen: Sparschwein
AFB II

Jonas spart für Kopfhörer für 100 €. Jede Woche legt er 10 € ins Sparschwein, jede vierte Woche gibt es von der Oma 15 € dazu.

int guthaben = 0;
int woche = 0;
while (guthaben < 100) {
    woche++;
    if (woche % 4 == 0) {
        guthaben += 25;
    } else {
        guthaben += 10;
    }
}

Ermittle mit einer Tracetabelle, nach wie vielen Wochen die Schleife endet und wie viel Geld dann im Sparschwein ist.

€
Ansatz: Tabelle mit den Spalten woche, woche % 4, guthaben. Die Bedingung wird immer vor einem Durchlauf geprüft.
Zwischenstand: Nach Woche 4 sind es 10 + 10 + 10 + 25 = 55 €. Wie geht es in Woche 5 bis 8 weiter?
Lösung: Nach Woche 7 sind es 85 € (< 100), also noch ein Durchlauf: Woche 8 ist wieder eine vierte Woche.
Vollständige Lösung

Woche 1: 10 · 2: 20 · 3: 30 · 4: 55 · 5: 65 · 6: 75 · 7: 85 · 8: 85 + 25 = 110. Jetzt ist 110 < 100 falsch → Ende.

Die Schleife endet nach 8 Wochen mit 110 €. Hier passt while besser als for, weil die Anzahl der Durchläufe vorher nicht feststeht.

A3
Von Python nach Java: Paketversand
AFB II

Ein Versandshop berechnet das Porto: bis 1000 g 5 €, danach für jede angefangenen 500 g 2 € mehr. Mia hat die Python-Funktion nach Java übertragen:

def kosten(gramm):
    if gramm <= 1000:
        return 5
    zusatz = (gramm - 1000 + 499) // 500
    return 5 + zusatz * 2
public static kosten(int gramm) {
    if (gramm <= 1000) {
        return 5;
    }
    zusatz = (gramm - 1000 + 499) / 500;
    return 5 + zusatz * 2
}

Überprüfe Mias Übersetzung: Finde alle Fehler, verbessere sie und berechne mit der korrigierten Methode das Porto für 2300 g und für 3000 g.

€
€
Ansatz: Was verlangt Java, was Python nicht braucht? Denk an Typen (bei Methoden und Variablen) und an das Zeilenende.
Rechenweg: // in Python entspricht bei int-Werten dem / in Java. Für 2300 g: (2300 − 1000 + 499) / 500 = 1799 / 500.
Lösung: Drei Fehler: Rückgabetyp int fehlt, int vor zusatz fehlt, Semikolon am Ende fehlt. 1799 / 500 = 3 → 5 + 6 = 11 €.
Vollständige Lösung
public static int kosten(int gramm) {
    if (gramm <= 1000) {
        return 5;
    }
    int zusatz = (gramm - 1000 + 499) / 500;
    return 5 + zusatz * 2;
}

Beim ersten javac-Lauf erscheinen nur zwei Meldungen: invalid method declaration; return type required und ';' expected. Erst wenn diese behoben sind, meldet der Compiler cannot find symbol … variable zusatz — Syntaxfehler werden vor den Typfehlern gefunden.

2300 g: 1799 / 500 = 3 angefangene Blöcke → 5 + 3 · 2 = 11 €. 3000 g: 2499 / 500 = 4 → 5 + 8 = 13 €. Das „+ 499“ sorgt fürs Aufrunden: 1001 g kostet schon 7 €.

A4
Trickaufgabe — Kanuverleih mit Lücke
AFB II

Ein Kanuverleih am See zeigt an, wie viele Sitzplätze gerade frei sind.

public class Kanu {
    private String name;
    private int plaetze;
    private boolean verliehen;

    public Kanu(String name, int plaetze) {
        this.name = name;
        plaetze = plaetze;
    }

    public Kanu(String name) {
        this.name = name;
        this.plaetze = 1;
    }

    public void verleihen() {
        verliehen = true;
    }

    public int freiePlaetze() {
        if (verliehen) {
            return 0;
        }
        return plaetze;
    }
}
Kanu a = new Kanu("Otter", 3);
Kanu b = new Kanu("Biber");
Kanu c = new Kanu("Hecht", 2);
c.verleihen();
int frei = a.freiePlaetze() + b.freiePlaetze() + c.freiePlaetze();

Der Programmierer sagt: „Es sind 4 Plätze frei.“ Untersuche Zeile für Zeile, welche Werte die Objekte wirklich haben.

Ansatz: Lege für jedes Objekt eine Objektkarte an und starte mit den Standardwerten (0, false, null). Führe dann den passenden Konstruktor aus.
Genau hinsehen: Vergleiche die zweite Zeile beider Konstruktoren. Worauf bezieht sich plaetze, wenn ein Parameter genauso heißt wie das Attribut?
Lösung: plaetze = plaetze; weist den Parameter sich selbst zu. Das Attribut von a bleibt 0.
Vollständige Lösung
a: Kanu
  • name = "Otter"
  • plaetze = 0
  • verliehen = false
b: Kanu
  • name = "Biber"
  • plaetze = 1
  • verliehen = false
c: Kanu
  • name = "Hecht"
  • plaetze = 0
  • verliehen = true

Im Konstruktor mit zwei Parametern verdeckt der Parameter plaetze das Attribut. plaetze = plaetze; ändert also nur den Parameter — das Attribut behält den Standardwert 0. javac meldet dabei keinen Fehler.

a.freiePlaetze() = 0, b liefert 1 (hier steht this.), c ist verliehen → 0. frei = 0 + 1 + 0 = 1. Der Programmierer hat mit 3 + 1 = 4 gerechnet — richtig wäre das nur mit this.plaetze = plaetze;.

Kanu
  • - name: Zeichenkette
  • - plaetze: Ganzzahl
  • - verliehen: Wahrheitswert
  • c Kanu(name: Zeichenkette, plaetze: Ganzzahl)
  • c Kanu(name: Zeichenkette)
  • + verleihen()
  • + freiePlaetze(): Ganzzahl
Klassenkarte mit überladenem Konstruktor
A5
Vom Diagramm zum Code: Sponsorenlauf
AFB II

Beim Sponsorenlauf der Schule zahlen Sponsoren für jede gelaufene Runde einen festen Betrag. Die SV hat die Klasse Laeufer geplant (siehe Karte).

Erstelle aus der Klassenkarte den vollständigen Java-Quelltext. Zu Beginn ist runden 0; spende() liefert Runden mal Betrag pro Runde. Wie viel bringt Ida mit 2,50 € pro Runde nach 13 Runden ein?

€
Ansatz: Übersetze Zeile für Zeile: - runden: Ganzzahl → private int runden;, Fließkommazahl → double. Methoden ohne Rückgabetyp in der Karte bekommen void.
Konstruktor: Kein Rückgabetyp, Name = Klassenname. Nur name und euroProRunde kommen als Parameter — runden setzt du selbst auf 0.
Lösung: return runden * euroProRunde; — für Ida 13 · 2,5 = 32,5.
Vollständige Lösung
public class Laeufer {
    private String name;
    private int runden;
    private double euroProRunde;

    public Laeufer(String name, double euroProRunde) {
        this.name = name;
        this.euroProRunde = euroProRunde;
        runden = 0;
    }

    public void rundeLaufen() {
        runden = runden + 1;
    }

    public int getRunden() {
        return runden;
    }

    public double spende() {
        return runden * euroProRunde;
    }
}

Test: Laeufer ida = new Laeufer("Ida", 2.5);, 13-mal ida.rundeLaufen(); → ida.spende() = 32.5, also 32,50 €. runden * euroProRunde ist int * double; der int wird automatisch erweitert.

Laeufer
  • - name: Zeichenkette
  • - runden: Ganzzahl
  • - euroProRunde: Fließkommazahl
  • c Laeufer(name: Zeichenkette, euroProRunde: Fließkommazahl)
  • + rundeLaufen()
  • + getRunden(): Ganzzahl
  • + spende(): Fließkommazahl
Entwurf der SV
A6
Warum private? Schrittziel der Smartwatch
AFB II

Eine Smartwatch speichert ein Tagesziel für Schritte. Erlaubt sind nur Werte von 1000 bis 30000 in 500er-Schritten. Start ist 6000.

public void setZiel(int ziel) {
    if (ziel >= 1000 && ziel <= 30000 && ziel % 500 == 0) {
        this.ziel = ziel;
    }
}

public void erhoehen() {
    setZiel(ziel + 2500);
}

Version A (private int ziel): s.setZiel(8000); s.setZiel(12750); s.setZiel(500); s.erhoehen(); s.setZiel(31000);
Version B (public int ziel, ohne Setter): s.ziel = 8000; s.ziel = 12750; s.ziel = 500; s.ziel += 2500; s.ziel = 31000;

Erläutere an beiden Abläufen, wie Kapselung ungültige Zustände verhindert. Gib jeweils den Endwert von ziel an.

Schritte
Schritte
Ansatz: Prüfe bei Version A jeden Wert gegen alle drei Bedingungen. Bei Version B gibt es keine Prüfung — jede Zuweisung gilt.
Zwischenschritte A: 8000 ✓ · 12750 ✗ (kein Vielfaches von 500) · 500 ✗ (zu klein) · erhoehen() will 8000 + 2500 · 31000 ✗.
Lösung: A endet bei 10500, B beim letzten zugewiesenen Wert 31000 — zwischendurch standen dort auch 12750 und 500.
Vollständige Lösung

Version A: 6000 → 8000 → (12750 abgewiesen) → (500 abgewiesen) → 10500 → (31000 abgewiesen) = 10500. Weil ziel privat ist, führt jeder Weg zum Attribut durch den Setter — auch erhoehen() nutzt ihn. Ein ungültiger Zustand kann nie entstehen.

Version B: Jede Zuweisung wirkt direkt: 8000 → 12750 → 500 → 3000 → 31000. Das Objekt war dreimal in einem ungültigen Zustand; welche Regeln gelten, müsste jede Stelle im Programm selbst wissen.

Fazit: Mit private und Setter steht die Regel an einer Stelle in der Klasse. Wer von außen s.ziel = 500; schreibt, bekommt schon von javac die Meldung ziel has private access in Schrittziel.

A7
Array auswerten: Kopfrechen-Wettbewerb
AFB II

Beim Kopfrechen-Wettbewerb der 6. Klassen stehen die Punkte der Teams im Array punkte. Das Programm soll das beste Team finden und zählen, wie viele Teams mindestens 10 Punkte haben.

int[] punkte = {12, 7, 15, 9, 15, 4};
int best = 0;
int anzahl = 0;
for (int i = 1; i < punkte.length; i++) {
    if (punkte[i] > punkte[best]) {
        best = i;
    }
    if (punkte[i] >= 10) {
        anzahl++;
    }
}

Werte das Programm aus: Welche Werte haben best und anzahl am Ende, und tut das Programm, was es soll?

Ansatz: best ist ein Index, kein Punktwert. Verfolge i, punkte[i], punkte[best] und anzahl in einer Tabelle.
Genau lesen: Bei welchem Index startet die Schleife? Und ist 15 > 15?
Lösung: best wird nur bei i = 2 geändert. Gezählt werden nur 15 (i = 2) und 15 (i = 4) — die 12 an Index 0 fehlt.
Vollständige Lösung

i = 1: 7 — nichts · i = 2: 15 > 12 → best = 2, anzahl = 1 · i = 3: 9 — nichts · i = 4: 15 > 15 ist falsch, anzahl = 2 · i = 5: 4 — nichts.

best = 2 (bei Gleichstand gewinnt das erste Team mit 15 Punkten), anzahl = 2.

Für best ist der Start bei i = 1 in Ordnung, weil Index 0 schon als Startwert dient. Für anzahl ist er ein Fehler: Team 0 hat 12 Punkte und wird nicht gezählt — richtig wären 3. Abhilfe: anzahl in einer eigenen Schleife ab i = 0 zählen oder vor der Schleife punkte[0] prüfen.

A8
Überschreiben mit super: Saftbar
AFB II

Die Saftbar im Freizeitbad rechnet in Cent. Smoothies kosten mit Extra-Obst 80 ct mehr.

public class Getraenk {
    private String name;
    private int preisCent;

    public Getraenk(String name, int preisCent) {
        this.name = name;
        this.preisCent = preisCent;
    }

    public int getPreisCent() {
        return preisCent;
    }

    public String beschreibung() {
        return name + ": " + getPreisCent() + " ct";
    }
}

public class Smoothie extends Getraenk {
    private boolean extraObst;

    public Smoothie(String name, int preisCent, boolean extraObst) {
        super(name, preisCent);
        this.extraObst = extraObst;
    }

    @Override
    public int getPreisCent() {
        if (extraObst) {
            return super.getPreisCent() + 80;
        }
        return super.getPreisCent();
    }
}
Getraenk g = new Getraenk("Tee", 150);
Smoothie s = new Smoothie("Mango", 320, true);
Getraenk t = new Smoothie("Beere", 290, false);
int summe = g.getPreisCent() + s.getPreisCent() + t.getPreisCent();
String text = s.beschreibung();

Erkläre, welche getPreisCent()-Methode bei jedem Aufruf läuft, und gib summe sowie die Zahl in text an.

ct
ct
Ansatz: Entscheidend ist, mit welcher Klasse das Objekt bei new erzeugt wurde — nicht der Typ der Variablen. t ist also ein Smoothie.
beschreibung(): Die Methode ist nur in Getraenk geschrieben, ruft aber getPreisCent() auf — und zwar die des Objekts s.
Lösung: 150 + (320 + 80) + 290 = 840. text = „Mango: 400 ct“.
Vollständige Lösung

g ist ein Getraenk → die Methode aus Getraenk: 150. s ist ein Smoothie → die überschriebene Methode; sie holt mit super.getPreisCent() den gespeicherten Preis 320 und addiert 80 = 400. t ist trotz Variablentyp Getraenk ein Smoothie → überschriebene Methode, aber ohne Extra-Obst: 290.

summe = 150 + 400 + 290 = 840 ct. s.beschreibung() ist geerbt; darin läuft wieder die Smoothie-Version von getPreisCent() → „Mango: 400 ct“.

Ohne super. würde sich getPreisCent() in Smoothie endlos selbst aufrufen. Und direkt auf preisCent zugreifen darf Smoothie nicht, weil das Attribut in Getraenk privat ist.

A9
Suchen und besetzen: Theater-AG
AFB II

Die Theater-AG verwaltet die Rollen eines Stücks. Die Klasse Rolle ist fertig (siehe Karten), in Theaterstueck fehlen noch drei Methoden.

import java.util.ArrayList;

public class Theaterstueck {
    private String titel;
    private ArrayList<Rolle> rollen;

    public Theaterstueck(String titel) {
        this.titel = titel;
        rollen = new ArrayList<>();
    }

    public void hinzufuegen(Rolle r) {
        rollen.add(r);
    }
    // suche, besetzen, anzahlFrei fehlen
}

Implementiere suche(name) (liefert die Rolle oder null), besetzen(name) (true nur, wenn es die Rolle gibt und sie noch frei ist) und anzahlFrei(). Teste mit den Rollen Kater, König, Müllerssohn, Zauberer, Prinzessin und den Aufrufen besetzen("Kater"), besetzen("Kater"), besetzen("Riese"), besetzen("Zauberer").

Ansatz: suche läuft mit for-each über rollen und vergleicht mit equals. Erst nach der Schleife steht return null;.
Wiederverwenden: besetzen ruft suche auf. Prüfe zuerst auf null — sonst stürzt r.istBesetzt() mit einer NullPointerException ab.
Lösung: true · false (schon besetzt) · false (gibt es nicht) · true → 5 − 2 = 3 freie Rollen.
Vollständige Lösung
public Rolle suche(String name) {
    for (Rolle r : rollen) {
        if (r.getName().equals(name)) {
            return r;
        }
    }
    return null;
}

public boolean besetzen(String name) {
    Rolle r = suche(name);
    if (r == null || r.istBesetzt()) {
        return false;
    }
    r.besetzen();
    return true;
}

public int anzahlFrei() {
    int frei = 0;
    for (Rolle r : rollen) {
        if (!r.istBesetzt()) {
            frei++;
        }
    }
    return frei;
}

Testlauf: true, false, false, true → 2-mal true. Frei bleiben König, Müllerssohn und Prinzessin: anzahlFrei() = 3.

Wegen || wird r.istBesetzt() nur geprüft, wenn r nicht null ist. Die zweite false-Rückgabe zeigt: Der Testfall „doppelt besetzen“ ist wichtig.

Rolle
  • - name: Zeichenkette
  • - besetzt: Wahrheitswert
  • c Rolle(name: Zeichenkette)
  • + getName(): Zeichenkette
  • + istBesetzt(): Wahrheitswert
  • + besetzen()
Die fertige Klasse Rolle; ein Theaterstück hat viele Rollen
A10
Wie lange sucht die Suche? Vokabeltrainer
AFB II

Ein Vokabeltrainer speichert 250 Vokabeln in einer ArrayList — unsortiert, in der Reihenfolge, in der sie eingegeben wurden. Die Suche nach einer Vokabel läuft wie in der Bücherverwaltung von vorne nach hinten und vergleicht jeden Eintrag mit equals.

Schätze die Anzahl der Vergleiche ab: höchstens, im Mittel (wenn jede Vokabel gleich oft gesucht wird) und höchstens nach einem Update mit 500 Vokabeln.

Vergleiche
Vergleiche
Vergleiche
Ansatz: Steht die gesuchte Vokabel an Platz k (gezählt ab 1), braucht die Suche genau k Vergleiche. Wann ist k am größten?
Mittelwert: Bilde den Durchschnitt von 1, 2, 3, …, 250. Paare den ersten mit dem letzten Wert: 1 + 250 = 2 + 249 = …
Lösung: höchstens n, im Mittel (n + 1) / 2 = 251 / 2 = 125,5.
Vollständige Lösung

a) Ungünstigster Fall: Die Vokabel steht ganz hinten oder fehlt → alle 250 Einträge werden verglichen.

b) Bei gleich häufiger Suche ist der Mittelwert von 1 bis 250: (1 + 250) / 2 = 125,5 Vergleiche.

c) Doppelt so viele Vokabeln → höchstens 500 Vergleiche; auch der Mittelwert verdoppelt sich ungefähr (250,5). Die lineare Suche wächst also im gleichen Verhältnis wie die Liste. Suchen, die nicht gefunden werden, sind immer der teuerste Fall.