I am currently working on an English translation. If you like to help to proofread please contact me: ullenboom ät g m a i l dot c o m.

Java Videotraining 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 noch 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 geht es um elektronische Konsumgeräte, und die meisten Aufgaben bauen aufeinander auf. Dabei werden wir erst einfache Elektrogeräte wie Radios und Fernsehgeräte aufbauen; später werden wir Abstraktionen realisieren und diese Elektrogeräte auf dem Schiff sammeln. Und wenn Captain CiaoCiao und Bonny Brain in Urlaub fahren, muss alles schön ausgeschaltet werden. Auf diese Weise können die Themen Assoziationen und Vererbung geübt werden. Zur Erinnerung: Eine Assoziation nennen wir umgangssprachlich Hat- oder Kennt-Beziehung; eine Vererbung ist eine Ist-eine-Art-von-Beziehung.

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. 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.

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. 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. (Optional: Die Lautstärke soll 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 1. 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.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.1.3. Private Parts: Objektvariablen privat machen ⭐

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

Aufgabe:

  • Mache alle Objektvariablen aus Radio privat.

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

  • Gibt es interne Methoden, die private sein sollten?

1.1.4. Setter und Getter anlegen ⭐

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:

  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.

Setter und Getter sind eine wichtige Namenskonvention. Ist eine Property XXX vom Typ boolean, so ist das Präfix im allgemeinen isXXX(), nicht getXXX(). Unsere existierende Methode isOn() ist also auch ein Getter.

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. Sendernamen in Frequenzen konvertieren ⭐

Bisher hat das Radio nur Objekteigenschaften. Ergänzen wir eine statische Methode, die keine Beziehung zu einem konkreten Radio-Objekt hat.

Aufgabe:

  • Implementiere in der Klasse Radio eine neue 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.2.2. 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.

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 1. UML-Diagramm mit statischen Eigenschaften

1.3. Aufzählungen

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

1.3.1. 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.3.2. Gültige Start- und Endfrequenz bei Modulation setzen ⭐

Für Rundfunk werden drei Frequenzbereiche (Frequenzband genannt) unterschieden, die über AM kodieren:

  • Langwelle: 148,5 kHz bis 283,5 kHz

  • Mittelwelle: 526,5 kHz bis 1606,5 kHz

  • Kurzwelle: Kurzwellenrundfunk nutzt mehrere Bänder zwischen 3,2 MHz und 26,1 MHz.

Über FM kodiert:

  • Ultrakurzwelle (UKW): 87,5 MHz bis 108 MHz

Aufgabe:

  • Füge zwei neue private Objektvariablen ein:

    • minFrequency

    • maxFrequency

  • Beim Aufruf von setModulation(Modulation) sollen die Objektvariablen minFrequency und maxFrequency auf ihre minimalen und maximalen Wertebereiche gesetzt werden, nämlich für AM 148,5 kHz bis 26,1 MHz und für FM 87,5 MHz bis 108 MHz.

1.4. Konstruktoren

Konstruktoren sind besondere Initialisierungsroutinen, die beim Anlegen eines Objektes 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. Anlegevarianten: Radio-Konstruktoren schreiben ⭐

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

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.4.2. Copy-Konstruktor implementieren ⭐

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

Aufgabe:

  • Implementiere für Radio einen Copy-Konstruktor.

1.4.3. Fabrikmethoden realisieren ⭐

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

  • Es gibt prinzipiell Konstruktoren, aber die sind 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.

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 drei 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.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. 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 2. UML-Diagramm der gerichteten Assoziation

1.5.2. 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.

Aufgabe:

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

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

  3. 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>();
  4. Weise einem Ship in der main(…​)-Methode in Application mehrere Radios zu. Wie kommen Radios auf das Schiff?

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

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

  7. 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 3. UML-Diagramm der 1:n-Assoziation

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. 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 4. UML-Diagramm der Vererbungsbeziehung

1.6.2. 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.6.3. 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 5. UML-Diagramm

1.6.4. 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: "Radio wurde hinzugefügt, schon GEZahlt?".

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

Feuer ist etwas, was Captain CiaoCiao auf seinen Schiffen überhaupt nicht gebrauchen kann. Wenn es brennt, muss 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.UML
Abbildung 6. UML-Diagramm

1.6.6. toString() überschreiben ⭐

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

1.6.7. 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 aber die Bildröhre ein-/ausgeschaltet wird.

1.7. Polymorphie und dynamisches Binden

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

1.7.1. Urlaub! Alle Geräte ausschalten ⭐

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

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.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 sehr wichtig. 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. 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.

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 7. 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( "Achtung", "Platte voll", MessageType.INFO );
}
catch ( Exception e ) { e.printStackTrace(); }

1.9. 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.9.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
Abbildung 8. UML-Diagramm

1.9.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.0.

    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.9.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(…​) vom Ship nach dem Hinzufügen in die eigene Datenstruktur sort(…​) auf, um nach dem Hinzufügen immer eine interne sortierte Liste zu haben.

1.9.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.9.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();
  }
}

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. 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.