1. Records, Schnittstellen, Aufzählungen, Versiegelte Klassen

1.1. Records

Records sind in Java nützlich, weil sie eine einfache und kompakte Möglichkeit bieten, Datenobjekte mit einer festen Anzahl von Attributen zu definieren und zu verwenden.

1.1.1. Record für Komplexe Zahlen entwickeln ⭐

Komplexe Zahlen sind eine wichtige mathematische Konzeption, die in verschiedenen Bereichen wie Ingenieurwesen, Physik und Informatik verwendet werden. Eine komplexe Zahl besteht aus einem Realteil und einem Imaginärteil. Sie wird in der Form a + bi geschrieben, wobei dann a der Realteil und b der Imaginärteil ist. Die mathematischen Operationen werden wie folgt realisiert:

  • Addition und Subtraktion: Die Operation erfolgt elementweise, d. h., der Realteil/Imaginärteil einer Zahl wird mit dem Realteil/Imaginärteil der anderen Zahl addiert/subtrahiert.

  • Multiplikation: Die Operation erfolgt gemäß der Regel: (a + bi) × (c + di) = (ac - bd) + (ad + bc)i.

  • Betrag einer komplexen Zahl: Der Betrag von z = a + bi ist definiert als |z| = sqrt(a2 + b2).

Aufgabe:

  • Schreibe ein neues Record Complex mit den Record-Komponenten real und imaginary.

  • Lege eine Konstante I an, die die imaginäre Einheit repräsentiert, also die Quadratwurzel aus -1.

  • Implementiere die Methoden add(Complex other), subtract(Complex other), multiply(Complex other) und abs().

  • Überschreibe die toString()-Methode, die Real- und Imaginärteil auf drei Stellen hinter dem Komma rundet, sodass die Ausgabe zum Beispiel (-3,000 + 2,000i) oder (-2,000 - 3,000i) wird.

1.1.2. Record Patterns ⭐

Die tollkühnen Piraten haben nicht nur einen ausgeprägten Sinn für Beute, sondern auch eine Vorliebe für exotische Haustiere. Bisher repräsentiert eine Anwendung Haustiere in zwei Records:

record MischiefMonkey( String name, boolean isMutinous ) { }
record FeistyParrot( String name, String favoritePhrase, boolean isMutinous ) { }

Die Modellierung hat sich als starr erwiesen, wenn neue Tiertypen dazukommen und Eigenschaften sich ändern. Daher sollen die Daten aus den Records in java.util.Properties-Objekte konvertiert werden. Properties ist ein besonderer Assoziativspeicher, der einen String mit einem anderen String assoziiert. Die Methode setProperty(String key, String value) setzt ein neues Schlüssel-Werte-Paar in das Properties-Objekt.

Aufgabe:

  • Schreibe eine Methode Properties convertToProperties(Object), um Haustiere des Typs MischiefMonkey und FeistyParrot in Properties-Objekte umzuwandeln.

  • Haustiere, die meuterisch sind (Zustand isMutinous) sollen ignoriert werden und zu einem leeren Properties Objekt ohne Einträge führen.

  • Die Methode sollte mit null oder unbekannten Typen aufgerufen werden können und in den Fällen ein leeres Properties-Objekt zurückgeben.

Beispiele (Ausgabe zeigt die toString()-Repräsentation von Properties):

  • new MischiefMonkey( "Jack", true ){}

  • new FeistyParrot( "Captain Squawk", "Avast, ye scallywags!", false ){favoritePhrase=Avast, ye scallywags!, name=Captain Squawk}

  • new MischiefMonkey( "Barbossa", false ){name=Barbossa}

  • new FeistyParrot( "Polly", "Pieces of eight!", true ){}

  • new FeistyParrot( "Marauder", "Walk the plank!", false ){favoritePhrase=Walk the plank!, name=Marauder}

1.2. Schnittstellen

Abstrakte Klassen sind immer noch Klassen mit all ihren Möglichkeiten: Objektvariablen, Konstruktoren, Methoden, unterschiedliche Sichtbarkeiten. Oftmals reicht eine einfachere Form der Vorschrift, und hierfür bietet Java Schnittstellen. Sie haben keine Objektvariablen, können aber Konstanten, abstrakte Methoden, statische Methoden und default-Methoden haben — eine Objektvariable ist eine Art, wie man etwas speichert, was zur Klasse gehört, nicht zur Schnittstelle.

