Rheinwerk Computing < openbook >


 
Inhaltsverzeichnis
Materialien zum Buch
Vorwort
1 Java ist auch eine Sprache
2 Imperative Sprachkonzepte
3 Klassen und Objekte
4 Arrays und ihre Anwendungen
5 Der Umgang mit Zeichen und Zeichenketten
6 Eigene Klassen schreiben
7 Objektorientierte Beziehungsfragen
8 Schnittstellen, Aufzählungen, versiegelte Klassen, Records
9 Ausnahmen müssen sein
10 Geschachtelte Typen
11 Besondere Typen der Java SE
12 Generics<T>
13 Lambda-Ausdrücke und funktionale Programmierung
14 Architektur, Design und angewandte Objektorientierung
15 Java Platform Module System
16 Die Klassenbibliothek
17 Einführung in die nebenläufige Programmierung
18 Einführung in Datenstrukturen und Algorithmen
19 Einführung in grafische Oberflächen
20 Einführung in Dateien und Datenströme
21 Einführung ins Datenbankmanagement mit JDBC
22 Bits und Bytes, Mathematisches und Geld
23 Testen mit JUnit
24 Die Werkzeuge des JDK
A Java SE-Module und Paketübersicht
Stichwortverzeichnis


Buch bestellen
Ihre Meinung?



Spacer
<< zurück
Java ist auch eine Insel von Christian Ullenboom

Einführung, Ausbildung, Praxis
Buch: Java ist auch eine Insel


Java ist auch eine Insel

Pfeil 8 Schnittstellen, Aufzählungen, versiegelte Klassen, Records
Pfeil 8.1 Schnittstellen
Pfeil 8.1.1 Schnittstellen sind neue Typen
Pfeil 8.1.2 Schnittstellen deklarieren
Pfeil 8.1.3 Abstrakte Methoden in Schnittstellen
Pfeil 8.1.4 Implementieren von Schnittstellen
Pfeil 8.1.5 Ein Polymorphie-Beispiel mit Schnittstellen
Pfeil 8.1.6 Die Mehrfachvererbung bei Schnittstellen
Pfeil 8.1.7 Keine Kollisionsgefahr bei Mehrfachvererbung *
Pfeil 8.1.8 Erweitern von Interfaces – Subinterfaces
Pfeil 8.1.9 Konstantendeklarationen bei Schnittstellen
Pfeil 8.1.10 Nachträgliches Implementieren von Schnittstellen *
Pfeil 8.1.11 Statische ausprogrammierte Methoden in Schnittstellen
Pfeil 8.1.12 Erweitern und Ändern von Schnittstellen
Pfeil 8.1.13 Default-Methoden
Pfeil 8.1.14 Erweiterte Schnittstellen deklarieren und nutzen
Pfeil 8.1.15 Öffentliche und private Schnittstellenmethoden
Pfeil 8.1.16 Erweiterte Schnittstellen, Mehrfachvererbung und Mehrdeutigkeiten *
Pfeil 8.1.17 Bausteine bilden mit Default-Methoden *
Pfeil 8.1.18 Markierungsschnittstellen *
Pfeil 8.1.19 (Abstrakte) Klassen und Schnittstellen im Vergleich
Pfeil 8.2 Aufzählungstypen
Pfeil 8.2.1 Methoden auf Enum-Objekten
Pfeil 8.2.2 Aufzählungen mit eigenen Methoden und Initialisierern *
Pfeil 8.2.3 enum mit eigenen Konstruktoren *
Pfeil 8.3 Versiegelte Klassen und Schnittstellen
Pfeil 8.3.1 Versiegelte Klassen und Schnittstellen (sealed classes/interfaces)
Pfeil 8.3.2 Unterklassen sind final, sealed, non-sealed
Pfeil 8.3.3 Abkürzende Schreibweisen
Pfeil 8.4 Records
Pfeil 8.4.1 Einfache Records
Pfeil 8.4.2 Records mit Methoden
Pfeil 8.4.3 Konstruktoren von Records anpassen
Pfeil 8.4.4 Konstruktoren ergänzen
Pfeil 8.4.5 Versiegelte Schnittstellen und Records
Pfeil 8.4.6 Zusammenfassung
Pfeil 8.5 Zum Weiterlesen
 

Zum Seitenanfang

8    Schnittstellen, Aufzählungen, versiegelte Klassen, Records Zur vorigen ÜberschriftZur nächsten Überschrift

»Die Bildung kommt nicht vom Lesen, sondern vom Nachdenken über das Gelesene.«

– Carl Hilty (1833–1909)

Klassen sind die wichtigsten Typen der Sprache Java, aber nicht die einzigen Typen; es gibt zudem Schnittstellen, Aufzählungstypen, Records und Annotationstypen. Annotationstypen sind ein Thema von Reflection und spielen für uns erst einmal keine Rolle, über die anderen drei Typen handelt dieses Kapitel.

 

Zum Seitenanfang

8.1    Schnittstellen Zur vorigen ÜberschriftZur nächsten Überschrift

Schnittstellen sind eine gute Ergänzung zu abstrakten Klassen/Methoden, denn im objektorientierten Design wollen wir das »Was« vom »Wie« trennen. Abstrakte Methoden sagen wie Schnittstellen etwas über das »Was« aus, aber erst die konkreten Implementierungen realisieren das »Wie«.

 

Zum Seitenanfang

8.1.1    Schnittstellen sind neue Typen Zur vorigen ÜberschriftZur nächsten Überschrift

Da Java nur Einfachvererbung kennt, ist es schwierig, Klassen mehrere Typen zu geben. Da es aber möglich sein soll, dass in der objektorientierten Modellierung eine Klasse mehrere Typen annimmt, gibt es das Konzept der Schnittstelle (engl. interface). Eine Klasse kann dann von einer Klasse erben und eine beliebige Anzahl Schnittstellen implementieren und auf diese Weise weitere Typen annehmen.

Eine Schnittstelle ist wie eine Klasse ein Typ und hat viele Gemeinsamkeiten, nur die Intention ist eine andere. Eine Schnittstelle kann enthalten:

  • abstrakte Methoden

  • private und öffentliche konkrete Methoden (sogenannte Default-Methoden)

  • private und öffentliche statische Methoden

  • Konstanten (also statische Variablen)

  • geschachtelte Typen, wie Aufzählungen

Eine Schnittstelle darf keinen Konstruktor deklarieren. Das ist auch klar, da Exemplare von Schnittstellen nicht erzeugt werden können, sondern nur von den konkreten implementierenden Klassen. Auch kann eine Schnittstelle keine Objektvariablen deklarieren, jede Variable ist automatisch eine Klassenvariable.

Klassenvererbung ist immer linear, etwa so: Candy erbt von Object, Workout erbt von Event, Event erbt von Object; von den Unterklassen gibt es einen direkten Pfad zu den Oberklassen. Da es in Java keine Mehrfachvererbung gibt, können wir nicht an einer Stelle zum Beispiel sagen, dass ein Candy ein Object ist, aber zusätzlich den Typ Buyable annehmen soll, weil es auch einen Preis haben kann. Eine Klasse kann nicht von mehreren Typen erben, das geht durch die Einfachvererbung nicht. Das ist ein Nachteil, weil es in der Praxis schon wichtig ist, dass eine Klasse unter verschiedenen »Sichten« auftreten soll, hier helfen Schnittstellen.

 

Zum Seitenanfang

8.1.2    Schnittstellen deklarieren Zur vorigen ÜberschriftZur nächsten Überschrift

Die Deklaration einer Schnittstelle erinnert an eine abstrakte Klasse, nur steht anstelle von class das Schlüsselwort interface:

interface Buyable {

}

Die Schnittstelle kann nun von Klassen implementiert werden.

 

Zum Seitenanfang

8.1.3    Abstrakte Methoden in Schnittstellen Zur vorigen ÜberschriftZur nächsten Überschrift

Die wichtigsten Elemente in Schnittstellen sind abstrakte Methoden. Wir kennen das schon von abstrakten Klassen: Eine abstrakte Methode hat keine Implementierung, sondern deklariert nur den Kopf einer Methode (also Modifizierer), den Rückgabetyp und die Signatur (ohne Rumpf). Deklariert wird also nur eine Vorschrift – die Implementierung einer Objektmethode übernimmt später eine Klasse.[ 167 ](Oder ein Lambda-Ausdruck, doch dazu später mehr in Kapitel 13, »Lambda-Ausdrücke und funktionale Programmierung« )

Sollen in einem Spiel gewisse Dinge käuflich sein, haben sie einen Preis. Eine Schnittstelle Buyable soll allen Klassen die Methode price() vorschreiben:

Listing 8.1     src/main/java/com/tutego/insel/game/i/v1/Buyable.java, Buyable

interface Buyable {

double price();

}

Da Objektmethoden in Schnittstellen standardmäßig abstrakt und öffentlich sind, können die Modifizierer abstract und public entfallen und sind redundant. Die von den Schnittstellen deklarierten Operationen sind – wie auch bei abstrakten Methoden – mit einem Semikolon abgeschlossen. Eine Implementierung ist möglich, wie wir später sehen werden.

[»]  Hinweis

Der Name einer Schnittstelle endet oft auf -ble (Accessible, Adjustable, Runnable). Er beginnt üblicherweise nicht mit einem Präfix wie »I«, obwohl die Eclipse-Entwickler diese Namenskonvention nutzen und sie auch in .NET üblich ist.

 

Zum Seitenanfang

8.1.4    Implementieren von Schnittstellen Zur vorigen ÜberschriftZur nächsten Überschrift

Möchte eine Klasse eine Schnittstelle verwenden, so folgt hinter dem Klassennamen das Schlüsselwort implements und dann der Name der Schnittstelle. Die Ausdrucksweise ist dann: »Klassen werden vererbt und Schnittstellen implementiert.«

Nehmen wir an, ein Spieler kann für seine Reise ein Fahrrad kaufen. Fahrräder sind käuflich und sollen die Schnittstelle Buyable implementieren (siehe Abbildung 8.1). Jedes Fahrrad soll dabei immer einen Einheitspreis von 199 haben.

Listing 8.2     src/main/java/com/tutego/insel/game/i/v1/Bike.java, Bike

