Java Videotraining Werbung

Videotraining Spring 3 Boot Werbung

1. Eigene Klassen schreiben

Natürlich haben wir bis jetzt schon diverse Klassen geschrieben. Aber diese Klassen hatten bisher nur statische Eigenschaften, und wir haben unsere eigenen Klassen bisher nicht benutzt, um davon Exemplare zu bilden. Objekte selbst haben wir natürlich schon aufgebaut, und zwar aus Klassen der Standardbibliothek. Dieses Wissen wollen wir jetzt erweitern. Wir wollen Klassen schreiben und selbst von unseren eigenen Typen Exemplare bilden.

In diesem Kapitel entwickeln wir ein digitales Haustierspiel, inspiriert von den beliebten Tamagotchis der 90er Jahre. Wir beginnen mit einfachen Pokopettos, die gefüttert und gepflegt werden müssen. Im Laufe der Aufgaben erweitern wir das Spiel um verschiedene Pokopetto-Arten und komplexere Spielmechaniken.

Dabei üben wir wichtige Konzepte der Objektorientierung:

  • Wie Objekte Zustände und Verhalten haben

  • Wie Objekte miteinander in Beziehung stehen (Assoziationen oder "Hat"-Beziehungen)

  • Wie wir Gemeinsamkeiten in Hierarchien organisieren (Vererbung oder "Ist-eine-Art-von"-Beziehungen)

Captain CiaoCiao und Bonny Brain werden uns auf dieser Reise begleiten und ihre virtuelle Pokopetto-Sammlung immer weiter ausbauen und verbessern.

Voraussetzungen

  • neue Klassen anlegen können

  • Objektvariablen in Klassen setzen können

  • Methoden implementieren können

  • Sichtbarkeiten private, paketsichtbar und public kennen

  • statische Eigenschaften programmieren können

  • Aufzählungstypen deklarieren und nutzen können

  • einfache und überladene Konstruktoren implementieren können

  • Arten von Assoziationen kennen

  • 1:1-Assoziation implementieren können

  • einfache Listen nutzen können

  • Delegation von Operationen einsetzen können

  • Vererbungsbeziehungen mit extends realisieren können

  • Methoden überschreiben können

  • zwei Bedeutungen von super verstehen

  • dynamisches Binden einsetzen können

  • Schnittstellen nutzen und deklarieren können

  • Default-Methoden in Schnittstellen einsetzen können

Verwendete Datentypen in diesem Kapitel:

1.1. Klassendeklaration und Objekteigenschaften

Für einen neuen Typ schreiben wir in Java eine neue Klasse. Legen wir in diesem Abschnitt ein paar Klassen an, und geben wir den Klassen Objektvariablen und Methoden.

1.1.1. Pokopetto aufbauen (NEU) ⭐

Captain CiaoCiao liebt es, neue Technologien zu erforschen. Als er in einem japanischen Hafen ein merkwürdiges elektronisches Spielzeug entdeckt — ein sogenanntes Pokopetto — ist er sofort fasziniert. Dieses virtuelle Haustier erinnert ihn an die Tamagotchis der 90er Jahre…​

Wie sein Vorbild muss ein Pokopetto gefüttert und gepflegt werden, damit es gesund und glücklich bleibt. Der Zustand eines Pokopettos wird durch drei Zustände bestimmt:

  • seinen Hunger (0 = satt bis 100 = kurz vor dem Verhungern),

  • seine Stimmung (0 = erschöpft/traurig bis 100 = energiegeladen/glücklich) und

  • sein Alter (gemessen in erfolgreichen Interaktionen).

Aufgabe:

  • Lege eine neue Klasse Pokopetto an.

  • Setze folgende Objektvariablen in die Klasse, sodass jedes Pokopetto einen eigenen Zustand haben kann:

    • hunger (Hungergefühl des Pokopetto)

    • mood (Stimmung und Energielevel)

    • age (Alter des Pokopetto in Spielzügen)

      Welche Variablentypen sind sinnvoll?

  • Schreibe zusätzlich eine Klasse PokopettoDemo, die ein Pokopetto-Objekt in ihrer main(…)-Methode aufbaut. Belege und erfrage zum Test die Variablen. Wir wollen in dieser Klasse im Folgenden verschiedene Features testen.

  • Setze in der Pokopetto-Klasse die Startwerte für die Objektvariablen.

    • hunger soll mit 30 starten (leicht hungrig)

    • mood soll mit 80 starten (gute Stimmung)

Pokopetto Attributes UML
Abbildung 1. UML-Diagramm der Pokopetto-Klasse ohne Datentypen

Berücksichtige die Namenskonventionen: Klassen beginnen mit einem Großbuchstaben, und Variablen sowie Methoden mit einem Kleinbuchstaben; nur Konstanten sind in Großbuchstaben. Wir schreiben alles auf Englisch und verzichten auf deutsche Bezeichner.

1.1.2. Pokopetto steuern (NEU) ⭐

Nach seiner ersten Begegnung mit einem Pokopetto beschließt Captain CiaoCiao, die Funktionsweise genauer zu studieren. Er erstellt eine Liste der grundlegenden Aktionen, die ein Pokopetto ausführen kann. Nun sollen die Methoden implementiert werden, die das Verhalten des virtuellen Haustiers steuern.

Für die Werteänderungen steht folgende Hilfsmethode zur Verfügung:

int clampPercentage( int value ) {
  return Math.clamp( value, 0, 100 );
}

Diese Methode stellt sicher, dass Werte im gültigen Bereich von 0 bis 100 bleiben. Kopiere die Methode in die Klasse Pokopetto. Sie nutzt die clamp(…​)-Methode aus Java 21. Für ältere Java-Versionen verwende Math.min(Math.max(value, 0), 100).

Aufgabe:

  • Ergänze folgende Statusabfrage-Methoden:

    • boolean isHungry(): Gibt true zurück, wenn hunger > 30

    • boolean isStarving(): Gibt true zurück, wenn hunger > 70

    • boolean isExhausted(): Gibt true zurück, wenn mood < 30

    • boolean isTired(): Gibt true zurück, wenn mood < 60

  • Implementiere die Aktionsmethoden:

    • void feed(): Wenn hungrig, reduziere hunger um 30 und erhöhe mood um 20

    • void play(): Wenn nicht erschöpft und nicht am Verhungern, reduziere mood um 30 und erhöhe hunger um 15

    • void sleep(): Erhöhe mood um 40 und hunger um 10

      Nutze intern clampPercentage(…​) für alle Wertänderungen von hunger und mood. Das age-Attribut soll nur erhöht werden, wenn eine Aktion erfolgreich durchgeführt wurde.

  • Implementiere eine Methode public String toString(), die den Status als Text zurückgibt. Das allgemeine Format soll sein: Pokopetto is [exhausted|tired|energetic] and [starving|hungry|full].

  • Lege eine neue Klasse PokopettoGame mit einer main(…​) Methode an und kopiere Folgendes hinein, um die Objektmethoden zu testen und mit den Pokopettos zu spielen.

    Pokopetto pokopetto = new Pokopetto();
    
    while ( !pokopetto.isStarving() && !pokopetto.isExhausted() ) {
      System.out.println( pokopetto );
      System.out.println( "Choose an action: feed, play, sleep, or quit" );
      String action = new Scanner( System.in ).nextLine().toLowerCase();
    
      switch ( action ) {
        case "feed"  -> pokopetto.feed();
        case "play"  -> pokopetto.play();
        case "sleep" -> pokopetto.sleep();
        case "quit"  -> { System.out.println( "Goodbye!" ); return; }
        default      -> System.out.println( "Invalid action. Try again." );
      }
    }
    
    System.out.println( "Pokopetto is not feeling well! Game over." );