1.2.1. Verbrauch von Elektrogeräten vergleichen ⭐

Jedes Elektrogerät hat eine Leistung, die in Watt gemessen wird.

Aufgabe Teil 1:

  1. Deklariere in ElectronicDevice eine private int-Objektvariable watt, und generiere mit der Entwicklungsumgebung Setter/Getter.

  2. Ergänze eine toString()-Methode, die etwa Folgendes zurückgibt: "ElectronicDevice[watt=12kW]". Einige Unterklassen hatten schon toString() überschrieben; sie sollten dann in ihrem toString()-Methoden ein super.toString() enthalten.

Aufgabe Teil 2:

  1. Schreibe eine neue Klasse ElectronicDeviceWattComparator, die die Schnittstelle java.util.Comparator<ElectronicDevice> implementiert.

  2. Die compare(…​)-Methode soll eine Ordnung der Elektrogeräte definieren, wobei ein Elektrogerät »kleiner« ist, wenn es weniger Leistung aufnimmt.

  3. Setzte zum besseren Verständnis ein println(…​) in die eigene compare(…​)-Methode, sodass zu sehen ist, welche Objekte verglichen werden.

Beispiel:

ElectronicDevice ea1 = new Radio(); ea1.setWatt( 200 );
ElectronicDevice ea2 = new Radio(); ea2.setWatt( 20 );
Comparator<ElectronicDevice> c = new ElectronicDeviceWattComparator();
System.out.println( c.compare(ea1, ea2) );
System.out.println( c.compare(ea2, ea1) );

Ziel der Aufgabe: ElectronicDeviceWattComparator als Implementierung der Schnittstelle Comparator, wie im UML-Diagramm gezeigt:

ElectronicDeviceWattComparator UML
Figure 1. UML-Diagramm

1.2.2. Elektrogeräte mit dem höchsten Verbrauch finden ⭐

Die Klasse java.util.Collections hat eine statische Methode, die das größte Element einer Sammlung liefert (die Generics in den spitzen Klammern wurden für die einfache Darstellung entfernt):

static T max( Collection coll, Comparator comp )

Übergeben werden müssen der max(…​)-Methode folglich

  1. eine Collection-Implementierung wie ArrayList und

  2. eine Comparator-Implementierung. Hier lässt sich unser ElectronicDeviceWattComparator verwenden.

Aufgabe:

  • Setze in das Schiff eine Methode findMostPowerConsumingElectronicDevice(), die das Gerät mit dem größten Verbrauch liefert.

Beispiel:

  • Folgendes Programm liefert die Ausgabe 12000.

    Radio grannysRadio = new Radio();
    grannysRadio.volumeUp();
    grannysRadio.setWatt( 12_000 );
    
    TV grandpasTv = new TV();
    grandpasTv.setWatt( 1000 );
    
    Ship ship = new Ship();
    ship.load( grannysRadio );
    ship.load( grandpasTv );
    System.out.println( ship.findMostPowerConsumingElectronicDevice().getWatt() );

1.2.3. Schnittstelle Comparator zum Sortieren einsetzen ⭐

Möchte man Objekte einer Liste sortieren, kann man die sort(…​)-Methode auf List-Objekten nutzen. Wichtig ist, der sort(…​)-Methode mitzuteilen, wann ein Objekt etwa »kleiner« ist als ein anderes. Dazu lässt sich unser ElectronicDeviceWattComparator verwenden; er ist Voraussetzung für Objekte, die man sortieren möchte — das verrät auch schon die Signatur void sort(Comparator<…​> c).

Aufgabe:

  • Rufe in load(…​) des Ship-Objekts nach dem Hinzufügen in die eigene Datenstruktur sort(…​) auf, um nach dem Hinzufügen immer eine interne sortierte Liste zu haben.

1.2.4. Statische und Default-Methoden in Schnittstellen ⭐⭐⭐

Schnittstellen können statische Methoden enthalten und als Fabrikmethoden dienen, also Instanzen von Klassen liefern, die diese Schnittstelle implementieren.

Aufgabe:

  1. Lege eine Schnittstelle Distance an.

  2. Setze in Distance zwei statische Methoden Distance ofMeter(int value) und Distance ofKilometer(int value), die ein neues Objekt vom Typ Distance liefern.

  3. Setze in Distance eine abstrakte Methode int meter(). Was muss man implementieren?

  4. Setze eine Default-Methode int kilometer() in die Schnittstelle Distance.