public class Bike implements Buyable {

@Override public double price() {

return 199;

}

}

Die Annotation @Override zeigt wieder eine überschriebene Methode (hier die implementierte Methode einer Schnittstelle) an.

[»]  Hinweis

Sind die in Schnittstellen deklarierten Operationen public, müssen auch die implementierten Methoden in den Klassen immer öffentlich sein. protected ist als Sichtbarkeit nicht erlaubt. Und private Schnittstellenmethoden sind in implementierenden Klassen sowieso nicht sichtbar.

Bike ist eine Klasse, die keine explizite Oberklasse hat, also Object erweitert. Das ist ein Indiz dafür, dass es kein Problem ist, wenn eine Klasse eine andere Klasse erweitert und zusätzlich eine Schnittstelle implementiert.

Ein Museumsbesuch ist ein Ereignis und käuflich:

Listing 8.3     src/main/java/com/tutego/insel/game/i/v1/MuseumVisit.java, MuseumVisit

class MuseumVisit extends Event implements Buyable {

int price;

MuseumVisit( int price ) { this.price = price; }



@Override public double price() {

return price;

}

}

Es ist also kein Problem – und bei uns so gewünscht –, wenn eine Klasse eine andere Klasse erweitert und zusätzlich Operationen aus Schnittstellen implementiert. Ein kleiner Hinweis: Wir nutzen eine sehr einfache Implementierung für die Ereignisse, abstract class Event {}, da es auf Zustände oder abstrakte Methoden in den folgenden Beispielen nicht ankommt.

UML-Diagramm der Schnittstelle »Buyable« und Implementierungen »Bike« und »MuseumVisit«

Abbildung 8.1     UML-Diagramm der Schnittstelle »Buyable« und Implementierungen »Bike« und »MuseumVisit«

Es gelten dann folgende Typbeziehungen (die sich auch mit instanceof testen lassen):

  • Event ist ein Event.

  • Event ist ein Object.

  • MuseumVisit ist ein MuseumVisit.

  • MuseumVisit ist ein Event.

  • MuseumVisit ist ein Object.

  • MuseumVisit ist ein Buyable.

  • Bike ist ein Bike.

  • Bike ist ein Buyable.

  • Bike ist ein Object.

Fordert eine Methode ein Objekt eines gewissen Typs, haben wir viele Möglichkeiten:

Methode fordert Typ

ein gültiger Argumenttyp ist

Object

Object, Event, MuseumVisit, Bike, Buyable

Event

Event, MuseumVisit

Buyable

Buyable, Bike, MuseumVisit

Bike

Bike

MuseumVisit

MuseumVisit

Tabelle 8.1     Was Methoden bekommen können, wenn sie gewisse Typen fordern

Wir lesen ab: Wenn ein konkreter Typ wie Bike oder MuseumVisit gefordert ist, haben wir wenig Optionen. Bei Basistypen gibt es üblicherweise immer mehrere Varianten – wer wenig will, kann eben viel bekommen.

Implementiert eine Klasse nicht alle Operationen aus den Schnittstellen, so erbt sie damit abstrakte Methoden und muss selbst wieder als abstrakt gekennzeichnet werden.

inline image  IntelliJ zeigt bei der Tastenkombination (Strg)+(H) eine Typhierarchie an.

 

Zum Seitenanfang

8.1.5    Ein Polymorphie-Beispiel mit Schnittstellen Zur vorigen ÜberschriftZur nächsten Überschrift

Obwohl Schnittstellen auf den ersten Blick nichts »bringen« – Programmierer wollen gerne etwas vererbt bekommen, damit sie Implementierungsarbeit sparen können –, sind sie eine enorm wichtige Erfindung. Über Schnittstellen lassen sich ganz unterschiedliche Sichten auf ein Objekt beschreiben. Jede Schnittstelle ermöglicht eine neue Sicht auf das Objekt, eine Art Rolle. Implementiert eine Klasse diverse Schnittstellen, können ihre Exemplare in verschiedenen Rollen auftreten. Hier wird erneut das Substitutionsprinzip wichtig, bei dem ein mächtiges Objekt zum Beispiel als Argument einer Methode verwendet wird, obwohl je nach Kontext der Parametertyp einer Methode nur die kleine Schnittstelle ist.

Mit Bike oder MuseumVisit haben wir zwei Klassen, die Buyable implementieren. Damit existieren zwei Klassen, die einen gemeinsamen Typ und eine gemeinsame Methode price() besitzen:

Listing 8.4     src/main/java/com/tutego/insel/game/i/v1/Application.java, main

Buyable hercules = new Bike();

Buyable binarium = new MuseumVisit( 8 );

System.out.println( hercules.price() ); // 199.0

System.out.println( binarium.price() ); // 8.0

Für Buyable wollen wir eine statische Methode calculateSum(….) schreiben, die den Preis einer Sammlung zum Kauf stehender Objekte berechnet. Damit calculateSum(…) eine beliebige Anzahl Argumente, aber mindestens eins, annehmen kann, realisieren wir die Methode mit einem Vararg:

Listing 8.5     src/main/java/com/tutego/insel/game/i/v1/PriceUtils.java, PriceUtils

class PriceUtils {



static double calculateSum( Buyable first, Buyable... more ) {

double result = first.price();



for ( Buyable buyable : more )

result += buyable.price();



return result;

}

}

Die Methode nimmt käufliche Dinge an, wobei es ihr völlig egal ist, um welche Buyable-Typen es sich genau handelt. Was zählt, ist die Tatsache, dass die Elemente die Schnittstelle Buyable implementieren.

Die dynamische Bindung tritt schon in der ersten Anweisung, first.price(), auf. Auch später rufen wir auf jedem Objekt, das Buyable implementiert, die Methode price() auf. Indem wir die unterschiedlichen Werte summieren, bekommen wir den Gesamtpreis der Elemente aus der Parameterliste.

Sie soll wie folgt aufgerufen werden:

Listing 8.6     src/main/java/com/tutego/insel/game/i/v1/PriceUtilsDemo.java, main

Bike hercules = new Bike();

MuseumVisit binarium = new MuseumVisit( 8 );

Buyable winora = new Bike();

Buyable mimomenta = new MuseumVisit( 12 );

double sum = PriceUtils.calculateSum( hercules, binarium, winora, mimomenta );

System.out.println( sum ); // 418.0
[+]  Tipp

Wie schon erwähnt, sollte der Typ einer Variablen immer der kleinste nötige sein. Dabei sind Schnittstellen als Variablentypen nicht ausgenommen. Entwickler, die alle ihre Variablen vom Typ einer Schnittstelle deklarieren, wenden das Konzept Programmieren gegen Schnittstellen an. Sie binden sich also nicht an eine spezielle Implementierung, sondern an einen Basistyp.

Im Zusammenhang mit Schnittstellen bleibt zusammenfassend zu sagen, dass hier bei Methodenaufrufen dynamisches Binden pur auftaucht.

 

Zum Seitenanfang

8.1.6    Die Mehrfachvererbung bei Schnittstellen Zur vorigen ÜberschriftZur nächsten Überschrift

Eine Klasse kann höchstens eine Basisklasse haben – egal, ob sie abstrakt ist oder nicht. Der Grund ist, dass Mehrfachvererbung zu dem Problem führen kann, dass eine Klasse von zwei Oberklassen die gleiche Methode erbt und dann nicht weiß, welche sie aufnehmen soll. Bei Schnittstellen sieht das anders aus; eine Klasse kann beliebig viele Schnittstellen implementieren. Das liegt daran, dass von einer Schnittstelle kein Code kommt, sondern nur eine Vorschrift zur Implementierung – im schlimmsten Fall gibt es die Vorschrift, eine Operation umzusetzen, mehrfach.

[»]  Begriffe

Wenn einige Entwickler sagen, Java hat Mehrfachvererbung, und die anderen sagen, Java hätte keine Mehrfachvererbung, dann haben beide Parteien recht. Bei Klassenvererbung, auch genannt Implementierungsvererbung, sind keine zwei Oberklassen erlaubt; bei der Schnittstellenvererbung kann eine Klasse sehr wohl mehrere Basistypen haben. Üblicherweise wird der Begriff Mehrfachvererbung in Java nicht verwendet, da er sich traditionell auf Klassenvererbung bezieht.

Schreiben wir eine neue Klasse Flight.

  1. Die Klasse repräsentiert ein Ereignis, daher kann Flight eine Unterklasse von Event sein.

  2. Flüge sind käuflich, die Klasse kann die Schnittstelle Buyable und damit die Methode price() implementieren,

  3. Der Flugpreis eines Fluges soll sich mit Flugpreisen von anderen Flügen vergleichen lassen. Dazu gibt es schon eine passende Schnittstelle in der Java-Bibliothek: java.lang.Comparable. Die Schnittstelle Comparable fordert, dass unser Flight die Methode int compareTo(Flight) implementiert. Der Rückgabewert der Methode zeigt an, wie sich der eine Flugpreis zum anderen Flugpreis verhält. Wir wollen definieren, dass der günstigere Flug »vor« einem teureren steht, und das wird so programmiert, dass, wenn der eigene Flug »kleiner« als der andere ist, compareTo(…) ein negatives Ergebnis liefert, wenn unser Flug »größer«, also teurer ist, compareTo(…) ein positives Ergebnis liefert, sonst 0. Die Methode Double.compare(double, double) hilft uns dabei. (Eigentlich sollten mit Comparable auch equals(…) und hashCode() aus Object überschrieben werden, doch das spart das Beispiel aus.[ 168 ](Wenn compareTo(…) bei zwei gleichen Objekten 0 ergibt, so sollte equals(…) auch true liefern. Doch wird equals(…) nicht überschrieben, so führt die in Object implementierte Methode nur einen Referenzvergleich durch. Bei zwei im Prinzip gleichen Objekten würde die equals(…)-Standardimplementierung also false liefern. Bei hashCode() gilt das Gleiche: Zwei gleiche Objekte müssen auch den gleichen Hashwert haben. Ohne Überschreiben der Methode ist das jedoch nicht gegeben; nur zwei identische Objekte haben den gleichen Hashcode. ))

Daraus folgt:

Listing 8.7     src/main/java/com/tutego/insel/game/i/v1/Flight.java, Flight