Pokopetto Methods UML
Abbildung 2. UML-Diagramm der überarbeiteten Pokopetto-Klasse

1.1.3. Private Parts: Eigenschaften aus Pokopetto privat machen (NEU) ⭐

Die privaten Details einer Implementierung dürfen nicht öffentlich sein, damit man jederzeit das Innere ändern kann.

Aufgabe:

  • Mache alle Objektvariablen aus Pokopetto privat. Gibt es Compilerfehler? Wenn, kommentiere Programmstücke aus.

  • Überlege, welche Methoden public werden können, welche private.

1.1.4. Setter und Getter für Pokopetto einführen (NEU) ⭐

Getter und Setter sind in der Java-Welt oft anzutreffen. Sie werden benutzt, um sogenannte Properties zu definieren. Viele Frameworks greifen automatisch auf die Properties über die Getter/Setter zu.

Aufgabe:

  • Ergänze eine neue private Objektvariable String name.

    • Implementiere einen Getter getName().

    • Implementiere einen Setter setName(String name), der eine NullPointerException wirft, wenn der Parameter null ist. Der Name soll ohne Weißraum am Anfang und Ende gespeichert werden.

  • Ergänze für das bestehende Attribut age nur einen Getter getAge(). Einen Setter darf es nicht geben, da das Alter von außen nicht verändert werden darf.

Pokopetto Setter Getter UML
Abbildung 3. UML-Diagramm der überarbeiteten Pokopetto-Klasse mit korrigierten Sichtbarkeiten und Setter/Getter

Setter und Getter sind eine wichtige Namenskonvention. Ist eine Property XXX vom Typ boolean, so ist das Präfix im Allgemeinen isXXX(), nicht getXXX(). Die bestehenden Methoden isHungry(), isStarving(), isExhausted() und isTired() sind somit korrekt benannte boolean Getter auch wenn intern keine entsprechende Objektvariable existiert. Die Getter können den Wert berechnen, wichtig ist nur die einheitliche Schnittstelle nach außen.

1.2. Statische Eigenschaften

Klassenvariablen und statische Methoden führen bei Programmiereinsteigern oft zur Verwirrung. Dabei ist es ganz einfach: Entweder können Zustände bei den individuellen Objekten gespeichert werden oder bei der Klasse selbst. Haben wir verschiedene Objekte mit Objektvariablen, können die Objektmethoden auf die individuellen Eigenschaften zurückgreifen. Eine statische Methode kann ohne explizite Angabe eines Objekts nur auf Klassenvariablen zurückgreifen.

1.2.1. Konstanten und statische Hilfsmethoden in Pokopetto einführen (NEU) ⭐

Bisher hat das Pokopetto nur Objekteigenschaften. Ergänzen wir statische Eigenschaften, die keine Beziehung zu einem konkreten Pokopetto-Objekt haben.

Aufgabe:

  • Die Startwerte 30 und 80 sind an dieser Stelle nicht optimal:

    int hunger = 30;
    int mood   = 80;

    Führen neue private statische finale Variablen INITIAL_HUNGER und INITIAL_MOOD ein, die für die Startbelebung verwendet werden können. Für den Startwert 0 bei age benötigen wir keine Konstante.

  • Kann clampPercentage(…​) statisch sein? Was spricht dafür, was dagegen?

  • Implementiere eine neue statische Methode String generateRandomName(), die einen zufälligen Namen aus vorgegebenen Namensbestandteilen generiert. Die Methode soll aus jedem der drei Arrays zufällig ein Element auswählen und es konkatenieren:

    String[] parts1 = { "Piko", "Mochi", "Neko", "Kawa", "Yuki", "Haru", "Saku", "Mimi", "Tama" };
    String[] parts2 = { "ni", "mo", "ka", "chi", "yu", "ri", "mi", "sa", "ta", "ko" };
    String[] parts3 = { "chan", "kun", "san", "chi", "tan", "rin", "pi", "nyan", "maru", "ko" };

    Gültig wäre zum Beispiel "Piko" + "mo" + "chi" = "Pikomochi".

  • Teste die Methode in PokopettoDemo.

Pokopetto Statics UML
Abbildung 4. UML-Diagramm der überarbeiteten Pokopetto-Klasse mit statischen Eigenschaften

1.3. Einfache Aufzählungen

Aufzählungen sind geschlossene Mengen, die in Java über das Schlüsselwort enum aufgebaut werden.

1.3.1. Evolutionsstufen eines Pokopetto (NEU) ⭐

Aufgabe:

  • Führe einen neuen Aufzählungstyp (Enum) für die verschiedenen Entwicklungsstufen eines Pokopetto ein.

    enum Evolution {
      BABY, TEEN, ADULT
    }
  • Implementiere eine neue Methode getEvolution() in der Pokopetto Klasse, die basierend auf dem Alter die aktuelle Entwicklungsstufe zurückgibt.

    • BABY: Alter 0–5

    • TEEN: Alter 6–15

    • ADULT: Alter ab 16

  • Teste die Methode in PokopettoDemo.

Pokopetto Enum UML
Abbildung 5. UML-Diagramm der überarbeiteten Pokopetto-Klasse mit Enum-Bezug, private Eigenschaften ausgeblendet

1.4. Konstruktoren

Konstruktoren sind besondere Initialisierungsroutinen, die beim Anlegen eines Objekts automatisch von der virtuellen Maschine aufgerufen werden. Wir nutzen Konstruktoren oft, um bei der Erzeugung von Objekten Zustände zuzuweisen, die wir uns dann im Objekt merken.

1.4.1. Pokopetto über verschiedene Konstruktoren aufbauen (NEU) ⭐

Unser Pokopetto hat bisher nur einen vom Compiler generierten Standardkonstruktor. Ersetzen wir diesen durch eigene Konstruktoren.

