- 1 Section
- 10 Lessons
- unbegrenzt
- Java Fortgeschritten10
- 1.1OOP in Java: Klassen, Kapselung, Konstruktoren
- 1.2Vererbung in Java
- 1.3Polymorphismus und Methodenüberschreibung
- 1.4Interfaces und abstrakte Klassen
- 1.5Collections: ArrayList, HashMap, LinkedList
- 1.6Generics und Typsicherheit
- 1.7Streams und Lambda-Ausdrücke
- 1.8Dateioperationen: IO und NIO
- 1.9Unit-Tests in Java: JUnit 5
- 1.10Praxisprojekt: Anwendung mit OOP und Tests
Praxisprojekt: Anwendung mit OOP und Tests
Du hast in K40b alle wichtigen Themen der fortgeschrittenen Java-Programmierung durchgearbeitet – von OOP-Grundlagen über Vererbung, Polymorphismus, Interfaces, Collections, Generics, Streams & Lambda und Dateioperationen bis zu JUnit-Tests. Diese Lektion bündelt das Gelernte in 10 prüfungsnahen Aufgaben, wie sie in IHK-Klausuren oder Code-Reviews auftauchen.
Aufgabe 1: Klasse mit Kapselung
Konto mit folgenden Eigenschaften:
– private Felder
kontonummer (String, unveränderlich) und saldo (double)– Konstruktor, der die Kontonummer entgegennimmt und den Saldo auf 0 setzt
– Methode
einzahlen(double betrag) – Saldo erhöhen, negative Beträge ablehnen– Methode
abheben(double betrag) – Saldo verringern, nur wenn genug Geld da ist– Getter für beide Felder
Wie zeigst du, dass die Kapselung „funktioniert"?
public class Konto { private final String kontonummer; private double saldo; public Konto(String kontonummer) { this.kontonummer = kontonummer; this.saldo = 0.0; } public void einzahlen(double betrag) { if (betrag <= 0) { throw new IllegalArgumentException("Betrag muss positiv sein"); } saldo += betrag; } public void abheben(double betrag) { if (betrag <= 0) { throw new IllegalArgumentException("Betrag muss positiv sein"); } if (betrag > saldo) { throw new IllegalStateException("Saldo nicht ausreichend"); } saldo -= betrag; } public String getKontonummer() { return kontonummer; } public double getSaldo() { return saldo; } }
Kapselung zeigt sich daran:
- Niemand kann von außen
konto.saldo = -1000000schreiben – das Feld istprivate - Negative Einzahlungen, Überziehung, fehlende Mittel werden in den Methoden validiert
kontonummeristfinal: einmal gesetzt, nie wieder geändert- Es gibt keine Setter – die Klasse kontrolliert vollständig, wie sich der Saldo ändern darf
Aufgabe 2: Vererbung anwenden
Konto aus Aufgabe 1 zu einer Klasse Sparkonto, die einen Zinssatz hat. Methode zinsenGutschreiben() soll den aktuellen Saldo um den entsprechenden Prozentsatz erhöhen.
a) Wie übergibst du Kontonummer und Zinssatz im Konstruktor?
b) Was hat es mit dem Aufruf
super(...) auf sich?
public class Sparkonto extends Konto { private double zinssatz; // z. B. 0.02 für 2 % public Sparkonto(String kontonummer, double zinssatz) { super(kontonummer); // Konstruktor der Oberklasse Konto aufrufen this.zinssatz = zinssatz; } public void zinsenGutschreiben() { double zinsen = getSaldo() * zinssatz; einzahlen(zinsen); // nutzt die geerbte Methode } public double getZinssatz() { return zinssatz; } }
a) Der Konstruktor von Sparkonto nimmt beide Werte entgegen. Die Kontonummer wird per super(kontonummer) an die Oberklasse weitergereicht, der Zinssatz im Sparkonto-eigenen Feld gespeichert.
b) super(...) ruft den Konstruktor der Oberklasse auf und muss die erste Anweisung im Konstruktor der Unterklasse sein. Würde sie fehlen, müsste die Oberklasse einen parameterlosen Konstruktor haben – den hat Konto aber nicht. Compile-Fehler wäre die Folge.
Beachte: einzahlen() und getSaldo() sind geerbt, müssen also nicht neu geschrieben werden. Sparkonto baut nur auf das Vorhandene auf.
Aufgabe 3: Polymorphismus erkennen
class Tier {
public void laut() { System.out.println("..."); }
}
class Hund extends Tier {
@Override
public void laut() { System.out.println("Wuff!"); }
}
class Welpe extends Hund {
@Override
public void laut() { System.out.println("Wuff (klein)"); }
}
public static void main(String[] args) {
Tier t1 = new Hund();
Tier t2 = new Welpe();
Hund h = new Welpe();
t1.laut();
t2.laut();
h.laut();
}
Was wird ausgegeben? Begründe.
Die Ausgabe ist:
Wuff! Wuff (klein) Wuff (klein)
Begründung (dynamic dispatch):
t1.laut()– statischer TypTier, dynamischer Typ Hund. Java ruft die Methode der tatsächlichen Klasse auf → „Wuff!"t2.laut()– statischer TypTier, dynamischer TypWelpe. → „Wuff (klein)"h.laut()– statischer TypHund, dynamischer TypWelpe. Auch hier zählt der dynamische Typ → „Wuff (klein)"
Polymorphismus heißt: welche Methode ausgeführt wird, entscheidet der tatsächliche Objekttyp – nicht der deklarierte Variablentyp. Der deklarierte Typ entscheidet nur, welche Methoden überhaupt aufgerufen werden dürfen.
Aufgabe 4: Interface oder abstrakte Klasse?
a) Modelliere das Ganze: was ist Interface, was abstrakte Klasse?
b) Schreibe den Code für die Basis-Klasse/das Basis-Interface plus eine konkrete Klasse
Rechnung, die beides kann.
a) Modellierung:
- Abstrakte Klasse
Dokument: hat gemeinsames Feldtitelund gemeinsame Methoden – passt zur Vererbungs-Hierarchie - Interface
Versendbar: eine Fähigkeit, die manche Dokumente haben – nicht alle. Ideal als Interface, das gezielt implementiert wird
b) Code:
public abstract class Dokument { protected String titel; public Dokument(String titel) { this.titel = titel; } public String getTitel() { return titel; } public abstract void drucken(); } public interface Versendbar { void versenden(String empfaenger); } public class Rechnung extends Dokument implements Versendbar { public Rechnung(String titel) { super(titel); } @Override public void drucken() { System.out.println("Drucke Rechnung: " + titel); } @Override public void versenden(String empfaenger) { System.out.println("Sende Rechnung an " + empfaenger); } }
Warum diese Aufteilung sinnvoll ist:
- Wenn man später ein
Lieferscheinohne E-Mail-Versand möchte, erbt es einfach vonDokumentund implementiertVersendbarnicht - Eine Methode kann gezielt
Versendbarals Parametertyp nehmen und damit nur die versendbaren Dokumente bedienen - Klassen aus völlig anderen Hierarchien könnten
Versendbarebenfalls implementieren (z. B.Nachricht) – Mehrfachvererbung über Interfaces
Aufgabe 5: Collection wählen
a) Eine Warteschlange in einer Werkstatt – wer zuerst kommt, ist zuerst dran
b) Eine Liste von eindeutigen E-Mail-Adressen für einen Newsletter, die häufig durchsucht wird („ist diese Adresse schon dabei?")
c) Eine Zuordnung Benutzer-ID → Benutzer-Objekt, mit häufigen Lookups
d) Eine alphabetisch sortierte Liste von Städtenamen
e) Eine Liste mit zehntausenden Bestellungen, auf die hauptsächlich per Index zugegriffen wird
- a) Warteschlange:
LinkedListoder besserArrayDequealsQueue. Beide unterstützen FIFO mit O(1) für Hinzufügen am Ende und Entfernen am Anfang - b) Newsletter-Adressen:
HashSet<String>. Eindeutigkeit ist automatisch garantiert, Mitgliedschaftsprüfung (contains) in O(1) - c) Benutzer-Lookup:
HashMap<Long, Benutzer>. Schneller Schlüssel-Wert-Zugriff in O(1) - d) Sortierte Städte:
TreeSet<String>. Hält Elemente automatisch sortiert, ohne Duplikate - e) Bestellungen per Index:
ArrayList. Schneller Index-Zugriff in O(1), kompakter Speicher. Eine LinkedList wäre für Index-Zugriffe deutlich langsamer (O(n))
Faustregel: List für geordnete Sequenzen, Set für eindeutige Elemente, Map für Schlüssel-Wert. Hash-Varianten sind schnell aber unsortiert, Tree-Varianten sortiert aber langsamer.
Aufgabe 6: Generische Methode
letzte(List<T> liste, int n), die die letzten n Elemente einer Liste zurückgibt – egal welchen Typs.
a) Wie schreibst du die Methoden-Signatur, sodass sie für jeden Typ funktioniert?
b) Was passiert, wenn n größer ist als die Listengröße?
c) Wie nutzt der Aufrufer die Methode? Brauchst du beim Aufruf eine explizite Typ-Angabe?
public class Utils { public static <T> List<T> letzte(List<T> liste, int n) { if (n <= 0) return List.of(); int start = Math.max(0, liste.size() - n); return new ArrayList<>(liste.subList(start, liste.size())); } }
a) Das <T> direkt vor dem Rückgabetyp deklariert die Methode als generisch. T wird zum Platzhalter für den Element-Typ – funktioniert mit beliebigen Typen.
b) Falls n größer als liste.size() ist, ist liste.size() - n negativ. Math.max(0, ...) begrenzt den Startindex auf 0, sodass alle Elemente zurückgegeben werden. Sicherer als eine IndexOutOfBoundsException.
c) Aufruf:
List<String> alle = List.of("a", "b", "c", "d", "e"); List<String> letzte3 = Utils.letzte(alle, 3); // [c, d, e] List<Integer> zahlen = List.of(1, 2, 3, 4); List<Integer> letzte2 = Utils.letzte(zahlen, 2); // [3, 4]
Der Compiler leitet den Typ aus dem ersten Argument ab (Type Inference) – keine explizite Typ-Angabe nötig. Möglich wäre auch Utils.<String>letzte(alle, 3), aber selten nötig.
Aufgabe 7: Stream-Pipeline schreiben
Bestellung-Objekten mit den Feldern kunde (String), betrag (double), bezahlt (boolean).
Schreibe eine Stream-Pipeline, die:
1. Nur die bezahlten Bestellungen behält
2. Den Gesamt-Betrag dieser Bestellungen summiert
3. Auf zwei Nachkommastellen rundet
Bonus: zähle in einer zweiten Pipeline, wie viele Bestellungen pro Kunde noch nicht bezahlt sind.
1. Summe bezahlter Bestellungen:
double summe = bestellungen.stream() .filter(Bestellung::isBezahlt) .mapToDouble(Bestellung::getBetrag) .sum(); double gerundet = Math.round(summe * 100.0) / 100.0;
Wichtig: mapToDouble wandelt in einen DoubleStream um, der direkt .sum(), .average(), .max() kennt. Effizienter als Stream<Double> mit reduce.
Bonus: unbezahlte Bestellungen pro Kunde:
Map<String, Long> offenProKunde = bestellungen.stream() .filter(b -> !b.isBezahlt()) .collect(Collectors.groupingBy( Bestellung::getKunde, Collectors.counting() ));
groupingBy baut eine Map nach dem Gruppierungs-Schlüssel auf, counting() zählt die Elemente pro Gruppe – das Ergebnis ist eine Map von Kundennamen zu Anzahl offener Bestellungen.
Aufgabe 8: try-with-resources und IOException
zaehleZeilenMitWort(Path datei, String wort), die zählt, in wie vielen Zeilen einer Textdatei das gegebene Wort vorkommt.
a) Wie liest du die Datei effizient, auch wenn sie groß ist?
b) Wie behandelst du eine
IOException sinnvoll?c) Warum ist
try-with-resources hier wichtig?
import java.nio.file.Files; import java.nio.file.Path; import java.io.IOException; import java.util.stream.Stream; public static long zaehleZeilenMitWort(Path datei, String wort) throws IOException { try (Stream<String> lines = Files.lines(datei)) { return lines .filter(z -> z.contains(wort)) .count(); } }
a) Effizientes Lesen: Files.lines(path) gibt einen Stream zurück, der zeilenweise lazy gelesen wird. Eine 10-GB-Datei wird nicht komplett in den Speicher geladen, sondern Zeile für Zeile verarbeitet.
b) IOException-Behandlung: zwei sinnvolle Varianten:
- Weiterreichen mit
throws IOException(wie oben): der Aufrufer entscheidet, was bei Fehler passiert - Fangen mit Fallback:
try (...) { return ...; } catch (IOException e) { return -1; }– nur wenn der Aufrufer den Fehler nicht selbst handhaben kann
Was du nicht tun solltest: ein leeres catch (Exception e) {}. Das verschluckt den Fehler und macht Bugs unmöglich zu finden.
c) try-with-resources: Files.lines() öffnet die Datei und gibt ein Datei-Handle zurück. Ohne automatisches Schließen würde das Handle offen bleiben, bis der Garbage Collector kommt – und das kann sehr spät sein. Bei vielen geöffneten Dateien kommt irgendwann „Too many open files". try-with-resources schließt den Stream garantiert, auch bei einer Exception im Schleifen-Körper.
Aufgabe 9: JUnit-Test schreiben
Konto aus Aufgabe 1. Decke mindestens folgende Fälle ab:
a) Neues Konto hat Saldo 0
b) Einzahlen erhöht den Saldo
c) Negative Einzahlung wirft
IllegalArgumentExceptiond) Abheben mehr als Saldo wirft
IllegalStateExceptionNutze sinnvoll
@BeforeEach und ggf. assertThrows.
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class KontoTest { private Konto konto; @BeforeEach void setUp() { konto = new Konto("DE12 3456 7890"); } @Test void neuesKontoHatSaldoNull() { assertEquals(0.0, konto.getSaldo(), 0.001); } @Test void einzahlenErhoehtSaldo() { konto.einzahlen(100.0); konto.einzahlen(50.0); assertEquals(150.0, konto.getSaldo(), 0.001); } @Test void negativeEinzahlungWirftException() { IllegalArgumentException ex = assertThrows( IllegalArgumentException.class, () -> konto.einzahlen(-10) ); assertTrue(ex.getMessage().contains("positiv")); } @Test void abhebenMehrAlsSaldoWirftException() { konto.einzahlen(50); assertThrows( IllegalStateException.class, () -> konto.abheben(100) ); // Saldo blieb unverändert assertEquals(50.0, konto.getSaldo(), 0.001); } }
Beachte:
@BeforeEachbaut für jeden Test ein frisches Konto auf – Tests sind unabhängig- Bei
double-Vergleichen immer eine Toleranz (0.001) angeben – Fließkomma-Arithmetik ist nicht exakt assertThrowsbekommt ein Lambda mit dem zu testenden Aufruf und liefert das Exception-Objekt zurück, das du weiter inspizieren kannst- AAA-Pattern erkennbar: Arrange (BeforeEach + ggf. Einzahlung), Act (die getestete Aktion), Assert (Prüfung des Ergebnisses)
Aufgabe 10: Komposition vs. Vererbung
Auto. Es hat einen Motor, der eigene Eigenschaften hat (PS, Hubraum, Kraftstoff). Zwei Modellierungen sind denkbar:
a)
Auto extends Motor – das Auto „ist ein" Motor mit zusätzlichen Eigenschaftenb)
Auto mit Feld private Motor motor; – das Auto „hat einen" Motor
Welche Variante ist besser, und warum? Welche Vorteile bringt die andere Variante?
Variante b) Komposition ist klar besser.
Begründung:
- „Ist-ein"-Test versagt: ein Auto ist kein Motor. Es hat einen Motor. Vererbung modelliert „ist-ein"-Beziehungen – passt hier nicht
- Motor austauschbar: bei Komposition kann ein Auto zur Laufzeit einen anderen Motor bekommen. Bei Vererbung wäre der Typ festgenagelt
- Mehrere Komponenten: ein Auto hat Motor, Getriebe, Bremsen, Räder – Mehrfach-Komposition geht einfach, Mehrfach-Vererbung nicht
- Lockere Kopplung: bei Komposition sieht Auto nur die öffentliche API des Motors, nicht seine inneren Details. Refactoring im Motor wird einfacher
- Testbarkeit: in Tests kann ein Mock-Motor übergeben werden, das geht bei Vererbung schlechter
Beispiel-Code:
public class Motor { private final int ps; private final double hubraum; public Motor(int ps, double hubraum) { this.ps = ps; this.hubraum = hubraum; } public int getPs() { return ps; } } public class Auto { private Motor motor; private String marke; public Auto(String marke, Motor motor) { this.marke = marke; this.motor = motor; } public void setMotor(Motor motor) { this.motor = motor; } }
Wann ist Vererbung trotzdem richtig? Wenn die „ist-ein"-Beziehung wirklich passt und stabil bleibt: SportAuto extends Auto, LKW extends Auto sind sinnvoll, weil ein Sportauto wirklich ein Auto ist – nur eines mit zusätzlichen oder geänderten Eigenschaften.
Faustregel: „Favor composition over inheritance". Vererbung erst, wenn sich die Beziehung wirklich nicht anders modellieren lässt. Mehr dazu in K48 (OOP-Konzepte) und K49 (Design Patterns).
Lernhinweise für die IHK-Prüfung
Die wichtigsten K40b-Themen für die FIAE-Prüfung Teil 2:
- OOP-Grundbegriffe: Klasse vs. Objekt, Konstruktor, Kapselung mit
privateund Gettern/Settern, Sichtbarkeits-Modifier - Vererbung vs. Komposition: typische Klausurfrage – wann was, warum „Komposition vor Vererbung"
- Polymorphismus: dynamic dispatch verstehen, Code-Output bei überschriebenen Methoden vorhersagen
- Interface vs. abstrakte Klasse: was ist der Unterschied, kann eine Klasse mehrere Interfaces implementieren (ja), mehrere Klassen erben (nein)
- Collections-Wahl: List/Set/Map zum richtigen Anwendungsfall, Performance-Charakteristika kennen
- Generics-Grundlagen: was bedeutet
List<String>, wie schreibe ich eine generische Klasse oder Methode - Streams-Konzept: Quelle, Zwischenoperationen (filter, map), Endoperation (collect, count), Lambda-Syntax
- Dateioperationen: try-with-resources, IOException, Path/Files
- JUnit-Grundlagen: was ist ein Unit-Test, @Test/@BeforeEach, AAA-Pattern, Test-Isolation
Praxis-Tipp: ein eigenes kleines Projekt anlegen – z. B. einen Bibliotheks-Verwalter mit Buch, Bibliothek, Ausleihe. Alle Themen aus K40b lassen sich darin unterbringen: Vererbung (verschiedene Medientypen), Interfaces (Ausleihbar), Collections (Liste der Bücher, Map der Ausleihen), Generics (eigene Container-Klasse), Streams (Suche und Statistik), Dateioperationen (Speichern/Laden), JUnit (Tests für jede Klasse).
Zusammenfassung
Die 10 Aufgaben decken alle Kernthemen aus K40b ab: Klassen mit Kapselung (private Felder, Validierung in Methoden, final-Felder), Vererbung mit extends und super(...), Polymorphismus mit dynamic dispatch (dynamischer Typ entscheidet), Interface vs. abstrakte Klasse (Verhalten vs. Hierarchie + Zustand), Collection-Wahl nach Anwendungsfall, generische Methoden mit <T>, Stream-Pipelines mit filter/map/collect/groupingBy, try-with-resources für sichere Datei-IO, JUnit-Tests mit @BeforeEach und assertThrows, und das Designprinzip Komposition statt Vererbung. Für die IHK-Prüfung am wichtigsten: die richtige Lösungstechnik zur jeweiligen Fragestellung erkennen.