class Flight extends Event implements Buyable, Comparable<Flight> {

final double ticketPrice;



Flight( int ticketPrice ) { this.ticketPrice = ticketPrice; }



@Override public double price() {

return ticketPrice;

}



@Override public int compareTo( Flight other ) {

return Double.compare( ticketPrice, other.ticketPrice );

}

}

Die Implementierung nutzt Generics mit Comparable<Magazine>, was wir genauer erst in Kapitel 12, »Generics<T>«, lernen, aber an dieser Stelle schon einmal nutzen wollen. Der Hintergrund ist, dass Comparable dann genau weiß, mit welchem anderen Typ der Vergleich stattfinden soll.

Die Klasse »Flight« mit diversen Obertypen

Abbildung 8.2     Die Klasse »Flight« mit diversen Obertypen

Durch diese »Mehrfachvererbung« bekommt Flight ganz unterschiedliche Typen:

Listing 8.8     src/main/java/com/tutego/insel/game/i/v1/FlightDemo.java, main()

Flight londonToDurban = new Flight( 1200 );

System.out.println( londonToDurban instanceof Flight ); // true

System.out.println( londonToDurban instanceof Event ); // true

System.out.println( londonToDurban instanceof Object ); // true

System.out.println( londonToDurban instanceof Buyable ); // true

System.out.println( londonToDurban instanceof Comparable ); // true

Unser Flight lässt sich jetzt genau dort übergeben, wo ein Flight, Event, Object, Buyable (etwa bei PriceUtils.calculateSum(…)) oder Comparable eingefordert wird.

Das Comparable macht es möglich, zwei Flüge zu vergleichen:

Listing 8.9     src/main/java/com/tutego/insel/game/i/v1/FlightComparison.java, main()

Flight londonToDurban     = new Flight( 1200 );

Flight dortmundToBrussels = new Flight( 200 );

System.out.println( londonToDurban.compareTo( londonToDurban ) ); // 0

System.out.println( londonToDurban.compareTo( dortmundToBrussels ) ); // 1

System.out.println( dortmundToBrussels.compareTo( londonToDurban ) ); //-1

System.out.println( dortmundToBrussels.compareTo( dortmundToBrussels ) );// 0

So wie es der Methode calculateSum(…) egal ist, was für Buyable-Objekte konkret übergeben werden, so gibt es auch für Comparable einen sehr nützlichen Anwendungsfall: das Sortieren. Einem Sortierverfahren ist es egal, was für Objekte genau es sortiert, solange die Objekte sagen, ob sie vor oder hinter einem anderen Objekt liegen:

Listing 8.10     src/main/java/com/tutego/insel/game/i/v1/FlightComparison.java, main()

Flight londonToDurban        = new Flight( 1200 );

Flight dortmundToBrussels = new Flight( 200 );

Flight berlinToNairobi = new Flight( 1500 );

Flight duesseldorfToWindhoek = new Flight( 1400 );

Flight[] flights = {

londonToDurban, dortmundToBrussels, berlinToNairobi, duesseldorfToWindhoek

};

Arrays.sort( flights );

for ( Flight flight : flights )

System.out.print( (int) flight.price() + " "); // 200 1200 1400 1500

Die statische Methode Arrays.sort(…) erwartet ein Array, dessen Elemente Comparable sind. Der Sortieralgorithmus macht Vergleiche über compareTo(…), muss aber sonst über die Objekte nichts wissen. Unsere Flüge mit den Typen Flight, Event, Object, Buyable und Comparable können also sehr flexibel in unterschiedlichen Kontexten eingesetzt werden. Es muss somit für das Sortieren keine Spezialsortiermethode geschrieben werden, die nur Flüge sortieren kann, oder eine Methode zur Berechnung einer Summe, die nur auf Flügen arbeitet. Wir modellieren die unterschiedlichen Anwendungsszenarien mit jeweils unterschiedlichen Schnittstellen, die Unterschiedliches von dem Objekt erwarten.

 

Zum Seitenanfang

8.1.7    Keine Kollisionsgefahr bei Mehrfachvererbung * Zur vorigen ÜberschriftZur nächsten Überschrift

Bei der Mehrfachvererbung von Klassen besteht die Gefahr, dass zwei Oberklassen die gleiche Methode mit zwei unterschiedlichen Implementierungen den Unterklassen vererben. Die Unterklasse wüsste dann nicht, welche Logik sie erbt, also wäre eine spezielle Syntax in Java nötig, die das Dilemma auflösen würde. Das wollen die Sprachdesigner nicht einbauen.

Bei den Schnittstellen gibt es das Problem nicht, denn auch wenn zwei implementierende Schnittstellen die gleiche Operation vorschreiben würden, gäbe es keine zwei verschiedenen Implementierungen von Anwendungslogik. Die implementierende Klasse bekommt sozusagen zweimal die Aufforderung, die Operation zu realisieren. So wie bei folgendem Beispiel: Ein Politiker muss verschiedene Dinge vereinen – er muss sympathisch sein, aber auch durchsetzungsfähig handeln können.

Listing 8.11     src/main/java/com/tutego/insel/oop/Politician.java

interface Likeable {

void act();

}



interface Assertive {

void act();

}



public class Politician implements Likeable, Assertive {

@Override public void act() {

// Implementation

}

}

Zwei Schnittstellen schreiben die gleiche Operation vor. Eine Klasse implementiert diese beiden Schnittstellen und muss beiden Vorgaben gerecht werden (siehe Abbildung 8.3).

Eine Klasse erbt von zwei Schnittstellen die gleiche Operation.

Abbildung 8.3     Eine Klasse erbt von zwei Schnittstellen die gleiche Operation.

[»]  Hinweis

Ein Rückgabetyp gehört in Java nicht zur Signatur einer Methode. Wenn eine Klasse zwei Schnittstellen implementiert und die Signaturen der Operationen aus den Schnittstellen gleich sind, müssen auch die Rückgabetypen gleich sein. Es funktioniert bei der Implementierung nicht, wenn die Signaturen der Methoden aus den Schnittstellen gleich sind (also gleicher Methodenname, gleiche Parameterliste), aber die Rückgabetypen nicht typkompatibel sind. Der Grund ist einfach: Eine Klasse kann nicht zwei Methoden mit gleicher Signatur, aber unterschiedlichen Rückgabetypen implementieren. Würde Assertive ein boolean act() besitzen, müsste Politician dann void act() und boolean act() gleichzeitig realisieren – das geht nicht.

 

Zum Seitenanfang

8.1.8    Erweitern von Interfaces – Subinterfaces Zur vorigen ÜberschriftZur nächsten Überschrift

Ein Subinterface ist die Erweiterung eines anderen Interface. Diese Erweiterung erfolgt – wie bei der Vererbung – durch das Schlüsselwort extends.

interface Disgusting {

double disgustingValue();

}



interface Stinky extends Disgusting {

double olf();

}

Die Schnittstelle modelliert Stinkiges, das besonders abstoßend ist. Zusätzlich soll die Stinkquelle die Stärke der Stinkigkeit in der Einheit Olf angeben. Eine Klasse, die nun Stinky implementiert, muss die abstrakten Methoden aus beiden Schnittstellen implementieren, demnach die Methode disgustingValue() aus Disgusting sowie die Operation olf(), die in Stinky selbst angegeben wurde. Ohne die Implementierung beider Methoden wird eine implementierende Klasse abstrakt sein müssen.

[+]  Tipp

Eine Unterschnittstelle kann eine Operation der Oberschnittstelle »überschreiben«. Auf den ersten Blick ist das nicht sinnvoll, erfüllt aber zwei Zwecke:

  • In der Unterschnittstelle kann die API-Dokumentation präzisiert werden.[ 169 ](Leser können das bei java.util.Collection und java.util.Set einmal nachschauen. )

  • Wegen kovarianter Rückgaben kann eine Operation in der Unterschnittstelle einen spezielleren Rückgabetyp bekommen.

 

Zum Seitenanfang

8.1.9    Konstantendeklarationen bei Schnittstellen Zur vorigen ÜberschriftZur nächsten Überschrift

Schnittstellen können keine Objektvariablen haben und folglich keinen Zustand speichern, aber sie dürfen benannte Konstanten deklarieren.

[zB]  Beispiel

Die Schnittstelle Buyable soll eine Konstante für einen Maximalpreis deklarieren:

interface Buyable {

/* public static final */ int MAX_PRICE = 10_000_000;

double price();

}

Alle Klassenvariablen (Objektvariablen gibt es nicht) einer Schnittstelle sind immer implizit public static final. Das verhindert, dass die Variable neu belegt wird. Die Modifizierer könnten wir setzen, aber da sie implizit sind, wollen wir sie weglassen.

Nur weil die Klassenvariable final ist, verhindert es keine Objektmanipulation. Denn auch wenn die Variablen selbst nach der Initialisierung keine Änderung mehr zulassen, besteht bei mutabel referenzierten Objekten immer noch das Problem, dass eine spätere Änderung an den Objekten möglich ist.

[zB]  Beispiel und Tipp

Die Schnittstelle Volcano referenziert ein veränderbares StringBuilder-Objekt:

interface Volcano {

StringBuilder EYJAFJALLAJÖKULL = new StringBuilder( "Eyjafjallajökull" );

}

Da EYJAFJALLAJÖKULL eine öffentliche StringBuilder-Variable und StringBuilder ein veränderbarer Container ist, modifiziert eine Anweisung wie Volcano.EYJAFJALLAJÖKULL.replace(0, Volcano.EYJAFJALLAJÖKULL.length(), "Vesuvius"); den Inhalt, was der Idee einer Konstanten absolut widerspricht. Besser ist es, immer immutable Objekte zu referenzieren, also etwa Strings. Problematisch sind Arrays, in denen Elemente ausgetauscht werden können, sowie alle veränderbaren Objekte wie StringBuilder, ArrayList.

 

Zum Seitenanfang

8.1.10    Nachträgliches Implementieren von Schnittstellen * Zur vorigen ÜberschriftZur nächsten Überschrift