Aufgabe:

  • Folgende drei Konstruktoren sollen erstellt werden:

    • Parameterloser Konstruktor: Soll einen zufälligen Namen über generateRandomName() generieren und den initialen Hunger-Wert INITIAL_HUNGER und die initiale Stimmung INITIAL_MOOD verwenden.

    • Konstruktor mit zwei Parametern (name, hunger) soll den übergebenen Namen und Hunger-Wert verwenden und die initiale Stimmung INITIAL_MOOD verwenden. Verwende clampPercentage(…​) zur Sicherstellung gültiger Werte für hunger.

    • Konstruktor mit drei Parametern (name, hunger, mood) kann alle Objektwerte direkt aus den Parametern setzen. Verwende clampPercentage(…​) zur Sicherstellung gültiger Werte für hunger und mood.

  • Konstruktoren können mit this(…​) aufeinander verweisen. Implementiere damit eine Konstruktor-Hierarchie nach dem Telescoping Constructor Pattern. Die Konstruktoren sollen aufeinander aufbauen, wobei der Konstruktor mit den meisten Parametern die eigentliche Arbeit macht. Die anderen Konstruktoren rufen diesen mit Standardwerten auf, anstatt die Werte selbst zu setzen.

  • Teste den Aufbau in PokopettoDemo.

Pokopetto Constructors UML
Abbildung 6. UML-Diagramm der überarbeiteten Pokopetto-Klasse mit Konstruktoren

1.4.2. Copy-Konstruktor für Pokopetto implementieren (NEU) ⭐

Wird im Konstruktor einer Klasse ein Objekt des gleichen Typs als Vorlage angenommen, so sprechen wir von einem Copy-Konstruktor.

Aufgabe:

  • Implementiere für Pokopetto einen Copy-Konstruktor. Kopiere name, hunger, mood und age.

1.4.3. Pokopetto-Fabrik einrichten (NEU) ⭐

Neben Konstruktoren bieten einige Klassen eine alternative Variante zum Anlegen, sogenannte Fabrikmethoden. Dabei gilt:

  • Es gibt prinzipiell Konstruktoren, aber die sind oftmals privat, und folglich können Außenstehende keine Instanzen erzeugen.

  • Damit Objekte aufgebaut werden können, gibt es (statische) Methoden, die intern den Konstruktor aufrufen und die Instanz zurückgeben.

Captain CiaoCiao stellt fest, dass die Erstellung neuer Pokopettos mit mehreren Zahlenwerten verwirrend sein kann. Was bedeutet new Pokopetto("Mimimi", 30, 80) — ist das ein hungriges oder glückliches Pokopetto?

Aufgabe:

  • Erstelle eine neue Klasse PokopettoFactory mit folgenden statischen Methoden (nicht angegebe Werte sollen auf Standardwerte INITIAL_HUNGER/INITIAL_MOOD gesetzt werden):

    • Pokopetto createHungryDefaultPokopetto(String name, int hungerValue)

    • Pokopetto createHappyDefaultPokopetto(String name, int moodValue)

  • Zähle jedes erstellte Pokopetto mit. Jedes 10. Pokopetto soll mit maximaler Stimmung (mood = 100) erstellt werden.

1.5. Assoziationen

Eine Assoziation ist eine dynamische Verbindung von zwei oder mehreren Objekten. Assoziation können wir auf verschiedene Arten charakterisieren:

  • Kennt nur eine Seite die andere, oder kennen sich beide Seiten?

  • Ist die Lebensdauer eines Objektes an die Lebensdauer eines Objektes gebunden?

  • Mit wie vielen anderen Objekten hat ein Objekt eine Verbindung? Gibt es eine Verbindung zu nur einem anderen Objekt oder zu mehreren? Bei 1:n- oder n:m-Beziehungen benötigen wir Container, wie Arrays oder dynamische Datenstrukturen wie die java.util.ArrayList.

1.5.1. Mehrere Pokopettos im Spiel verwalten (NEU) ⭐⭐

Die Mannschaft ist so begeistert von Pokopettos, dass bald jeder ein eigenes haben möchte. Um den Überblick nicht zu verlieren, benötigt der Captain ein System zur Verwaltung mehrerer Pokopettos. Um dies umzusetzen, ersetzen wir das einzelne Pokopetto durch eine Sammlung von Pokopettos.

Aufgabe:

  • Refactor in PokopettoGame den Variablennamen pokopetto durch eine Umbenennung in den Plural pokopettos. Und ändere den Typ der Referenzvariablen zu einer ArrayList; die Variable kann final werden:

    final ArrayList<Pokopetto> pokopettos = new ArrayList<Pokopetto>();
  • Schreibe ein neues Kommando create, das ein neues Pokopetto Objekt erzeugt und der Liste hinzufügt.

  • Schreibe ein neues Kommando list, das eine Liste aller Pokopettos mit Index anzeigt.

  • Damit unsere Pokopettos nicht verhungern, wollen wir ihren Zustand überprüfen können.

    • Ergänze ein neues Kommando check, das ausgibt, wie viele Pokopettos in der Liste am Verhungern sind. Nutze zum Testen die Methode isStarving() der Pokopetto-Klasse.

    • Die Beispielausgabe könnte sein: 2 Pokopettos at index 0, 2 are starving.

  • Passe die drei Kommandos feed, play, sleep an, sodass sie einen Index benötigen. Das Format kann etwa sein: <command> <index>, z. B.feed 0 oder play 2. Fehlerhafte Indizes müssen erkannt werden und dürfen nicht zum Spielabbruch führen.

  • Da einzelne Pokopettos unabhängig voneinander sterben können, müssen diese aus der Liste entfernt werden. Das Spiel endet, wenn keine Pokopettos mehr in der Liste sind.

1.6. Vererbung

Vererbung modelliert eine Ist-eine-Art-von-Beziehung und verbindet zwei Typen sehr direkt. Die Modellierung ist sehr wichtig, um Gruppen von zusammenhängenden Dingen zu bilden.

1.6.1. Einführung einer Abstraktionshierarchie für Pokopettos (NEU) ⭐

Während einer langen Seereise beobachtet Captain CiaoCiao, dass sich einige Pokopettos anders verhalten als andere. Er beschließt, seine Forschungen zu systematisieren und verschiedene Pokopetto-Arten zu klassifizieren. Mit dieser Erweiterung führen wir eine objektorientierte Hierarchie für verschiedene Pokopetto-Arten ein. Die gemeinsamen Eigenschaften werden in einer Basisklasse gebündelt. Die neue Klassenhierarchie ermöglicht es, verschiedene Pokopetto-Arten mit unterschiedlichem Verhalten zu erstellen, während gemeinsame Eigenschaften in der Basisklasse gebündelt sind.

Aufgabe:

  • Lege eine neue Klasse DefaultPokopetto für Pokopettos an, die von Pokopetto abgeleitet ist. Durch die Vererbungsbeziehung verhält sich DefaultPokopetto genau wie die bisherigen Pokopettos.

  • Lege eine weitere Klasse HyperPokopetto als Unterklasse von Pokopetto an. Nach dem Anlegen einer Instanz soll es automatisch einen mood von 100 haben.

  • Welche Konstruktoren stehen in den neuen Klassen zur Verfügung? Erstelle verschiedene Pokopetto-Arten und überprüfe ihre Werte in PokopettoDemo.

Pokopetto Inheritance UML
Abbildung 7. UML-Diagramm der Pokopetto-Vererbungshierarchie

1.6.2. Konstruktor-Verkettung (NEU) ⭐