Beispiel in der Nutzung:

Distance oneKm = Distance.ofKilometer( 1 );
System.out.printf( "1 km = %d km, %d m%n", oneKm.kilometer(), oneKm.meter() );

Distance moreMeter = Distance.ofMeter( 12345 );
System.out.printf( "12345 m = %d km, %d m", moreMeter.kilometer(), moreMeter.meter() );

1.2.5. Ausgewählte Elemente mit Predicate löschen ⭐⭐

Wollen wir ein Schiff energieeffizient gestalten, müssen wir alle Geräte mit einem zu hohen Verbrauch entfernen.

Die List-Methode removeIf(Predicate<…​>filter) löscht alle Elemente, die ein Prädikat erfüllen. Die Klasse ArrayList ist eine Implementierung der Schnittstelle List, also steht die Methode bei einer ArrayList zur Verfügung.

Möchten wir z. B. aus einer List<String> aller leeren Strings löschen, können wir auf der Liste removeIf(new IsStringEmpty()) aufrufen, wobei IsStringEmpty wie folgt deklariert ist:

class IsStringEmpty implements Predicate<String> {
  @Override public boolean test( String t ) {
    return t.trim().isEmpty();
  }
}
Find electronic devices with the highest power consumption

Aufgabe:

  • Setze in das Schiff eine neue Methode removePowerConsumingElectronicDevices(), die alle Geräte mit einer Leistungsaufnahme größer einer selbstgewählten Konstanten MAXIMUM_POWER_COMSUMPTION löscht.

1.3. Aufzählungstypen (enum)

Aufzählungstypen (enum) repräsentieren abgeschlossene Mengen und sind in Java recht leistungsfähig; Sie erlauben nicht nur zusätzliche Objekt- und Klassenvariablen, neue private Konstruktoren, sondern können auch Schnittstellen implementieren, Methoden überschreiben, und sie haben einige Standardmethoden. Die kommenden Aufgaben adressieren diese schönen Möglichkeiten.

1.3.1. Aufzählung für Süßwaren ⭐

Captain CiaoCiao will eine jüngere Käuferschicht ansprechen und experimentiert statt mit Rum in seinem Labor mit Süßwaren.

Enumeration for candy

Aufgabe:

  • Deklariere eine Aufzählung CandyType mit Konstanten für

    • Caramels

    • Chocolate

    • Gummies

    • Licorice

    • Lollipops

    • Chewing Gums

    • Cotton Candy

  • Achte auf die übliche Namenskonvention.

  • Benutzer sollen von der Konsole eine Süßware eingeben können. Für die Eingabe soll das passende enum-Objekt gesucht werden, die Groß-/Kleinschreibung soll keine Rolle spielen. Führe für die Umwandlung vom String in ein Aufzählungselement vom Typ CandyType eine neue Methode static Optional<CandyType> fromName(String input) im Aufzählungstyp CandyType ein. Ausnahmen durch falsche Eingaben darf die Methode nicht auslösen; unbekannte Namen führen zu einem Optional.empty().

CandyType Enum UML
Figure 2. UML-Diagramm vom Aufzählungstyp (ohne Methode)

1.3.2. Zufällige Süßwaren liefern ⭐

Captain CiaoCiao startet seine Verköstigungstour und wählt immer zufällige Süßwaren aus.

Deliver random candies

Aufgabe:

  • Gib dem Aufzählungstyp CandyType eine Methode random(), die eine zufällige Süßware liefert.

    System.out.println( CandyType.random() );  // z. B. CHOCOLATE
    System.out.println( CandyType.random() );  // z. B. LOLLIPOPS
CandyType Enum random UML
Figure 3. UML-Diagramm des Aufzählungstyps mit statischen Methoden

1.3.3. Süßwaren mit Suchtfaktor auszeichnen ⭐⭐

Wir wissen, dass Süßwaren süchtig machen, manche mehr, manche weniger.