Implementiert eine Klasse eine bestimmte Schnittstelle nicht, so kann sie auch nicht am dynamischen Binden über diese Schnittstelle teilnehmen, auch wenn sie eine Methode hat, über die eine Schnittstelle abstrahiert. Besitzt zum Beispiel die nichtfinale Klasse FIFA eine öffentliche Methode price(), implementiert aber Buyable mit einer gleich benannten Methode nicht, so lässt sich zu einem Trick greifen: Wir schaffen eine neue Klasse, die die existierende Methode aus der Klasse und die Methode der Schnittstelle in die Typhierarchie bringt.

class FIFA {

public double price() { ... }

}



interface Buyable {

double price();

}



class FIFAisBuyable extends FIFA implements Buyable { }

Eine neue Unterklasse FIFAisBuyable erbt von der Klasse FIFA und implementiert die Schnittstelle Buyable, sodass der Compiler die existierende price()-Methode mit Vorgabe der Schnittstelle vereinigt. Nun lässt sich FIFAisBuyable als Buyable nutzen, und dahinter steckt die Implementierung von FIFA. Als Unterklasse bleiben auch alle sichtbaren Eigenschaften der Oberklasse erhalten. Die Lösung hilft uns allerdings nicht, wenn wir von anderer Stelle ein FIFA-Objekt bekommen.

 

Zum Seitenanfang

8.1.11    Statische ausprogrammierte Methoden in Schnittstellen Zur vorigen ÜberschriftZur nächsten Überschrift

In der Regel deklariert eine Schnittstelle Operationen, also abstrakte Objektmethoden, die eine Klasse später implementieren muss. Die in Klassen implementierte Schnittstellenmethode kann später wieder überschrieben werden, nimmt also ganz normal an der dynamischen Bindung teil. Einen Objektzustand kann die Schnittstelle nicht deklarieren, denn Objektvariablen sind in Schnittstellen tabu – jede deklarierte Variable ist automatisch statisch, also eine Klassenvariable.

In Schnittstellen sind statische Methoden erlaubt und lassen sich als Utility-Methoden neben Konstanten stellen. Es gibt also statische Klassenmethoden und statische Schnittstellenmethoden; beide werden nicht dynamisch gebunden.

[zB]  Beispiel

In Abschnitt 8.1.2 hatten wir eine Schnittstelle Buyable deklariert. Die Idee ist, dass alles, was käuflich ist, diese Schnittstelle implementiert und einen Preis hat. Zusätzlich gibt es eine Konstante für einen Maximalpreis:

interface Buyable {

int MAX_PRICE = 10_000_000;

double price();

}

Hinzufügen lässt sich nun eine statische Methode isValidPrice(double), die prüft, ob sich ein Kaufpreis im gültigen Rahmen bewegt:

interface Buyable {

int MAX_PRICE = 10_000_000;

static boolean isValidPrice( double price ) {

return price >= 0 && price < MAX_PRICE;

}

double price();

}

Von außen ist dann der Aufruf Buyable.isValidPrice(123) möglich.

Alle deklarierten Eigenschaften sind standardmäßig public, können aber seit Java 9 auch privat sein. Konstanten sind implizit immer statisch. Statische Methoden müssen den Modifizierer static tragen, andernfalls gelten sie als abstrakte Methode.

[»]  Hinweis

Statische Schnittstellenmethoden erlauben eine neue Möglichkeit zur Deklaration der main(…)-Methode:

interface HelloWorldInInterfaces {

static void main( String[] args ) {

System.out.println( "Hallo Welt einmal anders!" );

}

}

Das Schlüsselwort interface ist vier Zeichen länger als class, doch mit der Einsparung von public und einem Trenner ergibt sich eine Kürzung von drei Zeichen – wieder eine neue Möglichkeit zum Längefeilschen.

Der Zugriff auf eine statische Schnittstellenmethode ist ausschließlich über den Namen der Schnittstelle möglich, bzw. die Eigenschaften können statisch importiert werden. Bei statischen Methoden von Klassen ist im Prinzip auch der Zugriff über eine Referenz erlaubt (wenn auch unerwünscht), etwa wie bei new Integer(12).MAX_VALUE. Allerdings ist das bei statischen Methoden von Schnittstellen nicht zulässig. Implementiert etwa Car die Schnittstelle Buyable, würde new Car().isValidPrice(123) zu einem Compilerfehler führen. Selbst Car.isValidPrice(123) ist falsch, was doch ein wenig verwundert, da statische Methoden normalerweise vererbt werden.

Fassen wir die erlaubten Eigenschaften einer Schnittstelle zusammen:

Variable

Methode

Objekt-

nein, nicht erlaubt

ja, üblicherweise abstrakt

Klassen-

ja, als Konstante

ja, immer mit Implementierung

Tabelle 8.2     Erlaubte Eigenschaften einer Schnittstelle

Gleich werden wir sehen, dass Schnittstellenmethoden durchaus eine Implementierung besitzen können, also nicht zwingend abstrakt sein müssen.

[»]  Design

Eine Schnittstelle mit nur statischen Methoden ist ein Zeichen für ein Designproblem und sollte durch eine finale Klasse mit privatem Konstruktor ersetzt werden. Schnittstellen sind immer als Vorgaben zum Implementieren gedacht. Wenn nur statische Methoden in einer Schnittstelle vorkommen, erfüllt die Schnittstelle nicht ihren Zweck, Vorgaben zu machen, die unterschiedlich umgesetzt werden können.

 

Zum Seitenanfang

8.1.12    Erweitern und Ändern von Schnittstellen Zur vorigen ÜberschriftZur nächsten Überschrift

Sind Schnittstellen einmal deklariert und in einer großen Anwendung verbreitet, so sind Änderungen nur schwer möglich, da sie schnell die Kompatibilität brechen. Wird der Name einer Parametervariablen geändert, ist das kein Problem. Bekommt aber eine Schnittstelle eine neue Operation, führt das zu einem Compilerfehler, wenn nicht bereits alle implementierenden Klassen diese neue Methode implementieren. Framework-Entwickler müssen also sehr darauf achten, wie sie Schnittstellen modifizieren. Doch sie haben es in der Hand, wie weit die Kompatibilität gebrochen wird.

[»]  Geschichtsstunde

Schnittstellen später zu ändern, wenn schon viele Klassen die Schnittstelle implementieren, ist eine schlechte Idee. Denn erneuert sich die Schnittstelle, etwa wenn nur eine Operation hinzukommt oder sich ein Parametertyp ändert, dann sind plötzlich alle implementierenden Klassen kaputt. Sun selbst riskierte dies bei der Schnittstelle java.sql.Connection. Beim Übergang von Java 5 auf Java 6 wurde die Schnittstelle erweitert, und keine Treiberimplementierung konnte mehr compiliert werden.

Codekompatibilität und Binärkompatibilität *

Fügen wir in einer Schnittstelle eine Konstante (public static final-Variable) ein oder ändern wir den Namen eines Parameters, so ist das für die implementierenden Klassen in Ordnung, und es führt zu keinem Compilerfehler. Wir sprechen in diesem Fall von Änderungen, die codekompatibel sind.

Fügen wir eine neue Operation in eine Schnittstelle ein, führt das sofort zu einem Compilerfehler bei allen implementierenden Klassen. Würden wir jedoch nur die Schnittstelle neu in Bytecode übersetzen, wäre dies zur Laufzeit in Ordnung, denn bekommt eine Schnittstelle eine neue Methode, so ist das für die JVM überhaupt kein Problem. Die Laufzeitumgebung arbeitet auf den Klassendateien selbst, und sie interessiert es nicht, ob eine Klasse brav alle Methoden der Schnittstelle implementiert; sie löst nur Methodenverweise auf. Wenn eine Schnittstelle plötzlich »mehr« vorschreibt, hat die JVM damit kein Problem.

Während also fast alle Änderungen an Schnittstellen zu Compilerfehlern führen, sind einige Änderungen für die JVM in Ordnung. Wir nennen das Binär-Kompatibilität. Wenn zum Beispiel die Schnittstelle verändert, neu übersetzt und in den Modulpfad gesetzt wird, ist Folgendes in Ordnung:

  • neue Methoden in Schnittstelle hinzufügen

  • Die Schnittstelle erbt von einer zusätzlichen Schnittstelle.

  • eine throws-Ausnahme hinzufügen oder löschen

  • den letzten Parametertyp von T[] in T… ändern

  • neue Konstanten, also statische Variablen hinzufügen

Es gibt allerdings Änderungen, die nicht binärkompatibel sind und zu einem JVM-Fehler führen:

  • Ändern des Methodennamens

  • Ändern der Parametertypen und Umsortieren der Parameter

  • einen formalen Parameter hinzunehmen oder entfernen

Strategien zum Ändern von Schnittstellen

Falls die Schnittstelle nicht weit verbreitet wurde, so lassen sich einfacher Änderungen vornehmen. Ist der Name einer Operation zum Beispiel schlecht gewählt, wird ein Refactoring in der IDE den Namen in der Schnittstelle genauso ändern wie auch alle Bezeichner in den implementierenden Klassen. Problematischer ist es, wenn externe Nutzer sich auf die Schnittstelle verlassen. Dann müssen Klienten ebenfalls Anpassungen durchführen, oder Entwickler müssen auf »Schönheitsänderungen« wie das Ändern des Methodennamens einfach verzichten.