Die neuen Pokopetto-Arten haben ein Problem: Sie können nicht so flexibel erstellt werden wie die Original-Pokopettos, da Konstruktoren nicht vererbt werden.

Aufgabe:

  • Sieh dir die verfügbaren Konstruktoren in Pokopetto an und implementiere die gleichen Konstruktoren in DefaultPokopetto. Alle drei Konstruktor-Varianten sollen verfügbar sein.

  • Nutze super(…​), um den passenden Konstruktor der Oberklasse aufzurufen.

1.6.3. Objekttyp und Referenztyp (NEU) ⭐

Aufgabe:

In der Klasse PokopettoGame werden Objekte bisher mit new Pokopetto() erzeugt. Ändere dies, sodass die Objekterstellung stattdessen mit new DefaultPokopetto() erfolgt.

Beantworte die folgenden Fragen:

  • Was ist der Unterschied zwischen Referenztyp und Objekttyp?

  • Welche Bedeutung hat der Typ auf der linken Seite einer Zuweisung?

  • Welche Bedeutung hat der Typ auf der rechten Seite einer Zuweisung?

  • Beeinflusst diese Änderung das Verhalten des Spiels?

1.6.4. Erschöpfte Pokopettos erkennen (NEU) ⭐

Bei der täglichen Inspektion seiner Pokopetto-Sammlung stellt Captain CiaoCiao fest, dass einige seiner digitalen Haustiere erschöpft wirken. Er möchte schnell erkennen können, welche Pokopettos eine Pause benötigen.

Mit Vererbung können wir für beliebige Pokopettos, also egal welcher konkreten Unterklasse, bestimmte Eigenschaften prüfen.

Aufgabe:

  • Setze in der Klasse Pokopetto eine neue statische Methode:

    public static int numberOfExhaustedPokopettos( Pokopetto... pokopettos ) {
      // Liefert die Anzahl erschöpfter Pokopettos zurück,
      // die der Methode übergeben wurden
    }

    Für die Implementierung kann man auf die isExhausted()-Methode zurückgreifen, die für jede Art von Pokopetto existiert.

  • Teste die Methode in PokopettoDemo etwa so:

    Pokopetto p1 = ...   // erschöpftes Pokopetto aufbauen
    Pokopetto p2 = ...   // erschöpftes Pokopetto aufbauen
    Pokopetto p3 = ...   // energiegeladenes Pokopetto aufbauen
    int exhausted = Pokopetto.numberOfExhaustedPokopettos( p1, p2, p3 );

    Wenn p1 und p2 zwei erschöpfte Pokopettos sind und p3 ein nicht erschöpftes Pokopetto, soll die Methode die Rückgabe 2 liefern.

1.6.5. Spezielle Interaktionen mit Pokopetto-Arten (NEU) ⭐

Während seiner Pokopetto-Forschungen bemerkt Captain CiaoCiao, dass HyperPokopettos andere Erschöpfungsanzeichen zeigen als gewöhnliche Pokopettos.

Aufgabe:

  • Schließe bei der Zählmethode Pokopetto.numberOfExhaustedPokopettos(…​) explizit alle Objekte vom Typ HyperPokopetto aus.

  • Teste die Funktionalität in PokopettoDemo.

1.6.6. Gleichwertigkeitstest mit Pattern-Variable lösen ⭐

Java 14 führt das Pattern matching for instanceof ein, das Code angenehm verkürzen kann.

Aufgabe:

  • Gegeben ist die ältere Klasse Toaster mit einer equals(…​)-Methode für einen Test auf Gleichwertigkeit:

    Listing 1. com/tutego/exercise/oop/Toaster.java
    public class Toaster {
      int capacity;
      boolean stainless;
      boolean extraLarge;
    
      @Override public boolean equals( Object o ) {
        if ( !(o instanceof Toaster) ) return false;
    
        Toaster toaster = (Toaster) o;
        return    capacity == toaster.capacity
               && stainless == toaster.stainless && extraLarge == toaster.extraLarge;
      }
    
      @Override public int hashCode() {
        return Objects.hash( capacity, stainless, extraLarge );
      }
    }
  • Schreibe die equals(…​)-Methode so um, dass instanceof mit einer Pattern-Variable verwendet wird.

1.6.7. HyperPokopetto kennt keine Müdigkeit (NEU) ⭐

Während Captain CiaoCiao seine verschiedenen Pokopetto-Arten studiert, macht er eine erstaunliche Entdeckung: HyperPokopettos scheinen niemals müde zu werden! Egal, wie viel sie spielen, ihre Energie bleibt konstant hoch.

Aufgabe:

  • Die Methode play() soll bei HyperPokopetto überschrieben werden, sodass sich der mood-Wert nicht verringert, denn HyperPokopettos werden beim Spielen einfach nicht müde. hunger und age sollen wie üblich aktualisiert werden.

  • Auch die Methode isTired() soll überschrieben werden und immer false zurückgeben.

  • Zum Testen setze Folgendes in PokopettoDemo ein:

    HyperPokopetto poko = new HyperPokopetto();
    System.out.println( poko.getMood() );    // 100
    poko.play();
    System.out.println( poko.getMood() );    // immer noch 100
    System.out.println( poko.isTired() );    // false

1.6.8. Refactoring der HyperPokopetto play()-Methode (NEU) ⭐⭐

Captain CiaoCiao möchte seinen Code eleganter gestalten. Er hat bemerkt, dass die überschriebene play()-Methode in HyperPokopetto viel Code aus der Oberklasse dupliziert. Als erfahrener Programmierer weiß er: Code-Duplikation sollte vermieden werden.

Aufgabe:

  • Komprimiere die play()-Methode in der Klasse HyperPokopetto, indem die Implementierung der Oberklasse über einen super-Aufruf wiederverwendet wird.

  • Damit kann die Methode weiterhin den Hunger wie gewohnt erhöhen (über die Oberklassen-Implementierung), und der mood-Wert wird beim Aufruf von play() nicht gesenkt, sondern bleibt beim Ausgangswert konstant (Spezialverhalten der Unterklasse).

1.7. Polymorphie und dynamisches Binden

Eine zentrale Eigenschaft von objektorientierten Programmiersprachen ist die dynamische Auflösung von Methodenaufrufen. Diese Form von Aufrufen kann dabei nicht zur Compilezeit entschieden werden, sondern zur Laufzeit, wenn der Objekttyp bekannt ist.

1.7.1. Polymorphie beim Spielen (NEU) ⭐

Captain CiaoCiao experimentiert gerne: Er möchte beim Start des Spiels zufällig ein normales oder ein hyperaktives Pokopetto erstellt bekommen. So kann er besser die verschiedenen Verhaltensweisen studieren.