Aufgabe:

  • Verbinde mit jedem Aufzählungselement aus CandyType einen Suchtfaktor (int):

    • Caramels: 9

    • Chocolate: 5

    • Gummies: 4

    • Licorice: 3

    • Lollipops: 2

    • Chewing Gums: 3

    • Cotton Candy: 1

    Zur Speicherung des Suchtfaktors nutze einen Konstruktor im enum. Den Suchtfaktor soll eine neue nichtstatische Methode addictiveQuality() liefern.

  • Da Captain CiaoCiao eine Abhängigkeit in Richtung Süßwaren mit größerem Suchtfaktor erreichen möchte, soll eine neue CandyType-Methode next() die Süßware mit der nächsthöheren Abhängigkeit liefern. Lollipops hat zwei potenzielle Nachfolger, hier soll die Auswahl zufällig auf Chewing Gums und Licorice gehen. Caramels hat keinen »Nachfolger«, und es bleibt bei Caramels.

Beispiele:

  • CandyType.COTTON_CANDY.next() ist LOLLIPOPS.

  • CandyType.LOLLIPOPS.next() ist z. B. LICORICE.

  • CandyType.LOLLIPOPS.next() ist z. B. CHEWING_GUMS.

  • CandyType.CARAMELS.next() ist CARAMELS.

AddictiveQualityCandy Enum UML
Figure 4. UML-Diagramm vom Aufzählungstyp

1.3.4. Bewertung bei der Kanonenkugelprüfung (NEU) ⭐

Bald steht für die Piratencrew die gefürchtete Kanonenkugelprüfung an. Jeder Schuss wird bewertet, und aus der Treffgenauigkeit wird eine Prozentzahl ermittelt. Basierend darauf wird eine Note und eine Bewertung nach folgender Tabelle festgelegt:

Table 1. Notentabelle
BewertungProzenteNote

sehr gut

≥ 95 bis < 100

1,0

sehr gut

≥ 90 bis 95

1,3

gut

≥ 85 bis < 90

1,7

gut

≥ 80 bis < 85

2,0

gut

≥ 75 bis < 80

2,3

befriedigend

≥ 70 bis < 75

2,7

befriedigend

≥ 65 bis < 70

3,0

befriedigend

≥ 60 bis < 65

3,3

ausreichend

≥ 55 bis < 60

3,7

ausreichend

≥ 50 bis < 55

4,0

nicht ausreichend

< 50

5,0

Gegeben ist ein Aufzählungstyp Grade mit folgenden Konstanten für die Noten:

enum Grade {
  _5_0, _4_0, _3_7, _3_3, _3_0, _2_7, _2_3, _2_0, _1_7, _1_3, _1_0;

  static Grade fromPercentage( double percentage ) { … }
}

Aufgabe:

  • Implementiere die Methode fromPercentage(…​), die eine Prozentzahl (nur im Bereich 0 bis 100) als Parameter bekommt und das entsprechende Aufzählungselement liefert.

  • Schreibe einen switch-Ausdruck, der für alle möglichen Aufzählungselemente die entsprechende Bewertung auf dem Bildschirm ausgibt. Zum Beispiel soll Grade.fromPercentage(49.999) die Ausgabe Nicht ausreichend liefern.

1.3.5. Schnittstellen-Implementierungen über ein enum ⭐⭐

Ein Aufzählungstyp kann Schnittstellen implementieren, aber keine Klassen erweitern.

Gegeben ist eine Schnittstelle Distance:

interface Distance {
  double distance( double x1, double y1, double x2, double y2 );
  double distance( double x1, double y1, double z1, double x2, double y2, double z2 );
}

Aufgabe:

  • Übernimm die Schnittstelle Distance in das eigene Projekt.

  • Deklariere einen Aufzählungstyp Distances, der Distance implementiert mit genau einem Aufzählungselement EUCLIDEAN:

    enum Distances implements Distance {
      EUCLIDEAN
    }

    Wer nun eine Distance-Implementierung für den euklidischen Abstand benötigt, kann sie über Distances.EUCLIDEAN bekommen.

  • Füge die Implementierung hinzu, dass der euklidische Abstand von zwei Punkten berechnet wird; zur Erinnerung, für einen 2D-Punkt:

    Math.sqrt( (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) )
  • Erweitere den Aufzählungstyp Distances um ein weiteres Aufzählungselement MANHATTAN, damit es zwei Konstanten EUCLIDEAN und MANHATTAN gibt.
    Die Manhattan-Distanz bildet sich aus der Summe der absoluten Differenzen der Einzelkoordinaten, für einen 2D-Punkt also Math.abs(x1 - x2) + Math.abs(y1 - y2).