Kommen Operationen hinzu, hat sich eine Konvention etabliert, die im Java-Universum oft anzutreffen ist: Soll eine Schnittstelle um Operationen erweitert werden, so gibt es eine neue Schnittstelle, die die alte erweitert und deren Name auf »2« endet; java.awt.LayoutManager2 ist ein Beispiel aus dem Bereich der grafischen Oberflächen, Attributes2, EntityResolver2, Locator2 für XML-Verarbeitung sind weitere.[ 170 ](Ein Blick auf die API des Eclipse-Frameworks zeigt, dass dieses Muster dutzende Male angewendet wurde (http://help.eclipse.org/oxygen/topic/org.eclipse.platform.doc.isv/reference/api/index.html?overviewsummary). )

Default-Methoden sind eine weitere Möglichkeit zur späteren Erweiterung von Schnittstellen. Sie erweitern die Schnittstelle, bringen aber gleich schon eine vorgefertigte Implementierung mit, sodass Unterklassen nicht zwingend eine Implementierung anbieten müssen. Das schauen wir uns jetzt an.

 

Zum Seitenanfang

8.1.13    Default-Methoden Zur vorigen ÜberschriftZur nächsten Überschrift

Ist eine Schnittstelle einmal verbreitet, so sollte es dennoch möglich sein, Operationen hinzuzufügen. Entwicklern sollte es erlaubt sein, neue Operationen einzuführen, ohne dass Unterklassen verpflichtet werden, diese Methoden zu implementieren. Damit das möglich ist, muss die Schnittstelle eine Standardimplementierung mitbringen. Auf diese Weise ist das Problem der »Pflicht-Implementierung« gelöst, denn wenn eine Implementierung vorhanden ist, haben die implementierenden Klassen nichts zu meckern und können bei Bedarf das Standardverhalten überschreiben. Oracle nennt diese Methoden in Schnittstellen mit vordefinierter Implementierung Default-Methoden[ 171 ](Der Name hat sich während der Planung für dieses Feature mehrfach gewandelt. Ganz am Anfang war der Name »defender methods« im Umlauf, dann lange Zeit »virtuelle Erweiterungsmethoden« (engl. virtual extension methods). ). Schnittstellen mit Default-Methoden heißen erweiterte Schnittstellen.

Eine Default-Methode unterscheidet sich syntaktisch in zwei Aspekten von herkömmlichen implizit abstrakten Methodendeklarationen:

  • Die Deklaration einer Default-Methode beginnt mit dem Schlüsselwort default.[ 172 ](Am Anfang sollte default hinter dem Methodenkopf stehen, doch die Entwickler wollten default so wie einen Modifizierer wirken lassen; da Modifizierer aber am Anfang stehen, rutschte auch default nach vorne. Eigentlich ist ein Modifizierer auch gar nicht nötig, denn wenn es eine Implementierung, also einen Codeblock, in {} gibt, ist klar, dass es eine Default-Methode wird. Doch die Entwickler wollten eine explizite Dokumentation, so wie auch abstract eingesetzt wird – auch dieser Modifizierer bei Methoden wäre eigentlich gar nicht nötig, denn es gibt keinen Codeblock, wenn eine Methode abstrakt ist. )

  • Statt eines Semikolons markiert bei einer Default-Methode ein Block mit der Implementierung in geschweiften Klammern das Ende der Deklaration. Die Implementierung wollen wir Default-Code nennen.

Sonst verhalten sich erweiterte Schnittstellen wie normale Schnittstellen. Eine Klasse, die eine Schnittstelle implementiert, erbt alle Operationen, seien es die abstrakten Methoden oder die Default-Methoden. Falls die Klasse nicht abstrakt sein soll, muss sie alle von der Schnittstelle geerbten abstrakten Methoden realisieren. Sie kann die Default-Methoden überschreiben, muss das aber nicht, denn eine Vorimplementierung ist ja schon in der Default-Methode der Schnittstelle gegeben.

[»]  Hinweis

Erweiterte Schnittstellen bringen »Code« in eine Schnittstelle, doch das ging vorher auch schon, indem zum Beispiel eine implizite öffentliche und statische Variable auf eine Realisierung verweist:

interface Comparators {

Comparator<String> TRIM_COMPARATOR = new Comparator<String>() {

@Override public int compare( String s1, String s2 ) {

return s1.trim().compareTo( s2.trim() );

} };

}

Die Realisierung nutzt hier eine innere anonyme Klasse, ein Konzept, das genauer in Kapitel 10, »Geschachtelte Typen«, beleuchtet wird.

 

Zum Seitenanfang

8.1.14    Erweiterte Schnittstellen deklarieren und nutzen Zur vorigen ÜberschriftZur nächsten Überschrift

Realisieren wir dies in einem Beispiel. Bei den Ereignissen soll ein Lebenszyklus möglich sein; der besteht aus start() und finish(). Der Lebenszyklus ist als Schnittstelle vorgegeben, die Events implementieren können. Version 1 der Schnittstelle sieht also so aus:

Listing 8.12     src/main/java/com/tutego/insel/game/i/v2/EventLifecycle.java, EventLifecycle

interface EventLifecycle {

void start();

void finish();

}

Die Klasse Event implementiert die Schnittstelle:

Listing 8.13     src/main/java/com/tutego/insel/game/i/v2/EventLifecycle.java, EventLifecycle

abstract class Event implements EventLifecycle {

String about;

int duration;



abstract void process();



@Override public void start() { }

@Override public void finish() { }

}

Die Klasse überschreibt die beiden Methoden leer, sodass die Event-Unterklassen wie Nap oder Workout sich frei entscheiden können, ob sie die leeren Methoden noch einmal überschreiben oder es lassen.

Eine Unterklasse ist dann schnell geschrieben:

Listing 8.14     src/main/java/com/tutego/insel/game/i/v2/Nap.java, Nap

class Nap extends Event {

@Override void process() {

System.out.println( "Gähn" );

}

}

Je länger Software lebt, desto mehr offenbaren sich Fehlentscheidungen beim Design. Die Umstellung einer ganzen Architektur ist eine Mammutaufgabe, einfache Änderungen wie das Umbenennen sind über ein Refactoring schnell erledigt. Nehmen wir an, dass es auch bei unserer Schnittstelle einen Änderungswunsch gibt – nur den Start und das Ende zu melden, reicht nicht. Gibt es bei dem Ereignis eine Pause, soll eine neue Methode in die Schnittstelle kommen: pause(). Welche Konsequenzen hat es, wenn in EventLifecycle die neue Methode pause() kommt?

interface EventLifecycle {

void start();

void finish();

void pause();

}

Das wäre ein Problem! Plötzlich gäbe es bei Nap einen Fehler:

Class 'Nap' must either be declared abstract or implement abstract method 'pause()' in 'EventLifecycle'

Methoden in Schnittstellen zu ergänzen, ist schwierig, weil dann alle implementierenden Klassen geändert werden müssen. Hier spielen uns Default-Methoden perfekt in die Hände, denn wir können die Schnittstelle erweitern, aber eine leere Standardimplementierung mitgeben. So müssen Unterklassen die pause()-Methode nicht implementieren, können dies aber, wie Version 2 der nun erweiterten Schnittstelle EventLifecycle zeigt:

interface EventLifecycle {

void start();

void finish();

default void pause() {}

}

Klassen, die EventLifecycle schon genutzt haben, bekommen von der Änderung nichts mit. Der Vorteil: Die Schnittstelle kann sich weiterentwickeln, aber alles bleibt binärkompatibel, und nichts muss neu compiliert werden. Vorhandener Code kann auf die neue Methode zurückgreifen, die automatisch mit der »leeren« Implementierung vorhanden ist. Außerdem verhalten sich Default-Methoden wie andere Methoden von Schnittstellen auch: Es bleibt bei der dynamischen Bindung, wenn implementierende Klassen die Methoden überschreiben. Wenn eine Implementierung wie Workout zum Beispiel bei der Pause etwas macht, so überschreibt sie die Methode und stoppt zum Beispiel den Kalorienverbrauch. Ein Schläfchen dagegen hat nichts zu pausieren und kann mit dem Default-Code in pause() gut leben. Das Vorgehen ist ein wenig vergleichbar mit normalen, nichtfinalen Methoden: Sie können, müssen aber nicht überschrieben werden.

[»]  Hinweis

Statt des leeren Blocks könnte der Rumpf auch throw new UnsupportedOperationException ("Not yet implemented"); beinhalten, um anzukündigen, dass es keine Implementierung gibt. So führt eine hinzugenommene Default-Methode zwar zu keinem Compilerfehler, aber zur Laufzeit führen nicht überschriebene Methoden zu einer Ausnahme. Erreicht ist das Gegenteil vom Default-Code, weil eben keine Logik standardmäßig ausgeführt wird – das Auslösen einer Ausnahme zum Melden eines Fehlers wollen wir nicht als Logik ansehen.

Kontext der Default-Methoden

Default-Methoden verhalten sich wie Methoden in abstrakten Klassen und können alle Methoden der Schnittstelle (inklusive der geerbten Methoden) aufrufen.[ 173 ](Und damit lässt sich das bekannte Template-Design-Pattern realisieren. ) Die Methoden werden später dynamisch zur Laufzeit gebunden.

Nehmen wir eine Schnittstelle Buyable für käufliche Objekte:

interface Buyable {

double price();

}

Leider schreibt die Schnittstelle nicht vor, ob Dinge überhaupt käuflich sind. Eine Methode wie hasPrice() wäre in Buyable ganz gut aufgehoben. Was kann aber die Default-Implementierung sein? Wir können auf price() zurückgreifen und testen, ob die Rückgabe ein gültiger Preis ist. Das soll gegeben sein, wenn der Preis echt größer 0 ist:

interface Buyable {

double price();

default boolean hasPrice() { return price() > 0; }

}

Implementieren Klassen die Schnittstelle Buyable, müssen sie price() implementieren, da die Methode keine Default-Methode ist. Doch es ist ihnen freigestellt, hasPrice() zu überschreiben, mit eigener Logik zu füllen und nicht die Default-Implementierung zu verwenden. Wenn implementierende Klassen keine neue Implementierung wählen, bekommen sie den Default-Code und erben eine konkrete Methode hasPrice(). In dem Fall geht ein Aufruf von hasPrice() intern weiter an price() und dann genau an die Klasse, die Buyable und die Methode price() implementiert. Die Aufrufe sind dynamisch gebunden und landen bei der tatsächlichen Implementierung.

[»]  Hinweis

Eine Schnittstelle kann die Methoden der absoluten Oberklasse java.lang.Object ebenfalls deklarieren, etwa um mit Javadoc eine Beschreibung hinzuzufügen. Allerdings ist es nicht möglich, mittels Default-Code Methoden wie toString() oder hashCode() vorzubelegen.

Neben der Möglichkeit, auf Methoden der eigenen Schnittstelle zurückzugreifen, steht auch die this-Referenz zur Verfügung. Das ist sehr wichtig, denn so kann der Default-Code an Utility-Methoden delegieren und einen Verweis auf sich selbst übergeben. Hätten wir zum Beispiel schon eine hasPrice(Buyable)-Methode in einer Utility-Klasse PriceUtils implementiert, so könnte der Default-Code aus einer einfachen Delegation bestehen:

class PriceUtils {

public static boolean hasPrice( Buyable b ) { return b.price() > 0; }

}

interface Buyable {

double price();

default boolean hasPrice() { return PriceUtils.hasPrice( this ); }

}

Dass die Methode PriceUtils.hasPrice(Buyable) für den Parameter den Typ Buyable vorsieht und sich der Default-Code mit this auf genauso ein Buyable-Objekt bezieht, ist natürlich kein Zufall, sondern bewusst gewählt. Der Typ der this-Referenz zur Laufzeit entspricht dem der Klasse, die die Schnittstelle implementiert hat und deren Objektexemplar gebildet wurde.

Haben die Default-Methoden weitere Parameter, so lassen sich auch diese an die statische Methode weiterreichen:

class PriceUtils {

public static boolean hasPrice( Buyable b ) { return b.price() > 0; }

public static double priceOr( Buyable b, double defaultPrice ) {

if ( b != null && b.price() > 0 )

return b.price();

return defaultPrice;

}

}

interface Buyable {

double price();

default boolean hasPrice() { return PriceUtils.hasPrice( this ); }

default double priceOr( double defaultPrice ) {

return PriceUtils.priceOr( this, defaultPrice );

}

}

Da Schnittstellen statische Utility-Methoden mit Implementierung enthalten können, kann der Default-Code auf diese statischen Methoden delegieren. Allerdings ist zu überlegen, ob in einer Schnittstelle wirklich viel Code untergebracht werden sollte oder ob dieser nicht besser in eine paketsichtbare Implementierung wandern sollte. Es ist vorzuziehen, die Implementierung auszulagern, damit die Schnittstellen nicht so codelastig werden. Nutzt das JDK Default-Code, so gibt es in der Regel immer eine statische Methode in einer Utility-Klasse.

 

Zum Seitenanfang

8.1.15    Öffentliche und private Schnittstellenmethoden Zur vorigen ÜberschriftZur nächsten Überschrift

Seit Java 9 müssen die statischen und die Default-Methoden nicht mehr public sein; sie können auch private sein. Das ist gut, denn das beugt Codeduplikaten vor; mit privaten Methoden können Programmteile innerhalb der Schnittstelle ausgelagert werden. Private Methoden bleiben in der Schnittstelle und werden nicht in die implementierenden Klassen vererbt.

 

Zum Seitenanfang

8.1.16    Erweiterte Schnittstellen, Mehrfachvererbung und Mehrdeutigkeiten * Zur vorigen ÜberschriftZur nächsten Überschrift

Die Default-Methoden mussten eingeführt werden, um Schnittstellen im Nachhinein ohne nennenswerte Compilerfehler mit neuen Operationen ausstatten zu können. Ideal ist, wenn neue Default-Methoden hinzukommen und Standardverhalten definieren und es dadurch zu keinem Compilerfehler für implementierende Klassen kommt oder zu Fehlern bei Schnittstellen, die erweiterte Schnittstellen erweitern.

Erweiterte Schnittstellen mit Default-Code nehmen ganz normal an der objektorientierten Modellierung teil, können vererbt und überschrieben werden und werden dynamisch gebunden. Nun gibt es einige Sonderfälle, die wir uns anschauen müssen. Es kann vorkommen, dass zum Beispiel

  • eine Klasse von einer Oberklasse eine Methode erbt, aber gleichzeitig von einer Schnittstelle Default-Code für die gleiche Methode erhält, oder dass

  • eine Klasse von zwei erweiterten Schnittstellen unterschiedliche Implementierungen angeboten bekommt.

Gehen wir verschiedene Fälle durch.

Überschreiben von Default-Code

Eine Schnittstelle kann andere Schnittstellen erweitern und neuen Default-Code bereitstellen. Mit anderen Worten: Default-Methoden können andere Default-Methoden aus Oberschnittstellen überschreiben und mit neuem Verhalten implementieren.

Führen wir eine Schnittstelle Priced mit einer Default-Methode ein:

interface Priced {

default boolean hasPrice() { return true; }

}

Eine andere Schnittstelle kann die Default-Methode überschreiben:

interface NotPriced extends Priced {

@Override default boolean hasPrice() { return false; }

}

public class TrueLove implements NotPriced {

public static void main( String[] args ){

System.out.println( new TrueLove().hasPrice() ); // false

}

}

Implementiert die Klasse TrueLove die Schnittstelle NotPriced, so ist alles in Ordnung und es entsteht kein Konflikt. Die Vererbungsbeziehung ist linear TrueLove → NotPriced → Priced.

Klassenimplementierung geht vor Default-Methoden

Implementiert eine Klasse eine Schnittstelle und erbt sie außerdem von einer Oberklasse, kann Folgendes passieren: Die Schnittstelle hat Default-Code für eine Methode, und die Oberklasse vererbt ebenfalls die gleiche Methode mit Code. Dann bekommt die Unterklasse von zwei Seiten eine Implementierung. Zunächst muss der Compiler entscheiden, ob so etwas überhaupt syntaktisch korrekt ist. Ja, das ist es!

interface Priced {

default boolean hasPrice() { return true; }

}

class Unsaleable {

public boolean hasPrice() { return false; }

}

public class TrueLove extends Unsaleable implements Priced {

public static void main( String[] args ) {

System.out.println( new TrueLove().hasPrice() ); // false

}

}

TrueLove erbt die Implementierung hasPrice() von der Oberklasse Unsaleable und auch von der erweiterten Schnittstelle Priced. Der Code compiliert und führt zu der Ausgabe false – die Klasse mit dem Code »gewinnt« also gegen den Default-Code. Merken lässt sich das ganz einfach an der Reihenfolge class extends implements – hier steht extends am Anfang, also haben Methoden aus Implementierungen hier eine höhere Priorität als die Methoden aus erweiterten Schnittstellen.

Default-Methoden aus speziellen Oberschnittstellen ansprechen *

Eine Unterklasse kann eine konkrete Methode der Oberklasse überschreiben, aber dennoch auf die Implementierung der überschriebenen Methode zugreifen. Allerdings muss der Aufruf über super erfolgen, da sich sonst ein Methodenaufruf rekursiv verfängt.

Default-Methoden können andere Default-Methoden aus Oberschnittstellen ebenfalls überschreiben und mit neuem Verhalten implementieren. Doch genauso wie normale Methoden können sie mit super auf Default-Verhalten aus dem übergeordneten Typ zurückgreifen.

Nehmen wir für ein Beispiel unsere bekannte Schnittstelle Buyable und eine neue erweiterte Schnittstelle PeanutsBuyable an:

interface Buyable {

double price();

default boolean hasPrice() { return price() > 0; }

}

interface PeanutsBuyable extends Buyable {

@Override default boolean hasPrice() {

return Buyable.super.hasPrice() && price() < 50_000_000;

}

}

In der Schnittstelle Buyable sagt der Default-Code von hasPrice() aus, dass alles einen Preis hat, was größer als 0 ist. PeanutsBuyable dagegen nutzt eine erweiterte Definition und implementiert daher das Default-Verhalten neu. Nach den berühmten kopperschen Peanuts[ 174 ](https://de.wikipedia.org/wiki/Hilmar_Kopper#.E2.80.9EPeanuts.E2.80.9C) ist alles unter 50 Millionen problemlos käuflich und verursacht – zumindest für die Deutsche Bank – keine Schmerzen. In der Implementierung von hasPrice() greift PeanutsBuyable auf den Default-Code von Buyable zurück, um vom Obertyp eine Entscheidung über die Preiseigenschaft zu bekommen, die aber mit der Und-Verknüpfung noch spezialisiert wird.

Default-Code für eine Methode von mehreren Schnittstellen erben *

Wenn eine Klasse aus zwei erweiterten Schnittstellen den gleichen Default-Code angeboten bekommt, führt das zu einem Compilerfehler. Die Klasse RockAndRoll zeigt dieses Dilemma:

interface Sex {

default boolean hasPrice() { return false; }

}

interface Drugs {

default boolean hasPrice() { return true; }

}

public class RockAndRoll implements Sex, Drugs { } // inline image Compilerfehler

Selbst wenn beide Implementierungen identisch wären, müsste der Compiler das ablehnen, denn der Code könnte sich ja jederzeit ändern.

Mehrfachvererbungsproblem mit super lösen

Die Klasse RockAndRoll lässt sich so nicht übersetzen, weil die Klasse aus zwei Quellen Code bekommt. Das Problem kann aber einfach gelöst werden, indem in RockAndRoll die hasPrice()-Methode überschrieben und dann an eine Methode delegiert wird:

Listing 8.15     src/main/java/com/tutego/insel/oop/RockAndRoll.java

interface Sex {

default boolean hasPrice() { return false; }

}

interface Drugs {

default boolean hasPrice() { return true; }

}

public class RockAndRoll implements Sex, Drugs {

@Override public boolean hasPrice() { return Sex.super.hasPrice(); }

}

Im Rumpf der Methode hasPrice() können wir nicht einfach hasPrice() schreiben, denn dann hätten wir einen rekursiven Aufruf. Auch können wir nicht Sex.hasPrice() schreiben, da diese Syntax für den Aufruf von statischen Methoden reserviert ist. Es kommt daher super mit der neuen Schreibweise ins Spiel: Sex.super.hasPrice().

Abstrakte überschriebene Schnittstellenoperationen nehmen Default-Methoden weg

Default-Methoden haben die interessante Eigenschaft, dass Untertypen den Status von »hat Implementierung« in »hat keine Default-Implementierung« ändern können:

interface Priced {

default boolean hasPrice() { return false; }

}

interface Buyable extends Priced {

@Override boolean hasPrice();

}

Die Schnittstelle Priced bietet eine Default-Methode. Buyable erweitert die Schnittstelle Priced, aber überschreibt die Methode – jedoch nicht mit Code! Dadurch wird sie in Buyable abstrakt. Eine abstrakte Methode kann also durchaus eine Default-Methode überschreiben. Klassen, die Buyable implementieren, müssen also nach wie vor eine hasPrice()-Methode implementieren, wenn sie nicht selbst abstrakt sein wollen. Es ist schon ein interessantes Java-Feature, dass die Implementierung einer Default-Methode in einem Untertyp wieder »weggenommen« werden kann. Bei der Sichtbarkeit ist das zum Beispiel nicht möglich: Ist eine Methode einmal öffentlich, kann eine Unterklasse die Sichtbarkeit nicht einschränken.

Das Verhalten des Compilers hat einen großen Vorteil: Bestimmte Veränderungen der Oberschnittstelle sind erlaubt und haben keine Auswirkungen auf die Untertypen. Nehmen wir an, hasPrice() hätte es in Priced vorher nicht gegeben, sondern nur abstrakt in Buyable. Default-Code ist ja nur eine nette Geste, und diese sollte schmerzlos in Priced integriert werden können. Anders gesagt: Entwickler können in den Basistyp so eine Default-Methode ohne Probleme aufnehmen, ohne dass es in den Untertypen zu Fehlern kommt. Obertypen lassen sich also ändern, ohne die Untertypen anzufassen. Im Nachhinein kann aber zur Dokumentation die Annotation @Override an die Unterschnittstelle gesetzt werden.

Nicht nur eine Unterschnittstelle kann die Default-Methoden »wegnehmen«, sondern auch eine abstrakte Klasse:

abstract class Food implements Priced {

@Override public abstract double price();

}

Die Schnittstelle Priced bringt eine Default-Methode mit, doch die abstrakte Klasse Food nimmt diese wieder weg, sodass erweiternde Food-Klassen auf jeden Fall price() implementieren müssen, wenn sie nicht selbst abstract sein wollen.

 

Zum Seitenanfang

8.1.17    Bausteine bilden mit Default-Methoden * Zur vorigen ÜberschriftZur nächsten Überschrift

Default-Methoden geben Bibliotheksdesignern ganz neue Möglichkeiten. Heute ist noch gar nicht richtig abzusehen, was Entwickler damit machen werden und welche Richtung die Java-API einschlagen wird. Auf jeden Fall wird sich die Frage stellen, ob eine Standardimplementierung als Default-Code in eine Schnittstelle wandert oder wie bisher eine Standardimplementierung als abstrakte Klasse bereitgestellt wird, von der wiederum andere Klassen ableiten. Als Beispiel sei auf die Datenstrukturen verwiesen: Eine Schnittstelle Collection schreibt Standardverhalten vor, AbstractCollection gibt eine Implementierung so weit wie möglich vor, und Unterklassen wie Listen setzen dann noch einmal auf diese Basisimplementierung auf. Erweiterte Schnittstellen können Hierarchien abbauen, denn auf eine abstrakte Basisimplementierung kann verzichtet werden. Auf der anderen Seite kann aber eine abstrakte Klasse einen Zustand über Objektvariablen einführen, was eine Schnittstelle nicht kann.

Default-Methoden können aber noch etwas ganz anderes: Sie können als Bauelemente für Klassen dienen. Eine Klasse kann mehrere Schnittstellen mit Default-Methoden implementieren und erbt im Grunde damit Basisfunktionalität von verschiedenen Stellen. In anderen Programmiersprachen ist das als Mixin oder Trait bekannt. Das ist ein Unterschied zur Mehrfachvererbung, die in Java nicht zulässig ist. Schauen wir uns diesen Unterschied jetzt einmal genauer an.

Default-Methoden zur Entwicklung von Traits nutzen

Was ist das Kernkonzept der objektorientierten Programmierung? Wohl ohne zu zögern können wir Klassen, Kapselung und Abstraktion nennen. Klassen und Klassenbeziehungen sind das Gerüst eines jeden Java-Programms. Bei der Vererbung wissen wir, dass Unterklassen Spezialisierungen sind und das liskovsche Substitutionsprinzip (siehe Abschnitt 7.3.2, »Das Substitutionsprinzip«) gilt: Falls ein Typ gefordert ist, können wir auch einen Untertyp übergeben. So sollte perfekte Vererbung aussehen: Eine Unterklasse spezialisiert das Verhalten, aber erbt nicht einfach von einer Klasse, weil diese nützliche Funktionalität hat. Aber warum eigentlich nicht? Als Erstes ist zu nennen, dass das Erben aufgrund der Nützlichkeit oft gegen die Ist-eine-Art-von-Beziehung verstößt und dass uns Java zweitens nur Einfachvererbung mit nur einer einzigen Oberklasse erlaubt. Wenn eine Klasse etwas Nützliches wie Logging anbietet und unsere Klasse davon erbt, kann sie nicht gleichzeitig von einer anderen Klasse erben, um zum Beispiel Zustände in Konfigurationsdaten festzuhalten. Eine unglückliche Vererbung verbaut also eine spätere Erweiterung. Das Problem bei der »Funktionalitätsvererbung« ist also, dass wir uns nur einmal festlegen können.

Wenn eine Klasse eine gewisse Funktionalität einfach braucht, woher soll diese denn dann kommen, wenn nicht aus der Oberklasse? Eigentlich gibt es hier nur eine naheliegende Variante: Die Klasse greift auf andere Objekte per Delegation zurück. Wenn ein Punkt mit Farbe nicht von java.awt.Point erben soll, kann ein Farbpunkt einfach in einer internen Variablen einen Point referenzieren. Das ist eine Lösung, aber dann nicht optimal, wenn eine Ist-eine-Art-von-Beziehung besteht. Und Schnittstellen wurden ja gerade eingeführt, damit eine Klasse mehrere Typen besitzt. Abstraktionen über Schnittstellen und Oberklassen sind wichtig, und Delegation hilft hier nicht. Gewünscht ist eine Technik, die einen Programmbaustein in eine Klasse setzen kann – im Grunde so etwas wie Mehrfachvererbung, aber doch anders, weil die Bausteine nicht als komplette Typen auftreten; der Baustein selbst ist nur ein Implantat und allein uninteressant. Auch ein Objekt kann von diesem Bausteintyp nicht erzeugt werden.

Am ehesten sind die Bausteine mit abstrakten Klassen vergleichbar, doch das wären Klassen, und Nutzer könnten nur einmal von diesem Baustein erben. Mit den erweiterten Schnittstellen gibt es ganz neue Möglichkeiten: Sie bilden die Bausteine, von denen Klassen Funktionalität bekommen können.[ 175 ](Siehe etwa http://scg.unibe.ch/archive/papers/Scha02aTraitsPlusGlue2002.pdf. ) Diese Bausteine sind nützlich, denn so lässt sich ein Algorithmus in eine Extra-Compilationseinheit setzen und leichter wiederverwenden. Ein Beispiel: Nehmen wir zwei erweiterte Schnittstellen an, PersistentPreference und Logged. Die erste erweiterte Schnittstelle soll mit store() Schlüssel-Wert-Paare in die zentrale Konfiguration schreiben, und get() soll sie auslesen:

import java.util.prefs.Preferences;

interface PersistentPreference {

default void store( String key, String value ) {

Preferences.userRoot().put( key, value );

}

default String get( String key ) {

return Preferences.userRoot().get( key, "" );

}

}

Die zweite erweiterte Schnittstelle ist Logged und bietet uns drei kompakte Logger-Methoden:

import java.util.logging.*;

interface Logged {

default void error( String message ) {

Logger.getLogger( getClass().getName() ).log( Level.SEVERE, message );

}

default void warn( String message ) {

Logger.getLogger( getClass().getName() ).log( Level.WARNING, message );

}

default void info( String message ) {

Logger.getLogger( getClass().getName() ).log( Level.INFO, message );

}

}

Eine Klasse kann diese Bausteine nun einbauen:

class Player implements PersistentPreference, Logged {

// ...

}

Die Methoden sind nun Teil vom Player und können auch von Unterklassen überschrieben werden. Als Aufgabe für den Leser bleibt, die Implementierung von store() im Player zu verändern, sodass der Schlüssel immer mit player. beginnt. Die Frage, die der Leser beantworten sollte, ist, ob store() von Player auf das store() von der erweiterten Schnittstelle zugreifen kann.

Zustand in den Bausteinen?

Nicht jeder wünschenswerte Baustein ist mit erweiterten Schnittstellen möglich. Ein Grund ist, dass die Schnittstellen keinen Zustand einbringen können. Nehmen wir zum Beispiel einen Container als Datenstruktur, der Elemente aufnimmt und verwaltet. Einen Baustein für einen Container können wir nicht so einfach implementieren, da ein Container Kinder verwaltet, und hierfür ist eine Objektvariable für den Zustand nötig. Schnittstellen haben nur statische Variablen, und die sind für alle sichtbar. Selbst wenn die Schnittstelle eine modifizierbare Datenstruktur referenzieren würde, wäre jeder Nutzer des Containerbausteins von den Veränderungen betroffen. Da es keinen Zustand gibt, existieren auch für Schnittstellen keine Konstruktoren und folglich auch nicht für solche Bausteine. Denn wo es keinen Zustand gibt, gibt es auch nichts zu initialisieren. Wenn eine Default-Methode einen Zustand benötigt, muss sie selbst diesen Zustand erfragen. Hier lässt sich eine Technik einsetzen, die Oracles Java Language Architect Brian Goetz »virtual field pattern«[ 176 ](https://mail.openjdk.java.net/pipermail/lambda-dev/2012-July/005171.html) nennt. Wie sie funktioniert, zeigt das folgende Beispiel.

Referenziert ein Behälter eine Menge von Objekten, die sortierbar sind, können wir einen Baustein Sortable mit einer Methode sort() realisieren. Die Schnittstelle Comparable soll die Klasse nicht direkt implementieren, da ja nur die referenzierten Elemente sortierbar sind, nicht aber Objekte der Klasse selbst; zudem soll eine neue Methode sort() in Sortable hinzukommen. Damit das Sortieren gelingt, muss die Implementierung irgendwie an die Daten gelangen – und hier kommt ein Trick ins Spiel: Zwar ist sort() eine Default-Methode, doch die erweiterte Schnittstelle Sortable besitzt eine abstrakte Methode getValues(), die die Klasse implementieren muss und dem Sortierer die Daten gibt. Im Quellcode sieht das so aus:

Listing 8.16     src/main/java/com/tutego/insel/oop/SortableDemo.java, Teil 1

import java.util.*;

interface Sortable<T extends Comparable<?>> {

T[] getValues();

void setValues( T[] values );

default void sort() {

T[] values = getValues();

Arrays.sort( values );

setValues( values );

};

}

Fassen wir zusammen: Damit sort() an die Daten kommt, erwartet Sortable von den implementierenden Klassen eine Methode getValues(), und damit die Daten nach dem Sortieren wieder zurückgeschrieben werden können, eine zweite Methode setValues(…). Der Clou ist, dass die spätere Implementierung von Sortable mit den beiden Methoden dem Sortierer Zugriff auf die Daten gewährt – allerdings auch jedem anderem Stück Code, da die Methoden öffentlich sind. Da bleibt ein unschönes »Geschmäckle« zurück.

Ein Nutzer von Sortable soll RandomValues sein; die Klasse erzeugt intern Zufallszahlen.

Listing 8.17     src/main/java/com/tutego/insel/oop/SortableDemo.java, Teil 2

class RandomValues implements Sortable<Integer> {

private List<Integer> values = new ArrayList<>();

public RandomValues() {

Random r = new Random();

for ( int i = r.nextInt( 20 ) + 1; i > 0; i-- )

values.add( r.nextInt(10000) );

}

@Override public Integer[] getValues() {

return values.toArray( new Integer[values.size()] );

}



@Override public void setValues( Integer[] values ) {

this.values.clear();

Collections.addAll( this.values, values );

}

}

Damit sind die Typen vorbereitet, und eine Demo schließt das Beispiel ab:

Listing 8.18     src/main/java/com/tutego/insel/oop/SortableDemo.java, Teil 3

public class SortableDemo {

public static void main( String[] args ) {

RandomValues r = new RandomValues();

System.out.println( Arrays.toString( r.getValues() ) );

r.sort();

System.out.println( Arrays.toString( r.getValues() ) );

}

}

Wird das Demoprogramm aufgerufen, kommt auf die Konsole zum Beispiel:

[2732, 4568, 4708, 4302, 4315, 5946, 2004]

[2004, 2732, 4302, 4315, 4568, 4708, 5946]

So interessant diese Möglichkeit auch ist, ein Problem wurde schon angesprochen: Jede Methode in einer Schnittstelle ist public oder private. Es wäre schön, wenn die Datenzugriffsmethode protected und somit nur sichtbar für die implementierende Klasse wäre, aber das geht nicht.

[ ! ]  Warnung!

Natürlich lässt sich mit Rumgetrickse ein Speicherort finden, der Exemplarzustände speichert. Es lässt sich zum Beispiel in der Schnittstelle ein Assoziativspeicher referenzieren, der eine this-Instanz mit einem Objekt assoziiert. Ein Containerbaustein, der mit add() Objekte in eine Liste setzt und sie mit iterable() herausgibt, könnte so aussehen:

interface ListContainer<T> {

Map<Object,List<Object>> $ = new HashMap<>();

default void add( T e ) {

if ( ! $.containsKey( this ) )

$.put( this, new ArrayList<Object>() );

$.get( this ).add( e );

}

default public Iterable<T> iterable() {

if ( ! $.containsKey( this ) )

return Collections.emptyList();

return (Iterable<T>) $.get( this );

}

}

Nicht nur die öffentliche Konstante $ ist ein Problem, sondern auch, dass die Variable ein übles doppeltes Speicherloch ist. Ein Exemplar der Klasse, die diese erweiterte Schnittstelle nutzt, kann nicht so einfach entfernt werden, denn in der Sammlung ist noch eine Referenz auf das Objekt vorhanden, und diese Referenz verhindert eine automatische Speicherbereinigung. Selbst wenn dieses Objekt weg wäre, hätten wir noch all die referenzierten Kinder der Sammlung in der Map. Das Problem ist nicht wirklich zu lösen, und hier müsste mit schwachen Referenzen tief in die Java-Voodoo-Kiste gegriffen werden. Alles in allem ist es keine gute Idee, und Java-Chefentwickler Brian Goetz macht auch klar:

»Please don’t encourage techniques like this. There are a zillion ›clever‹ things you can do in Java, but shouldn’t. We knew it wouldn’t be long before someone suggested this, and we can’t stop you. But please, use your power for good, and not for evil. Teach people to do it right, not to abuse it.«[ 177 ](https://mail.openjdk.java.net/pipermail/lambda-dev/2012-July/005166.html)

Daher: Diese Implementierung ist eine schöne »Spielerei«, aber der Zustand sollte eine Aufgabe der abstrakten Basisklassen oder des Delegates sein.

Zusammenfassung

Was wir in den letzten Beispielen zu den Bausteinen gemacht haben, war, ein Standardverhalten in Klassen einzubauen, ohne dass dabei der Zugriff auf die nur einmal existierende Basisklasse nötig war und ohne dass die Klasse an Hilfsklassen delegierte. In dieser Arbeitsweise können Unterklassen in jedem Fall die Methoden überschreiben und spezialisieren. Wir haben es also mit üblichen Klassen zu tun und mit erweiterten Schnittstellen, die nicht selbst eigenständige Entitäten bilden. In der Praxis wird es immer Fälle geben, in denen für eine Umsetzung eines Problems entweder eine abstrakte Klasse oder eine erweiterte Schnittstelle infrage kommt. Wir sollten uns dann noch einmal an die Unterschiede erinnern: Eine abstrakte Klasse kann Objektvariablen haben und Methoden aller Sichtbarkeiten und sie auch final setzen, sodass sie nicht mehr überschrieben werden können. Eine Schnittstelle dagegen ist ohne Zustand und mit puren virtuellen und öffentlichen Methoden darauf ausgelegt, dass die Implementierung überschrieben werden kann.

 

Zum Seitenanfang

8.1.18    Markierungsschnittstellen * Zur vorigen ÜberschriftZur nächsten Überschrift

Auch Schnittstellen ohne Methoden sind möglich. Diese leeren Schnittstellen werden Markierungsschnittstellen (engl. marker interfaces) genannt. Sie sind nützlich, da mit instanceof leicht überprüft werden kann, ob ein Objekt einen gewollten Typ einnimmt.

Die Java-Bibliothek bringt einige Markierungsschnittstellen schon mit, etwa:

  • java.util.RandomAccess: Eine Datenstruktur bietet schnellen Zugriff über einen Index.

  • java.rmi.Remote: Identifiziert Schnittstellen, deren Operationen von außen aufgerufen werden können.

  • java.lang.Cloneable: Sorgt dafür, dass die clone()-Methode von Object aufgerufen werden kann.

  • java.util.EventListener: Diesen Typ implementieren viele Horcher in der Java-Bibliothek.

  • java.io.Serializable: Zustände eines Objekts lassen sich in einen Datenstrom schreiben – mehr dazu folgt in Kapitel 20, »Einführung in Dateien und Datenströme«.

[»]  Hinweis

Seit es das Sprachmittel der Annotationen gibt, sind Markierungsschnittstellen bei neuen Bibliotheken nicht mehr anzutreffen.

 

Zum Seitenanfang

8.1.19    (Abstrakte) Klassen und Schnittstellen im Vergleich Zur vorigen ÜberschriftZur nächsten Überschrift

Eine abstrakte Klasse und eine Schnittstelle mit abstrakten Methoden sind sich sehr ähnlich: Beide schreiben den Unterklassen bzw. den implementierten Klassen Operationen vor, die implementiert werden müssen. Ein wichtiger Unterschied ist jedoch, dass beliebig viele Schnittstellen implementiert werden können, aber nur eine Klasse – sei sie abstrakt oder nicht – erweitert werden kann. Des Weiteren bieten sich abstrakte Klassen meist im Refactoring oder in der Designphase an, wenn Gemeinsamkeiten in eine Oberklasse ausgelagert werden sollen. Abstrakte Klassen können zudem Objektzustände enthalten, was Schnittstellen nicht können.

Beim Design gilt weiterhin der Grundgedanke für Schnittstellen: Wenn es um Vorschriften für Verhalten geht, ist eine Schnittstelle goldrichtig. Bei Basisimplementierungen kommen dann abstrakte Klassen ins Spiel, die in der Java-Bibliothek oft auf Abstract enden.

Wie wo was dynamisch binden

Es gibt bei Methoden von konkreten Klassen, abstrakten Klassen und Schnittstellen Unterschiede, wo der Aufruf letztendlich landet. Nehmen wir folgende Methode an:

void fun( T t ) {

t.m();

}

Fordert die Methode ein Argument vom Typ T und ruft auf der Parametervariablen t die Methode m() auf, so können wir Folgendes festhalten:

  • Ist T eine finale Klasse, so wird immer die Methode m() von T aufgerufen, da es keine Unterklassen geben kann, die m() überschreiben.

  • Ist T eine nichtfinale Klasse und m() eine finale Methode, wird genau m() aufgerufen, weil keine Unterklasse m() überschreiben kann.

  • Ist T eine nichtfinale Klasse und m() keine finale Methode, so könnten Unterklassen von T m() überschreiben, und t.m() würde dann dynamisch die überschriebene Methode aufrufen.

  • Ist T eine abstrakte Klasse und m() eine abstrakte Methode, so wird in jedem Fall eine Realisierung von m() in einer Unterklasse aufgerufen.

  • Ist T eine Schnittstelle und m() keine Default-Implementierung, so wird in jedem Fall eine Implementierung m() einer implementierenden Klasse aufgerufen.

  • Ist T eine Schnittstelle und m() eine Default-Implementierung, so kann t.m() bei der Default-Implementierung landen oder bei einer überschriebenen Version einer implementierenden Klasse.

 


Ihre Meinung?

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de

<< zurück
 Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Java ist auch eine Insel Java ist auch eine Insel

Jetzt Buch bestellen


 Buchempfehlungen
Zum Rheinwerk-Shop: Captain CiaoCiao erobert Java

Captain CiaoCiao erobert Java




Zum Rheinwerk-Shop: Algorithmen in Java

Algorithmen in Java




Zum Rheinwerk-Shop: Spring Boot 3 und Spring Framework 6

Spring Boot 3 und Spring Framework 6




Zum Rheinwerk-Shop: Java SE 9 Standard-Bibliothek

Java SE 9 Standard-Bibliothek




 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und in die Schweiz

InfoInfo



 

 


Copyright © Rheinwerk Verlag GmbH 2024

Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.

Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 

[Rheinwerk Computing]



Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de



Cookie-Einstellungen ändern