Aufgabe:

  • Implementiere die Pokopetto-Erzeugung so, dass mit 50 % Wahrscheinlichkeit entweder ein DefaultPokopetto oder ein HyperPokopetto erstellt wird.

  • Untersuche das polymorphe Verhalten, was sich durch diese Zeile ergibt:

    case "play" -> pokopetto.play();  // <-- Hier findet Polymorphie statt

    Der Aufruf von play() verhält sich unterschiedlich, je nachdem, ob es sich um ein normales Pokopetto handelt (wird müde vom Spielen) oder um ein HyperPokopetto (bleibt immer energiegeladen).

    • Setze einen Breakpoint in die play()-Methode der DefaultPokopetto und HyperPokopetto-Klasse.

    • Starte das Spiel mehrmals und beobachte, welche play()-Implementierung aufgerufen wird.

1.7.2. Pokopetto-Steckbriefe erstellen (NEU) ⭐

Captain CiaoCiao möchte für seine Pokopetto-Sammlung einen illustrierten Katalog erstellen. Seine Crew liebt ASCII-Art, daher soll für jedes Pokopetto in der Liste ein passendes Symbol mit Status angezeigt werden.

Aufgabe:

  • Kopiere die folgende Klasse PokopettoArt als geschachtelte Klasse in die Klasse PokopettoGame:

    public static class PokopettoArt {
      public static final String ENERGETIC = """
          \\(^o^)/
           |===|
           /   \\""";
    
      public static final String TIRED = """
          (-.-)
          |===|
          /   \\""";
    
      public static final String HUNGRY = """
          (o.o)
          |===|
          /   \\""";
    
      public static final String HYPER = """
          *\\(^O^)/*
           =====
           /   \\""";
    }
  • Implementiere ein neues Kommando catalog, das für alle Pokopettos in der Liste das passende ASCII-Art ausgibt. Die Logik ist wie folgt:

    • Für HyperPokopetto wird immer HYPER ausgegeben.

    • Für DefaultPokopetto wird je nach Zustand ausgegeben: bei isExhausted() die TIRED Zeichenfolge, bei isStarving() die HUNGRY Zeichenfolge, sonst ENERGETIC. Es hat die Rückgabe von isExhausted() Vorrang vor isStarving().

  • Nutze Pattern Matching for switch, um den Typ und Zustand zu prüfen.

1.8. Abstrakte Klassen und abstrakte Methoden

Abstrakte Klassen sind auf den ersten Blick etwas Komisches: Was soll man mit Klassen, von denen man keine Objekte bilden kann? Und was ist mit abstrakten Methoden? Eine Klasse ohne implementierte Methoden hat doch nichts zu bieten!

Beide Konzepte sind essenziell. Obertyp und Untertyp haben immer einen Vertrag; ein Untertyp muss mindestens das besitzen, was ein Obertyp vorschreibt, und darf die Semantik nicht brechen. Ist eine Oberklasse abstrakt bzw. sind Methoden abstrakt, so geben die Unterklassen, von denen man Objekte bilden kann, ein Versprechen ab, diese Funktionalität bereitzustellen.

1.8.1. Pokopetto als abstrakte Klasse? (NEU) ⭐

Captain CiaoCiao grübelt über seine Pokopetto-Architektur. Er stellt sich folgende Fragen:

  • Ergibt es Sinn, mit new Pokopetto() ein "reines" Pokopetto Objekt zu erstellen? Oder sollte jedes konkete Pokopetto von einer speziellen Art sein (Default oder Hyper)?

  • Welche Konsequenzen hätte es, wenn die Klasse Pokopetto abstract wäre?

    • Für die Erstellung neuer Pokopettos?

    • Für die bestehenden Methoden?

    • Für die Vererbungshierarchie?

1.8.2. Pokopetto-Persönlichkeiten (NEU) ⭐

Captain CiaoCiao bemerkt, dass jedes Pokopetto eine einzigartige Persönlichkeit entwickelt und sich beim Spielen anders verhält.

Aufgabe:

  • Mache die Klasse Pokopetto abstrakt und füge eine abstrakte Methode String getPersonality() hinzu.

    • Gibt es Compilerfehler? Wenn ja, warum?

  • Die Persönlichkeit soll sich wie folgt entwickeln:

    • Bei DefaultPokopetto soll die Persönlichkeit vom Alter abhängen: Bis Alter 5: schüchtern ("shy"), bis Alter 15: verspielt ("playful"), ab Alter 15: erwachsen ("mature").

    • Bei HyperPokopetto soll immer die Persönlichkeit "energetic" zurückgegeben werden.

  • Erweitere die toString()-Methode in der Basis-Klasse, dass ein Fragment wie + " and has a " + getPersonality() + " personality" ergänzt wird. Ist das ein Beispiel für einen polymorphen Aufruf?

Pokopetto Abstract UML
Abbildung 8. UML-Diagramm der abstrakten Pokopetto-Klasse mit Persönlichkeiten

1.8.3. TimerTask als Beispiel für eine abstrakte Klasse ⭐⭐

Captain CiaoCiao nimmt jeden Überfall per Video auf und analysiert die Abläufe in der Nachbesprechung. Allerdings füllen die Videos in feinster 8K-Qualität schnell die Festplatte. Er möchte rechtzeitig feststellen, ob er eine neue Festplatte kaufen muss.

TimerTask as an example for an abstract class

Ein java.util.Timer kann Aufgaben wiederholt durchführen. Dazu hat die Timer-Klasse eine Methode schedule(…​) zum Hinzufügen einer Aufgabe. Die Aufgabe ist vom Typ java.util.TimerTask.

Aufgabe:

  • Schreibe eine Unterklasse von TimerTask, die immer dann eine Meldung auf dem Bildschirm ausgibt, wenn die Anzahl freier Bytes auf dem Dateisystem unter eine gewisse Grenze fällt (z. B. weniger als 1000 MB).

    Die freien Bytes liefert:

    long freeDiskSpace = java.io.File.listRoots()[0].getFreeSpace();
  • Der Timer soll diesen TimerTask alle 2 Sekunden ausführen.

FreeDiskSpaceTimerTask UML
Abbildung 9. UML-Diagramm von TimerTask und eigener Unterklasse

Bonus: Integriere eine Nachricht in der Tray mit:

import javax.swing.*;
import java.awt.*;
import java.awt.TrayIcon.MessageType;
import java.net.URL;
try {
  String url =
    "https://cdn4.iconfinder.com/data/icons/common-toolbar/36/Save-16.png";
  ImageIcon icon = new ImageIcon( new URL( url ) );
  TrayIcon trayIcon = new TrayIcon( icon.getImage() );
  SystemTray.getSystemTray().add( trayIcon );

  trayIcon.displayMessage( "Warning", "Hard drive full", MessageType.INFO );
}
catch ( Exception e ) { e.printStackTrace(); }

1.9. Aufgaben aus 1. Auflage

1.9.1. Radio mit Objektvariablen und ein Hauptprogramm deklarieren ⭐

Der erste Typ unserer Sammlung von Elektrogeräten ist ein Radio. Ein Radio hat einen Zustand, den wir speichern wollen.

Aufgabe:

  • Lege eine neue Klasse Radio an.

  • Gib dem Radio die folgenden Objektvariablen:

    • isOn, ist das Radio an oder aus?

    • volume, wie laut spielt das Radio Musik ab?

  • Welche Variablentypen sind sinnvoll? Achte darauf, dass die Objektvariablen nicht statisch sind!

  • Schreibe zusätzlich eine Klasse Application, die ein Radio-Objekt in ihrer main(…​)-Methode aufbaut. Belege und erfrage zum Test die Variablen.

1.9.2. Methoden eines Radios implementieren ⭐

In die neue Klasse Radio sollen Methoden gesetzt werden, damit das Objekt etwas »kann«.

Aufgabe:

  • Ergänze folgende nichtstatische Methoden:

    • void volumeUp()/void volumeDown(): verändern die Objektvariable volume um 1 bzw. -1. Die Lautstärke darf nur im Bereich von 0 bis 100 liegen.

    • void on()/ void off()/ boolean isOn(): Greifen auf die Objektvariable isOn zurück; es ist in Ordnung, wenn eine Methode so heißt wie eine Objektvariable. Die Methoden on()/off() sollen Meldungen wie "an"/"aus" auf dem Bildschirm ausgeben.

    • public String toString(): Sie soll Informationen über den internen Zustand als String zurückgeben, wobei die Zeichenkette eine Form wie Radio[volume=2, is on] annehmen sollte.

  • In der main(…​)-Methode der Klasse Application können die Objektmethoden des Radios zum Beispiel so getestet werden:

    Listing 2. Ausschnitt aus Application.java
    Radio grandmasOldRadio = new Radio();
    System.out.println( grandmasOldRadio.isOn() );     // false
    grandmasOldRadio.on();
    System.out.println( grandmasOldRadio.isOn() );     // true
    System.out.println( grandmasOldRadio.volume );     // 0
    grandmasOldRadio.volumeUp();
    grandmasOldRadio.volumeUp();
    grandmasOldRadio.volumeDown();
    grandmasOldRadio.volumeDown();
    grandmasOldRadio.volumeDown();
    grandmasOldRadio.volumeDown();
    grandmasOldRadio.volumeDown();
    grandmasOldRadio.volumeDown();
    grandmasOldRadio.volumeDown();
    grandmasOldRadio.volumeDown();
    grandmasOldRadio.volumeDown();
    grandmasOldRadio.volumeDown();
    grandmasOldRadio.volumeDown();
    grandmasOldRadio.volumeUp();
    System.out.println( grandmasOldRadio.volume );     // 2
    System.out.println( grandmasOldRadio.toString() ); // Radio[volume=2, is on]
    System.out.println( grandmasOldRadio );            // Radio[volume=2, is on]
    grandmasOldRadio.off();

1.9.3. Private Parts: Objektvariablen privat machen ⭐

Aufgabe:

  • Mache alle Objektvariablen aus Radio privat.

  • Überlege, ob die Methoden public werden können.

  • Gibt es interne Methoden, die private sein sollten?

1.9.4. Setter und Getter anlegen ⭐

Aufgabe:

  1. Gib dem Radio eine neue private double-Objektvariable frequency, sodass man das Radio auf eine Frequenz einstellen kann.

  2. Passe die toString()-Methode so an, dass sie die Frequenz berücksichtigt.

  3. Das Schreiben dieser Setter und Getter ist oft langweilig, daher werden sie entweder über eine Entwicklungsumgebung automatisch generiert oder mithilfe von Werkzeugen automatisch in den Bytecode gesetzt. Generiere mithilfe der IDE Setter und Getter für die Frequenz.

  4. Es gibt Getter ohne Setter, wenn man Nur-lesen-Operationen realisieren möchte und verhindern will, dass Properties von außen verändert werden. Wenn eine Variable final ist, funktionieren auch nur Getter. Generiere für den Zustand volume nur einen Getter.

1.9.5. Sendernamen in Frequenzen konvertieren ⭐

Aufgabe:

  • Implementiere in die Klasse Radio eine statische Methode double stationNameToFrequency(String), die einem Sender als Zeichenkette eine Frequenz zuordnet (zum Beispiel hat der bekannte Piratensender »Walking the Plank« die Frequenz 98.3).

  • Wird der Methode null übergeben, dann soll die Rückgabe 0.0 sein. Auch bei unbekannten Sendernamen soll die Rückgabe 0.0 sein.

Beispiel:

  • Im Hauptprogramm können wir schreiben:

    System.out.println( Radio.stationNameToFrequency( "Walking the Plank" ) ); // 98.3

Stringvergleiche mit den Sendern können mit switch-case oder mit equals(…​) realisiert werden.

1.9.6. Logausgaben mit einer Tracer-Klasse schreiben ⭐

Man verwendet Logger, um Programmausgaben zu protokollieren und sie später nachvollziehen zu können — ganz ähnlich wie Captain CiaoCiao in seinem Logbuch festhält, was alles auf den Meeren, in den Häfen und innerhalb der Besatzung passiert ist.

Tracer

Aufgabe:

  1. Lege eine neue Klasse Tracer an.

  2. Ergänze eine statische Methode void trace(String), die einen an sie überreichten String auf dem Bildschirm ausgibt.

  3. Erweitere das Programm um zwei statische Methoden on() und off(), die sich in einem internen Zustand merken, ob trace(String) zu einer Ausgabe führt oder nicht. Am Anfang soll der Tracer abgeschaltet sein.

  4. Optional: Füge eine Methode trace(String format, Object... args) hinzu, die intern auf System.out.printf(format, args) geht, wenn das Tracing eingeschaltet ist.

Beispiel:

Wir können die Klasse dann so verwenden:

Tracer.on();
Tracer.trace( "Start" );
int i = 2;
Tracer.off();
Tracer.trace( "i = " + i );
//  Tracer.trace( "i = %d", i );
Tracer.on();
Tracer.trace( "End" );

Die erwartete Ausgabe ist:

Start
End
Tracer static UML
Abbildung 10. UML-Diagramm mit statischen Eigenschaften

1.9.7. Radio eine AM-FM-Modulation geben ⭐

Bei Radioübertragungen ist die Modulation wichtig; es gibt AM (Amplitudenmodulation) und FM (Frequenzmodulation)[1].

Aufgabe:

  • Deklariere einen neuen Aufzählungstyp Modulation mit den Werten AM und FM als eigene Datei.

  • Füge in Radio eine private Objektvariable Modulation modulation ein, in der sich das Radio die Modulation merkt.

  • Setze die Modulation über eine neue Radio-Methode void setModulation(Modulation modulation) um, einen Getter kann es auch geben.

  • Passe die toString()-Methode in Radio an.

1.9.8. Anlegevarianten: Radio-Konstruktoren schreiben ⭐

Aufgabe:

  • Schreibe einen Konstruktor für die Klasse Radio, sodass man ein Radio mit einer Frequenz (double) initialisieren kann. Man sollte Radios aber immer noch mit dem parameterlosen Konstruktor anlegen können!

  • Alternativ soll ein Radio-Objekt mit einem Sender (als String) initialisiert werden können (nutze dazu intern stationNameToFrequency(…​)). Der Sendername wird nicht gespeichert, nur die Frequenz.

  • Wie können wir die Konstruktorweiterschaltung mit this(…​) nutzen?

Beispiel: Auf folgende Weise soll man Radios aufbauen können:

Radio r1 = new Radio();
Radio r2 = new Radio( 102. );
Radio r3 = new Radio( "BFBS" );

1.9.9. Copy-Konstruktor implementieren ⭐

Aufgabe:

  • Implementiere für Radio einen Copy-Konstruktor.

1.9.10. Fabrikmethoden realisieren ⭐

Aufgabe:

  1. Lege eine neue Klasse TreasureChest für eine Schatztruhe an.

  2. Eine Schatztruhe kann Golddublonen und Edelsteine enthalten; lege zwei öffentliche finale Objektvariablen int goldDoubloonWeight und int gemstoneWeight an. Das Objekt ist also immutable, die Zustände lassen sich später nicht mehr ändern. Getter sind nicht nötig.

  3. Schreibe vier statische Fabrikmethoden, die ein TreasureChest-Objekt liefern:

    • TreasureChest newInstance()

    • TreasureChest newInstanceWithGoldDoubloonWeight(int)

    • TreasureChest newInstanceWithGemstoneWeight(int)

    • TreasureChest newInstanceWithGoldDoubloonAndGemstoneWeight(int, int)

Wo wäre hier das Problem mit einem üblichen Konstruktor?

1.9.11. Bildröhre mit Fernsehgerät verbinden ⭐

Bisher haben wir ein Elektrogerät: Radio. Es wird Zeit, ein zweites Elektrogerät hinzuzunehmen und eine 1:1-Assoziation einzubauen.

Aufgabe:

  • Lege eine neue Klasse TV an.

  • Das Fernsehgerät soll Methoden on()/off() bekommen, die kurze Meldungen auf die Konsole schreiben (eine Objektvariable ist für das Beispiel erst einmal nicht nötig).

  • Lege in der main(…​)-Methode von Application ein TV an.

  • Lege eine neue Klasse MonitorTube (Bildröhre) an.

    • Die MonitorTube soll ebenfalls on()/off()-Methoden mit Konsolenmeldungen bekommen.

  • Ein TV soll eine MonitorTube über eine private Objektvariable referenzieren. Wie kann das in Java Quellcode aussehen?

    • Implementiere eine unidirektionale Beziehung zwischen dem Fernsehgerät und der Bildröhre. Zum Lebenszyklus: Wenn das Fernsehgerät aufgebaut wird, soll auch die Bildröhre mit erzeugt werden, man muss die Bildröhre nicht auswechseln können.

  • Wenn das Fernsehgerät ein-/ausgeschaltet wird, so soll auch die Bildröhre ein-/ausgeschaltet werden.

  • Optional: Wie können wir eine bidirektionale Beziehung implementieren? Wo könnte ein Problem lauern?

Am Ende soll diese Relation implementiert werden:

TV has MonitorTube UML
Abbildung 11. UML-Diagramm der gerichteten Assoziation

1.9.12. Radios mit einer 1:n-Assoziation auf das Schiff aufnehmen ⭐⭐

Captain CiaoCiao besitzt eine ganze Flotte von Schiffen, und sie können Ladung aufnehmen. Am Anfang möchte Captain CiaoCiao nur Radios auf sein Schiff laden.

Add radio to ship

Aufgabe:

  1. Lege eine neue Klasse Ship (ohne main(…​)-Methode) an.

  2. Damit das Ship Radios aufnehmen kann, greifen wir auf die Datenstruktur java.util.ArrayList zurück. Als private Objektvariable kommt in Ship:

    ArrayList<Radio> radios = new ArrayList<Radio>();
  3. Schreibe in Ship eine neue load(…​)-Methode, damit das Schiff ein Radio aufnehmen kann.

  4. Baue in der main(…​)-Methode von Application zwei Schiffe auf.

  5. Weise einem Ship in der main(…​)-Methode mehrere Radios zu.

  6. Schreibe eine Ship-Methode int countDevicesSwitchedOn(), die liefert, wie viele Radios eingeschaltet sind. Achtung: Es geht nicht um die Gesamtanzahl Radios auf dem Schiff, sondern um die Anzahl eingeschalteter Radios!

  7. Optional: Gib dem Schiff ebenfalls eine toString()-Methode.

  8. Was müssen wir tun, wenn das Schiff auch andere Elektrogeräte laden möchte, etwa Eismaschinen oder Fernsehgeräte?

Ziel der Implementierung: Ein Schiff referenziert Radios.

Ship has Radio UML
Abbildung 12. UML-Diagramm der 1:n-Assoziation

1.9.13. Abstraktion in Elektrogeräte über Vererbung einführen ⭐

Bisher waren Radios und Fernsehgeräte ohne Verbindung. Doch es gibt eine Gemeinsamkeit: Sie sind alle elektronische Konsumgeräte.

Aufgabe:

  1. Lege eine neue Klasse ElectronicDevice für Elektrogeräte an.

  2. Leite die Klasse Radio von der Klasse ElectronicDevice ab — TV lassen wir erst einmal außen vor.

  3. Ziehe in die Oberklasse die Gemeinsamkeiten der möglichen Elektrogeräte.

  4. Schreibe eine neue Klasse IceMachine, die ebenfalls ein Elektrogerät ist.

Eine Entwicklungsumgebung kann heutzutage über ein Refactoring automatisch Eigenschaften in die Oberklasse verschieben; finde heraus, wie das geht.

Ziel der Aufgabe: Implementierung der folgenden Vererbungsbeziehung.

Radio IceMachine is a ElectronicDevice UML
Abbildung 13. UML-Diagramm der Vererbungsbeziehung

1.9.14. Anzahl eingeschalteter Elektrogeräte ermitteln ⭐

Mit Vererbung lässt sich erreichen, dass ein Parameter mit einem Obertyp deklariert wird, der dann eine ganze Gruppe von Typen damit anspricht, nämlich auch alle Untertypen.

Aufgabe:

  • Setze in der Klasse ElectronicDevice eine statische Methode:

    public static int numberOfElectronicDevicesSwitchedOn( ElectronicDevice... devices ) {
      // Liefert die Anzahl eingeschalteter Geräte zurück,
      // die der Methode übergeben wurden
    }

Beispiel:

  • Wenn etwa r1 und r2 zwei eingeschaltete Radios sind und ice eine ausgeschaltete Eismaschine, kann in main(…​) zum Beispiel stehen:

    int switchedOn =
      ElectronicDevice.numberOfElectronicDevicesSwitchedOn( r1, ice, r2 );
    System.out.println( switchedOn ); // 2

1.9.15. Schiff soll jedes Elektrogerät aufnehmen ⭐

Das Schiff kann bisher nur den Typ Radio speichern. Nun sollen allgemeine Elektrogeräte gespeichert werden.

Aufgabe:

  1. Ändere den Typ der dynamischen Datenstruktur von Radio in ElectronicDevice:

    private ArrayList<ElectronicDevice> devices =
        new ArrayList<ElectronicDevice>();
  2. Auch die Methode zum Hinzufügen ändert sich — wieso und warum?

Ziel der Aufgabe: Schiffe speichern alle Arten von Elektrogeräten.

Ship has ElectronicDevice UML
Abbildung 14. UML-Diagramm

1.9.16. Funktionierende Radios auf das Schiff nehmen ⭐

Captain CiaoCiao will den GEZ-Gott nicht erzürnen, und so soll bei der Aufnahme von Radios auf das Schiff eine Meldung auf der Konsole erscheinen. Außerdem mag Captain CiaoCiao keine kaputten Radios auf das Schiff aufnehmen.

Aufgabe:

  • Wenn der Hinzufügemethode load(…​) ein Radio übergeben wird, dann soll geprüft werden, ob es die Lautstärke 0 hat; in dem Fall soll es nicht in die Datenstruktur aufgenommen werden.

  • Wenn ein Radio hinzugefügt wird, soll folgende Konsolenausgabe erfolgen: Remember to pay license fee!.

1.9.17. Feuermelder geht nicht aus: Überschreiben von Methoden ⭐

Feuer ist etwas, was Captain CiaoCiao auf seinen Schiffen überhaupt nicht gebrauchen kann. Wenn es brennt, muss es schnellstmöglich gelöscht werden.

Aufgabe:

  • Implementiere eine Klasse Firebox für einen Feuermelder als Unterklasse von ElectronicDevice.

  • Feuermelder sollen nach dem Erzeugen immer eingeschaltet sein.

  • Die Methode off() soll mit leerem Rumpf oder mit Konsolenausgabe implementiert werden, sodass sich ein Feuermelder nicht ausschalten lässt.

Beispiel:

  • Ziel der Aufgabe: eine überschriebene off()-Methode, die den Zustand isOn nicht ändert. Das lässt sich so testen:

    Firebox fb = new Firebox();
    System.out.println( fb.isOn() );  // true
    fb.off();
    System.out.println( fb.isOn() );  // true
Firebox is ElectronicDevice
Abbildung 15. UML-Diagramm

1.9.18. toString() überschreiben ⭐

Gib ElectronicDevice und Radio eine eigene toString()-Methode.

1.9.19. Aufruf der Methoden der Oberklasse ⭐⭐

Ein Radio hat Methoden on()/off(), und auch die TV-Klasse hat schon Methoden on()/off(). Allerdings ist das TV noch kein ElectronicDevice. Der Grund ist, dass das Fernsehgerät wegen der Bildröhre (MonitorTube) eine Sonderbehandlung benötigt.

Erweitert auch TV die Klasse ElectronicDevice, überschreibt ein Fernsehgerät somit die Methoden der Oberklasse ElectronicDevice. Es ergibt sich aber ein Problem:

  • Wenn wir die beiden Methoden weglassen, würde die Röhre nicht ausgeschaltet werden, das Fernsehgerät aber bei Vererbung als Elektrogerät durchgehen.

  • Wenn wir die Methoden in der Klasse lassen, wird nur die Röhre ausgeschaltet, das Gerät wird aber nicht mehr ein- oder ausgeschaltet. Diesen Zustand verwaltete ja die Oberklasse über die Methoden on()/off().

Aufgabe:

  • Löse das Problem, dass ein TV ein ElectronicDevice ist, aber die Bildröhre ein-/ausgeschaltet wird.

1.9.20. Urlaub! Alle Geräte ausschalten ⭐

Bevor sich Captain CiaoCiao mit einem Tropical Storm-Cocktail in die Hängematte legt und seinen Urlaub genießt, müssen alle Elektrogeräte auf dem Schiff ausgeschaltet werden.

Holiday Switch off all devices

Aufgabe:

  • Implementiere in der Ship-Klasse eine Methode holiday(), die alle Elektrogeräte der Liste ausschaltet.

    public void holiday() {
      // rufe off() für alle Elemente in der Datenstruktur auf
    }
  • In der main(…​)-Methode von Application kann zum Beispiel stehen:

    Radio bedroomRadio = new Radio();
    bedroomRadio.volumeUp();
    Radio cabooseRadio = new Radio();
    cabooseRadio.volumeUp();
    TV mainTv = new TV();
    Radio crRadio = new Radio();
    Firebox alarm = new Firebox();
    Ship ship = new Ship();
    ship.load( bedroomRadio );
    ship.load( cabooseRadio );
    ship.load( mainTv );
    ship.load( crRadio );
    ship.load( alarm );
    ship.holiday();

1.9.21. Der große Umzug ⭐

Der furchtlose Captain CiaoCiao hat beschlossen, sein altes Schiff zu verlassen und auf einen frischen Kahn umzusteigen. Da nicht alle Piratenkameraden des Lesens mächtig sind, soll eine grafische Ladeliste erstellt werden, in der kleine Bildchen die verschiedenen Gegenstände darstellen.

Aufgabe:

  • Kopiere die folgende Klasse AsciiArt als geschachtelte Klasse in die Klasse Ship:

    public static class AsciiArt {
      public static final String RADIO = " .-.\n|o.o|\n|:_:|";
      public static final String BIG_TV = """
          .---..--------------------------------------..---.
          |   ||.------------------------------------.||   |
          |.-.|||                                    |||.-.|
          | o |||                                    ||| o |
          |`-'|||                                    |||`-'|
          |.-.|||                                    |||.-.|
          | O |||                                    ||| O |
          |`-'||`------------------------------------'||`-'|
          `---'`--------------------------------------'`---'
                 _||_                            _||_
                /____\\                          /____\\""";
      public static final String TV = " \\  /\n _\\/_\n|    |\n|____|";
      public static final String SOCKET = """
                ____
           ____|    \\
          (____|     `._____
           ____|       _|___
          (____|     .'
               |____/""";
    }
  • Implementiere in Ship eine neue Methode printLoadingList(), die über alle Geräte des Schiffs iteriert und bei der Ausgabe die folgenden Regeln umsetzt:

    • Wenn das Gerät ein Radio ist und die Wattzahl positiv ist, wird ein Radio auf dem Bildschirm ausgegeben durch Zugriff auf AsciiArt.RADIO. Kaputte Radios haben 0 Watt und dürfen nicht ausgegeben werden.

    • Ist das Gerät ein TV und liegt die Wattzahl über 10.000, wird das Bild eines großen Fernsehers (AsciiArt.BIG_TV) ausgegeben.

    • Wenn das Gerät ein TV ist (unabhängig von der Wattzahl), wird das Bild eines normalen Fernsehers (AsciiArt.TV) ausgegeben.

    • Wenn keines der oben genannten Fälle zutrifft, wird das Bild einer Steckdose (AsciiArt.SOCKET) ausgegeben.

  • Löse die Aufgabe mit dem Sprachfeature Pattern Matching for switch.


1. Die beiden Modulationsverfahren AM und FM unterscheiden sich darin, wie man mit einer Trägerfrequenz ein zu übertragendes Signal kodiert. Wikipedia gibt unter https://de.wikipedia.org/wiki/Modulation_%28Technik%29 eine Übersicht.