Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

 
Inhaltsverzeichnis
Vorwort
1 Java ist auch eine Sprache
2 Imperative Sprachkonzepte
3 Klassen und Objekte
4 Der Umgang mit Zeichenketten
5 Eigene Klassen schreiben
6 Objektorientierte Beziehungsfragen
7 Ausnahmen müssen sein
8 Äußere.innere Klassen
9 Besondere Typen der Java SE
10 Generics<T>
11 Lambda-Ausdrücke und funktionale Programmierung
12 Architektur, Design und angewandte Objektorientierung
13 Komponenten, JavaBeans und Module
14 Die Klassenbibliothek
15 Einführung in die nebenläufige Programmierung
16 Einführung in Datenstrukturen und Algorithmen
17 Einführung in grafische Oberflächen
18 Einführung in Dateien und Datenströme
19 Einführung ins Datenbankmanagement mit JDBC
20 Einführung in <XML>
21 Testen mit JUnit
22 Bits und Bytes und Mathematisches
23 Die Werkzeuge des JDK
A Java SE-Paketübersicht
Stichwortverzeichnis


Download:

- Beispielprogramme, ca. 35,4 MB


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 6 Objektorientierte Beziehungsfragen
Pfeil 6.1 Assoziationen zwischen Objekten
Pfeil 6.1.1 Unidirektionale 1:1-Beziehung
Pfeil 6.1.2 Zwei Freunde müsst ihr werden – bidirektionale 1:1-Beziehungen
Pfeil 6.1.3 Unidirektionale 1:n-Beziehung
Pfeil 6.2 Vererbung
Pfeil 6.2.1 Vererbung in Java
Pfeil 6.2.2 Spielobjekte modellieren
Pfeil 6.2.3 Die implizite Basisklasse java.lang.Object
Pfeil 6.2.4 Einfach- und Mehrfachvererbung *
Pfeil 6.2.5 Die Sichtbarkeit protected
Pfeil 6.2.6 Konstruktoren in der Vererbung und super(…)
Pfeil 6.3 Typen in Hierarchien
Pfeil 6.3.1 Automatische und explizite Typumwandlung
Pfeil 6.3.2 Das Substitutionsprinzip
Pfeil 6.3.3 Typen mit dem instanceof-Operator testen
Pfeil 6.4 Methoden überschreiben
Pfeil 6.4.1 Methoden in Unterklassen mit neuem Verhalten ausstatten
Pfeil 6.4.2 Mit super an die Eltern
Pfeil 6.4.3 Finale Klassen und finale Methoden
Pfeil 6.4.4 Kovariante Rückgabetypen
Pfeil 6.4.5 Array-Typen und Kovarianz *
Pfeil 6.5 Drum prüfe, wer sich ewig dynamisch bindet
Pfeil 6.5.1 Gebunden an toString()
Pfeil 6.5.2 Implementierung von System.out.println(Object)
Pfeil 6.5.3 Nicht dynamisch gebunden bei privaten, statischen und finalen Methoden
Pfeil 6.5.4 Dynamisch gebunden auch bei Konstruktoraufrufen *
Pfeil 6.5.5 Eine letzte Spielerei mit Javas dynamischer Bindung und überdeckten Attributen *
Pfeil 6.6 Abstrakte Klassen und abstrakte Methoden
Pfeil 6.6.1 Abstrakte Klassen
Pfeil 6.6.2 Abstrakte Methoden
Pfeil 6.7 Schnittstellen
Pfeil 6.7.1 Schnittstellen deklarieren
Pfeil 6.7.2 Implementieren von Schnittstellen
Pfeil 6.7.3 Ein Polymorphie-Beispiel mit Schnittstellen
Pfeil 6.7.4 Die Mehrfachvererbung bei Schnittstellen
Pfeil 6.7.5 Keine Kollisionsgefahr bei Mehrfachvererbung *
Pfeil 6.7.6 Erweitern von Interfaces – Subinterfaces
Pfeil 6.7.7 Konstantendeklarationen bei Schnittstellen
Pfeil 6.7.8 Statische ausprogrammierte Methoden in Schnittstellen
Pfeil 6.7.9 Erweitern von Schnittstellen
Pfeil 6.7.10 Default-Methoden
Pfeil 6.7.11 Erweiterte Schnittstellen deklarieren und nutzen
Pfeil 6.7.12 Erweiterte Schnittstellen, Mehrfachvererbung und Mehrdeutigkeiten *
Pfeil 6.7.13 Bausteine bilden mit Default-Methoden *
Pfeil 6.7.14 Initialisierung von Schnittstellenkonstanten *
Pfeil 6.7.15 Markierungsschnittstellen *
Pfeil 6.7.16 (Abstrakte) Klassen und Schnittstellen im Vergleich
Pfeil 6.8 Zum Weiterlesen
 

Zum Seitenanfang

6.7Schnittstellen Zur vorigen ÜberschriftZur nächsten Überschrift

Da Java nur Einfachvererbung kennt, ist es schwierig, Klassen mehrere Typen zu geben. Das kann immer nur in einer Reihe geschehen, also etwa so: GameObject erbt von Object, Building erbt von GameObject, Castle erbt von Building usw. Es wird schwierig, an einer Stelle zu sagen, dass ein Building ein GameObject ist, aber zum Beispiel zusätzlich einen Typ Buyable haben soll, was nur nicht gleich alle Spielobjekte haben sollen. Denn soll eine Klasse auf einer Ebene von mehreren Typen erben, geht das durch die Einfachvererbung nicht. Da es aber möglich sein soll, dass in der objektorientierten Modellierung eine Klasse mehrere Typen in einem Schritt besitzt, gibt es das Konzept der Schnittstelle (engl. interface). Eine Klasse kann dann neben der Oberklasse eine beliebige Anzahl Schnittstellen implementieren und auf diese Weise weitere Typen sammeln.

OOP-Design

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. Außerdem dürfen wir nicht vergessen, dass abstrakte Oberklassen Attribute haben können, Schnittstellen aber nicht.

 

Zum Seitenanfang

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

Eine Schnittstelle enthält keine Implementierungen von Objektmethoden, 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.[ 160 ](Oder ein Lambda-Ausdruck, doch dazu später mehr in Kapitel 11, »Lambda-Ausdrücke und funktionale Programmierung«. )

UML-Diagramm der Schnittstelle Buyable

Abbildung 6.10UML-Diagramm der Schnittstelle Buyable

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

Listing 6.49com/tutego/insel/game/vk/Buyable.java, Buyable

interface Buyable {

double price();

}

Die Deklaration einer Schnittstelle erinnert an eine abstrakte Klasse mit abstrakten Methoden, nur steht an Stelle von class das Schlüsselwort interface. Da alle Objektmethoden in Schnittstellen automatisch abstrakt und öffentlich sind, akzeptiert der Compiler das redundante abstract und public. Es ist aber guter Stil, diese Modifizierer nicht noch einmal explizit zu nennen, insbesondere weil es (im Moment noch) keine nichtöffentlichen Methoden gibt. Die von den Schnittstellen deklarierten Operationen sind – wie auch bei abstrakten Methoden – mit einem Semikolon abgeschlossen und haben niemals eine Implementierung.

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.

inline Existiert eine Klasse, in der Methoden in einer neuen Schnittstelle deklariert werden sollen, lässt sich RefactorExtract Interface… einsetzen. Es folgt ein Dialog, der uns Methoden auswählen lässt, die später in der neuen Schnittstelle deklariert werden. Eclipse legt die Schnittstelle automatisch an und lässt die Klasse die Schnittstelle implementieren. Dort, wo es möglich ist, erlaubt Eclipse, dass die konkrete Klasse durch die Schnittstelle ersetzt wird.

[»]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.

 

Zum Seitenanfang

6.7.2Implementieren 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.«

Für unsere Spielwelt sollen die Klassen Chocolate und Magazine die Schnittstelle Buyable implementieren. Eine Schokolade soll dabei immer einen sozialistischen Einheitspreis von 0,69 haben.

Listing 6.50com/tutego/insel/game/vk/Chocolate.java, Chocolate

public class Chocolate implements Buyable {

@Override public double price() {

return 0.69;

}

}

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

Während Chocolate nur die Schnittstelle Buyable implementiert, soll Magazine zusätzlich ein GameObject sein:

Listing 6.51com/tutego/insel/game/vk/Magazine.java, Magazine

public class Magazine extends GameObject implements Buyable {



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

Die Klassen Magazine und Chocolate implementieren Buyable.

Abbildung 6.11Die Klassen Magazine und Chocolate implementieren Buyable.

Es gelten dann folgende Typbeziehungen (die sich auch mit instanceof testen lässt):

  • GameObject ist ein Object

  • Magazine ist ein GameObject

  • Magazine ist ein Object

  • Magazine ist ein Buyable

  • Chocolate ist ein Buyable

  • Chocolate 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 (also beliebig), Magazine, Chocolate, GameObject, Buyable

GameObject

GameObject, Magazine

Buyable

Buyable, Magazine, Chocolate

Magazine

Magazine

Chocolate

Chocolate

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

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

[»]Hinweis

Da die in Schnittstellen deklarierten Operationen immer public sind, müssen auch die implementierten Methoden in den Klassen immer öffentlich sein. Sollte diese Vorgabe wirklich lästig sein, lässt sich immer noch eine abstrakte Klasse mit einer abstrakten Methode eingeschränkter Sichtbarkeit deklarieren. Dann gibt es aber auch nur einmal eine Vererbung.

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

inline Eclipse zeigt bei der Tastenkombination (Strg) + (T) eine Typhierarchie an; Oberklassen stehen oben und Unterklassen unten. Wird in dieser Ansicht erneut (Strg) + (T) gedrückt, dreht sich die Ansicht um, und Obertypen stehen unten; implementierte Schnittstellen tauchen mit unter den Obertypen auf.

 

Zum Seitenanfang

6.7.3Ein 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 Magazine und Chocolate haben wir zwei Klassen, die Buyable implementieren. Damit existieren zwei Klassen, die einen gemeinsamen Typ und beide eine gemeinsame Methode price() besitzen:

Buyable b1 = new Magazine();

Buyable b2 = new Chocolate();

System.out.println( b1.price() );

System.out.println( b2.price() );

Für Buyable wollen wir eine statische Methode calculateSum(….) schreiben, die den Preis einer Sammlung zum Verkauf stehender Objekte berechnet. Sie soll wie folgt aufgerufen werden:

Listing 6.52com/tutego/insel/game/vk/Playground.java, main()

Magazine madMag = new Magazine();

madMag.price = 2.50;

Buyable schoki = new Chocolate();

Magazine maxim = new Magazine();

maxim.price = 3.00;

System.out.printf( "%.2f", PriceUtils.calculateSum( madMag, maxim, schoki ) ); // 6,19

Damit calculateSum(…) eine beliebige Anzahl Argumente, aber mindestens eins, annehmen kann, realisieren wir die Methode mit einem Vararg:

Listing 6.53com/tutego/insel/game/vk/PriceUtils.java, calculateSum()

static double calculateSum( Buyable price1, Buyable... prices ) {

double result = price1.price();



for ( Buyable price : prices )

result += price.price();



return result;

}

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

Die dynamische Bindung tritt schon in der ersten Anweisung, price1.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.

[+]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

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

Bei Klassen gibt es die Einschränkung, dass nur von einer direkten Oberklasse abgeleitet werden darf – 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. Ohne Schwierigkeiten kann eine Klasse jedoch mehrere 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.

Dass in Java eine Klasse mehrere Schnittstellen implementieren kann, wird gelegentlich als Mehrfachvererbung in Java bezeichnet. Auf diese Weise besitzt die Klasse ganz unterschiedliche Typen. Ist U eine solche Klasse mit der Oberklasse O und implementiert sie die Schnittstellen I1 und I2, so liefert für ein Exemplar o vom Typ O der Test o instanceof O ein wahres Ergebnis genauso wie o instanceof I1 und o instanceof I2.

Begrifflichkeit

Wenn es um das Thema Mehrfachvererbung geht, dann müssen wir Folgendes unterscheiden: Geht es um Klassenvererbung, so genannte Implementierungsvererbung, ist Mehrfachvererbung nicht erlaubt. Geht es dagegen um Schnittstellenvererbung, so ist in dem Sinne Mehrfachvererbung erlaubt, denn eine Klasse kann beliebig viele Schnittstellen implementieren. Typvererbung ist hier ein gebräuchliches Wort. Üblicherweise wird der Begriff Mehrfachvererbung in Java nicht verwendet, da er sich traditionell auf Klassenvererbung bezieht.

Beginnen wir mit einem Beispiel. GameObject soll die Markierungsschnittstelle Serializable implementieren, sodass dann alle Unterklassen von GameObject ebenfalls vom Typ Serializable sind. Die Markierungsschnittstelle schreibt nichts vor, daher gibt es keine spezielle überschriebene Methode:

Listing 6.54com/tutego/insel/game/vl/GameObject.java, GameObject

public abstract class GameObject implements Serializable {



protected String name;



protected GameObject( String name ) {

this.name = name;

}

}

Damit gibt es schon verschiedene Ist-eine-Art-von-Beziehungen: GameObject ist ein java. lang.Object, GameObject ist ein GameObject, GameObject ist Serializable.

Ein Magazine soll zunächst ein GameObject sein. Dann soll es nicht nur die Schnittstelle Buyable und damit die Methode price() implementieren, sondern sich auch mit anderen Magazinen vergleichen lassen. Dazu gibt es schon eine passende Schnittstelle in der Java-Bibliothek: java.lang.Comparable. Die Schnittstelle Comparable fordert, dass unser Magazin die Methode int compareTo(Magazine) implementiert. Der Rückgabewert der Methode zeigt an, wie das eigene Magazin zum anderen aufgestellt ist. Wir wollen definieren, dass das günstigere Magazin vor einem teureren steht (eigentlich sollten mit Comparable auch equals(…) und hashCode() aus Object überschrieben werden, doch das spart das Beispiel aus[ 161 ](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. )):

Listing 6.55com/tutego/insel/game/vl/Buyable.java, Buyable

interface Buyable {

double price();

}

Listing 6.56com/tutego/insel/game/vl/Magazine.java, Magazine

public class Magazine extends GameObject implements Buyable, Comparable<Magazine> {



private double price;



public Magazine( String name, double price ) {

super( name );

this.price = price;

}



@Override public double price() {

return price;

}



@Override public int compareTo( Magazine that ) {

return Double.compare( this.price(), that.price() );

}



@Override public String toString() {

return name + " " + price;

}

}

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

Die Klasse Magazine mit diversen Obertypen

Abbildung 6.12Die Klasse Magazine mit diversen Obertypen

Durch diese »Mehrfachvererbung« bekommt Magazine mehrere Typen, sodass sich je nach Sichtweise Folgendes schreiben lässt:

Magazine m1 = new Magazine( "Mad Magazine", 2.50 );

GameObject m2 = new Magazine( "Mad Magazine", 2.50 );

Object m3 = new Magazine( "Mad Magazine", 2.50 );

Buyable m4 = new Magazine( "Mad Magazine", 2.50 );

Comparable<Magazine> m5 = new Magazine( "Mad Magazine", 2.50 );

Serializable m6 = new Magazine( "Mad Magazine", 2.50 );

Die Konsequenzen davon sind:

  • Im Fall m1 sind alle Methoden der Schnittstellen verfügbar, also price() und compareTo(…) sowie das Attribut name.

  • Über m2 ist keine Schnittstellenmethode verfügbar, und nur die geschützte Variable name ist vorhanden.

  • Mit m3 sind alle Bezüge zu Spielobjekten verloren. Aber ein Magazine als Object ist ein gültiger Argumenttyp für System.out.println(Object).

  • Die Variable m4 ist vom Typ Buyable, sodass es price() gibt, jedoch kein compareTo(…). Das Objekt könnte daher in PriceUtils.calculateSum(…) eingesetzt werden.

  • Mit m5 gibt es ein compareTo(…), aber keinen Preis.

  • Da Magazine die Klasse GameObject erweitert und darüber auch vom Typ Serializable ist, lässt sich keine besondere Methode auf m6 aufrufen – Serializable ist eine Markierungsschnittstelle ohne Operationen. Damit könnte das Objekt allerdings von speziellen Klassen der Java-Bibliothek serialisiert und so persistent gemacht werden.

Ein kleines Beispiel zeigt abschließend die Anwendung der Methoden compareTo(…) der Schnittstelle Comparable und price() der Schnittstelle Buyable:

Listing 6.57com/tutego/insel/game/vl/Playground.java, main(), Teil 1

Magazine spiegel = new Magazine( "Spiegel", 3.50 );

Magazine madMag = new Magazine( "Mad Magazine", 2.50 );

Magazine maxim = new Magazine( "Maxim", 3.00 );

Magazine neon = new Magazine( "Neon", 3.00 );

Magazine ct = new Magazine( "c't", 3.30 );

Da wir einem Magazin so viele Sichten gegeben haben, können wir unsere Methode calculateSum(…) mit Magazine-Argumenten aufrufen, da jedes Magazine ja Buyable ist:

Listing 6.58com/tutego/insel/game/vl/Playground.java, main(), Teil 2

System.out.println( PriceUtils.calculateSum( spiegel, madMag, ct ) ); // 9.3

Und die Magazine können wir vergleichen:

Listing 6.59com/tutego/insel/game/vl/Playground.java, main(), Teil 3

System.out.println( spiegel.compareTo( ct ) ); // 1

System.out.println( ct.compareTo( spiegel ) ); // –1

System.out.println( maxim.compareTo( neon ) ); // 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 6.60com/tutego/insel/game/vl/Playground.java, main(), Teil 4

Magazine[] mags = { spiegel, madMag, maxim, neon, ct };

Arrays.sort( mags );

System.out.println( Arrays.toString( mags ) );

// [Mad Magazine 2.5, Maxim 3.0, Neon 3.0, c't 3.3, Spiegel 3.5]

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 Magazine mit den unterschiedlichen Typen können also sehr flexibel in unterschiedlichen Kontexten eingesetzt werden. Es muss somit für das Sortieren keine Spezialsortiermethode geschrieben werden, die nur Magazine sortieren kann, oder eine Methode zur Berechnung einer Summe, die nur auf Magazinen arbeitet. Wir modellieren die unterschiedlichen Anwendungsszenarien mit jeweils unterschiedlichen Schnittstellen, die Unterschiedliches von dem Objekt erwarten.

 

Zum Seitenanfang

6.7.5Keine 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. 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 6.61Politician.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.

Eine Klasse erbt von zwei Schnittstellen die gleiche Operation.

Abbildung 6.13Eine 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

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

Ein Subinterface ist die Erweiterung eines anderen Interfaces. 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, was 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 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 Unterschnittsstelle kann eine Operation der Oberschnittstelle »überschreiben«. Auf den ersten Blick ist das nicht sinnvoll, erfüllt aber zwei Zwecke. Erstens: In der Unterschnittstelle kann die API-Dokumentation präzisiert werden.[ 162 ](Leser können das bei java.util.Collection und java.util.Set einmal nachschauen. ) Zweitens: Wegen kovarianter Rückgaben kann eine Operation in der Unterschnittstelle einen spezielleren Rückgabetyp bekommen.

 

Zum Seitenanfang

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

Obwohl in einer Schnittstelle keine Objektmethoden ausprogrammiert werden und keine Objektvariablen deklariert werden dürfen, sind static final-Variablen (benannte Konstanten) in einer Schnittstelle erlaubt.

[zB]Beispiel

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

interface Buyable {

int MAX_PRICE = 10_000_000;

double price();

}

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. Alle Attribute einer Schnittstelle sind immer implizit public static final. Das verhindert, dass die Variable neu belegt wird, aber es verhindert keine Objektmanipulation.

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 ist, kann sie leicht mit Vulcano.EYJAFJALLAJÖKULL.replace(0, Vulcano.EYJAFJALLAJÖKULL.length(), "Vesuvius"); verändert werden, was der Idee einer Konstante 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 Date, StringBuilder oder mutable Datenstrukturen.

Vererbung und Überschattung von statischen Variablen *

Die Konstanten einer Schnittstelle können einer anderen Schnittstelle vererbt werden. Dabei gibt es einige kleine Einschränkungen. Wir wollen an einem Beispiel sehen, wie sich die Vererbung auswirkt, wenn gleiche Bezeichner in den Unterschnittstellen erneut verwendet werden. Die Basis unseres Beispiels ist die Schnittstelle BaseColors mit ein paar Deklarationen von Farben. Zwei Unterschnittstellen erweitern BaseColors, und zwar CarColors und PlaneColors, die für Farbdeklarationen für Autos und Flugzeuge stehen. Eine besondere Schnittstelle FlyingCarColors erweitert die beiden Schnittstellen CarColors und PlaneColors, denn es gibt auch fliegende Autos, die eine Farbe haben können.

Listing 6.62Colors.java

interface BaseColors {

int WHITE = 0;

int BLACK = 1;

int GREY = 2;

}

interface CarColors extends BaseColors {

int WHITE = 1;

int BLACK = 0;

}

interface PlaneColors extends BaseColors {

int WHITE = 0;

int GREY = 2;

}

interface FlyingCarColors extends CarColors, PlaneColors { }

public class Colors {

public static void main( String[] args ) {

System.out.println( BaseColors.GREY ); // 2

System.out.println( CarColors.GREY ); // 2

System.out.println( BaseColors.BLACK ); // 1

System.out.println( CarColors.BLACK ); // 0

System.out.println( PlaneColors.BLACK ); // 1

System.out.println( FlyingCarColors.WHITE );

// inline field FlyingCarColors.WHITE is ambiguous

System.out.println( FlyingCarColors.GREY );

// inline field FlyingCarColors.GREY is ambiguous

}

}

Die erste wichtige Tatsache ist, dass unsere drei Schnittstellen ohne Fehler übersetzt werden können, aber nicht die Klasse Colors. Das Programm und der Compiler zeigen folgendes Verhalten:

  • Schnittstellen vererben ihre Eigenschaften an die Unterschnittstellen. CarColors und auch PlaneColors erben die Farben WHITE, BLACK und GREY aus BaseColors.

  • Konstanten dürfen überdeckt werden. CarColors vertauscht die Farbdeklarationen von WHITE und BLACK und gibt ihnen neue Werte. Wird jetzt der Wert CarColors.BLACK verlangt, liefert die Umgebung den Wert 0, während BaseColors.BLACK 1 ergibt. Auch PlaneColors überdeckt die Konstanten WHITE und GREY, obwohl die Farben mit dem gleichen Wert belegt sind.

  • Erbt eine Schnittstelle von mehreren Oberschnittstellen, so ist es zulässig, wenn die Oberschnittstellen jeweils ein gleichlautendes Attribut haben. So erbt etwa FlyingCarColors von CarColors und PlaneColors die Einträge WHITE, BLACK und GREY.

  • Unterschnittstellen können aus zwei Oberschnittstellen die Attribute gleichen Namens übernehmen, auch wenn die Konstanten einen unterschiedlichen Wert haben. Das testet der Compiler nicht. FlyingCarColors bekommt aus CarColors ein WHITE mit 1, aber aus PlaneColors das WHITE mit 0. Daher ist in dem Beispiel Colors auch der Zugriff FlyingCarColors.WHITE nicht möglich und führt zu einem Compilerfehler. Bei der Benutzung muss ein unmissverständlich qualifizierter Name verwendet werden, der deutlich macht, welches Attribut gemeint ist, also zum Beispiel CarColors.WHITE oder PlaneColors.WHITE. Ähnliches gilt für die Farbe GREY. Obwohl Grau durch die ursprüngliche Deklaration bei BaseColors und auch bei der Überschattung in PlaneColors immer 2 ist, ist die Nutzung durch FlyingCarColors.GREY nicht zulässig. Das ist ein guter Schutz gegen Fehler, denn wenn der Compiler dies durchließe, könnte sich im Nachhinein die Belegung von GREY in BaseColors oder PlaneColors ohne Neuübersetzung aller Klassen ändern und zu Schwierigkeiten führen. Diesen Fehler – die Oberschnittstellen haben für eine Konstante unterschiedliche Werte – müsste die Laufzeitumgebung erkennen. Doch das ist nicht möglich, und in der Regel setzt der Compiler die Werte auch direkt in die Aufrufstelle ein, und ein Zugriff auf die Konstantenwerte der Schnittstelle findet nicht mehr statt.

 

Zum Seitenanfang

6.7.8Statische 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

Im vorangehenden Kapitel 5 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 implizit immer public, sodass dieser Sichtbarkeitsmodifizierer redundant ist. Konstanten sind implizit immer statisch. Statische Methoden müssen den Modifizierer static tragen, andernfalls gelten sie als abstrakte Operationen.

[»]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:

Attribut

Methode

Objekt-

nein, nicht erlaubt

ja, üblicherweise abstrakt

Statische(s)

ja, als Konstante

ja, immer mit Implementierung

Tabelle 6.3Erlaubte 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

6.7.9Erweitern 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 umbenannt, ist das kein Problem. Bekommt aber eine Schnittstelle eine neue Operation, führt das zu einem Übersetzungsfehler, 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 hat dies bei der Schnittstelle java.sql.Connection riskiert. Beim Übergang von Java 5 auf Java 6 wurde die Schnittstelle erweitert, und keine Treiberimplementierung konnte mehr compiliert werden.

Code-Kompatibilität und Binär-Kompatibilität *

Es gibt Änderungen, wie zum Beispiel neu eingeführte Operationen in Schnittstellen, die zwar zu Compilerfehlern führen, aber zur Laufzeit in Ordnung sind. 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 sie 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 Klassenpfad gesetzt wird, ist Folgendes in Ordnung:

  • neue Methoden in Schnittstelle hinzufügen

  • Schnittstelle erbt von einer zusätzlichen Schnittstelle.

  • Hinzufügen oder Löschen einer throws-Ausnahme

  • letzten Parametertyp von T[] in T... ändern

  • neue Konstanten, also statische Variablen hinzufügen

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

  • Ändern des Methodennamens

  • Ändern der Parametertypen und Umsortieren der Parameter

  • 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 Methodenamens 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 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.[ 163 ](Ein Blick auf die API vom Eclipse-Framework zeigt, dass bei mehr als 3.700 Typen dieses Muster um die sechzigmal angewendet wurde (http://help.eclipse.org/mars/topic/org.eclipse.platform.doc.isv/reference/api/index.html?overview-summary.html). )

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

6.7.10Default-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[ 164 ](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:

Sonst verhalten sich erweiterte Schnittstellen wie normale Schnittstellen. Eine Klasse, die eine Schnittstelle implementiert, erbt alle Operationen, sei 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 8, »Äußere.innere Klassen«, beleuchtet wird.

 

Zum Seitenanfang

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

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

interface GameLifecycle {

void start();

void finish();

}

Klassen wie Player, Room, Door können die Schnittstelle erweitern, und wenn sie dies tun, müssen sie die beiden Methoden implementieren. Bei Spielobjekten, die diese Schnittstelle implementieren, kann unser Hauptprogramm, das Spiel, diese Methoden aufrufen und den Spielobjekten Rückmeldung geben, ob sie gerade in das Spiel gebracht wurden oder ob sie aus dem Spiel entfernt wurden.

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 die Initialisierung und das Ende zu melden, reicht nicht. Geht das Spiel in einen Pausemodus, soll ein Spielobjekt die Möglichkeit bekommen, im Hintergrund laufende Programme anzuhalten. Das soll durch eine zusätzliche pause()-Methode in der Schnittstelle realisiert werden. 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; Version 2 der nun erweiterten Schnittstelle GameLifecycle:

interface GameLifecycle {

void start();

void finish();

default void pause() {}

}

Klassen, die GameLifecycle 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 Unterklasse wie Flower zum Beispiel bei der Spielpause nicht mehr blühen möchte, so überschreibt sie die Methode und lässt etwa den Timer pausieren. Eine Tür dagegen hat nichts zu stoppen 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.[ 166 ](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 zu Logik 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 defaultPrice( 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 defaultPrice( double defaultPrice ) {

return PriceUtils.defaultPrice( this, defaultPrice );

}

}

Da Schnittstellen auch statische Utility-Methoden mit Implementierung enthalten können, kann der Default-Code auch hier weiterleiten. Allerdings sind die statischen Schnittstellen-Methoden immer öffentlich, und vielleicht möchte der Default-Code an eine geschützte paketsichtbare Methode weiterleiten. Außerdem ist es 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

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

Hintergrund zur Einführung von Default-Methoden war die Notwendigkeit, 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 auch von einer Schnittstelle Default-Code für die gleiche Methode oder

  • 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 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 – es steht extends am Anfang, also haben Methoden aus Implementierungen hier eine höhere Priorität als die 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[ 167 ](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 { } // 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. Um rekursive Aufrufe zu vermeiden, kommt wieder super mit der neuen Schreibweise ins Spiel:

interface Sex {

default boolean hasPrice() { return false; }

}

interface Drugs {

default boolean hasPrice() { return true; }

}

class RockAndRoll implements Sex, Drugs {

@Override public boolean hasPrice() { return 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

6.7.13Bausteine 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 bzw. 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 6.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 sie 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, wir nennen das Mixin bzw. Trait.[ 168 ](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-Kompilationseinheit setzen und leichter wiederverwenden. Ein Beispiel: Nehmen wir zwei erweiterte Schnittstellen PersistentPreference und Logged an. 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.

Default-Methoden weitergedacht

Für diese Bausteine, also die erweiterten Schnittstellen, gibt es viele Anwendungsfälle. Da die Java-Bibliothek schon an die 20 Jahre alt ist, würden heute einige Typen anders aussehen. Dass sich Objekte mit equals(…) vergleichen lassen können, könnte heute zum Beispiel in einer erweiterten Schnittstelle stehen, etwa so:[ 169 ](Die Schnittstelle compiliert mit dem jetzigen Java SE nicht, da eine Default-Methode keine Methode aus Object überschreiben kann. )

interface Equals {

default boolean equals( Object that ) {

return this == that;

}

}

So müsste java.lang.Object die Methode nicht für alle vorschreiben, wobei das jetzt sicherlich kein Nachteil ist. Natürlich gilt das Gleiche auch für die hashCode()-Methode, die heutzutage aus einer erweiterten Schnittstelle Hashable stammen könnte.

Und java.lang.Number ist ein weiteres Beispiel. Die abstrakte Basisklasse für Werte repräsentierende Objekte deklariert die abstrakten Methoden doubleValue(), floatValue(), intValue(), longValue() und die konkreten Methoden byteValue() und shortValue(). Bisher erben AtomicInteger, AtomicLong, BigDecimal, BigInteger, Byte, Double, Float, Integer, Long, Short von dieser Oberklasse. Auch diese Funktionalität ließe sich mit einer erweiterten Schnittstelle umsetzen.

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; und selbst wenn die Schnittstelle eine modifizierbare Datenstruktur referenzieren würde, würde jeder Nutzer des Container-Bausteins von den Veränderungen betroffen sein. 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«[ 170 ](http://mail.openjdk.java.net/pipermail/lambda-dev/2012-July/005171.html) nennt. Wie das geht, 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 6.63SortableDemo.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 6.64SortableDemo.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 6.65SortableDemo.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() ) );

}

}

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, ob sie nun eine abstrakte oder eine Default-Methode ist. Es wäre schön, wenn die Datenzugriffsmethoden nicht öffentlich sind, aber das geht nicht.

Wo wir gerade bei der Sichtbarkeit sind: Gibt es im Default-Code Codeduplizierung, so kann der gemeinsame Code bisher nicht in private Methoden ausgelagert werden, da es private Operationen in Schnittstellen nicht gibt. Allerdings läuft gerade ein Test, ob so etwas eingeführt werden soll.

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 Container-Baustein, 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 es ein großartiges 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, 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, 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.«[ 171 ](http://mail.openjdk.java.net/pipermail/lambda-dev/2012-July/005166.html)

Daher: Es ist eine schöne Spielerei, aber der Zustand sollte eine Aufgabe der abstrakten Basisklassen oder vom Delegate 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 delegiert. 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 in Frage kommt. Wir sollten uns dann noch einmal an die Unterschiede erinnern: Eine abstrakte Klasse kann Methoden aller Sichtbarkeiten haben und sie auch final setzen, sodass sie nicht mehr überschrieben werden können. Eine Schnittstelle dagegen ist mit puren virtuellen und öffentlichen Methoden darauf ausgelegt, dass die Implementierung überschrieben werden kann.

 

Zum Seitenanfang

6.7.14Initialisierung von Schnittstellenkonstanten * Zur vorigen ÜberschriftZur nächsten Überschrift

Eine Schnittstelle kann Attribute deklarieren, aber das sind dann immer initialisierte public static final-Konstanten. Nehmen wir eine eigene Schnittstelle PropertyReader an, die in einer Konstanten ein Properties-Objekt für Eigenschaften referenziert und eine Methode getProperties() für implementierende Klassen vorschreibt:

import java.util.Properties;



public interface PropertyReader {



Properties DEFAULT_PROPERTIES = new Properties();



Properties getProperties();

}

Würden wir DEFAULT_PROPERTIES nicht mit new Properties() initialisieren, gäbe es einen Compilerfehler, da ja jede Konstante final ist, also einmal belegt werden muss.

[»]Hinweis

Referenziert eine Schnittstelle eine veränderbare Datenstruktur (wie Properties), dann muss uns die Tatsache bewusst sein, dass die Datenstruktur als statische Variable global ist. Das heißt, alle implementierenden Klassen teilen sich diese Datenstruktur.

Nun stellt sich ein Problem, wenn die statischen Attribute nicht einfach mit einem Standardobjekt initialisiert werden sollen, sondern wenn zusätzlicher Programmcode zur Initialisierung gewünscht ist. Für unser Beispiel soll das Properties-Objekt unter dem Schlüssel date die Zeit speichern, zu der die Klasse initialisiert wurde. Über statische Initialisierer ist dies jedenfalls nicht möglich:

import java.util.*;



public interface PropertyReader {



Properties DEFAULT_PROPERTIES = new Properties();



static { // inline Compilerfehler: "Interfaces can't have static initializers"

DEFAULT_PROPERTIES.setProperty( "date", LocalDate.now().toString() );

}



Properties getProperties();

}

Zwar sind statische Initialisierungsblöcke nicht möglich, aber mit drei Tricks kann die Initialisierung erreicht werden. Wir müssen dazu etwas auf innere Klassen vorgreifen, ein Thema, das Kapitel 8, »Äußere.innere Klassen«, genauer aufgreift.

Konstanteninitialisierung über anonyme innere Klassen, Lösung A

Eine innere anonyme Klasse formt eine Unterklasse, sodass im Exemplarinitialisierer das Objekt (bei uns die Datenstruktur) initialisiert werden kann:

import java.util.*;



public interface PropertyReader {



Properties DEFAULT_PROPERTIES = new Properties() { {

setProperty( "date", LocalDate.now().toString() );

} };



Properties getProperties();

}

Ein Beispielprogramm zeigt die Nutzung:

Listing 6.66SystemPropertyReaderDemo.java

import java.util.Properties;



public class SystemPropertyReaderDemo implements PropertyReader {



@Override public Properties getProperties() {

return System.getProperties();

}



public static void main( String[] args ) {

System.out.println( PropertyReader.DEFAULT_PROPERTIES ); // {date=Thu ...

}

}

Die vorgeschlagene Lösung funktioniert nur, wenn Unterklassen möglich sind; finale Klassen fallen damit raus.

Konstanteninitialisierung über statische innere Klassen, Lösung B

Mit einem anderen Trick lassen sich auch diese Hürden nehmen. Die Idee liegt in der Einführung zweier Hilfskonstrukte:

  • einer inneren statischen Klasse, die wir $$ nennen wollen. Sie enthält einen statischen Initialisierungsblock, der auf DEFAULT_PROPERTIES zugreift und das Properties-Objekt initialisiert.

  • einer Konstante $ vom Typ $$. Als public static final-Variable initialisieren wir sie mit new $$(), was dazu führt, dass die JVM beim Laden der Klasse $$ den static-Block abarbeitet und so das Properties-Objekt belegt.

Da leider innere Klassen und Konstanten von Schnittstellen nicht privat sein können und so unglücklicherweise von außen zugänglich sind, geben wir ihnen die kryptischen Namen $ und $$, sodass sie nicht so attraktiv erscheinen:

Listing 6.67PropertyReader.java

import java.util.*;



public interface PropertyReader {



Properties DEFAULT_PROPERTIES = new Properties();



$$ $ = new $$();



static final class $$ {

static {

DEFAULT_PROPERTIES.setProperty( "date", LocalDate.now().toString() );

}

}



Properties getProperties();

}

Innerhalb vom static-Block lässt sich auf das Properties-Objekt zugreifen, und somit lassen sich auch die Werte eintragen. Ohne die Erzeugung des Objekts $ geht es nicht, denn andernfalls würde die Klasse $$ nicht initialisiert werden. Doch es gibt eine weitere Variante, die sogar ohne die Zwischenvariable $ auskommt.

Konstanteninitialisierung über statische innere Klassen, Lösung C

Bei der dritten Lösung gehen wir etwas anders vor. Wir bauen kein Exemplar mit DEFAULT_PROPERTIES = new Properties() auf, sondern initialisieren DEFAULT_PROPERTIES mit einer Erzeugermethode einer eigenen internen Klasse, sodass die Initialisierung zu DEFAULT_PROPERTIES = $$.$() wird:

Listing 6.68PropertyReader2.java

import java.util.*;



public interface PropertyReader2 {



Properties DEFAULT_PROPERTIES = $$.$();



static class $$ {

static Properties $() {

Properties p = new Properties();

p.setProperty( "date", LocalDate.now().toString() );

return p;

}

}



Properties getProperties();

}

Mit dieser Lösung kann prinzipiell auch das Aufbauen eines neuen Properties-Exemplars in $() entfallen und können etwa schon vorher aufgebaute Objekte zurückgegeben werden.

[»]Hinweis

Aufzählungen über enum können einfacher initialisiert werden.

 

Zum Seitenanfang

6.7.15Markierungsschnittstellen * 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 18, »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

6.7.16(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, doch 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.

Im Design ist weiterhin der Grundgedanke bei Schnittstellen: Wenn es Vorschriften für Verhalten ist, dann ist eine Schnittstelle goldrichtig. Bei Basisimplementierungen kommen dann abstrakte Klassen ins Spiel, 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 f( T t ) {

t.m();

}

Fordert die Methode ein Argument vom Typ T und ruft auf dem Parameter 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.

 


Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.

>> Zum Feedback-Formular
<< zurück
 Zum Katalog
Zum Katalog: Java ist auch eine Insel Java ist auch eine Insel

Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?

Ihre Meinung



 Buchempfehlungen
Zum Katalog: Java ist auch eine Insel

Java ist auch eine Insel




Zum Katalog: Java SE 9-Standard-Bibliothek

Java SE 9-Standard-Bibliothek




Zum Katalog: Professionell entwickeln mit Java EE 8

Professionell entwickeln mit Java EE 8




Zum Katalog: Entwurfsmuster

Entwurfsmuster




Zum Katalog: IT-Projektmanagement

IT-Projektmanagement




 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich

InfoInfo



 

 


Copyright © Rheinwerk Verlag GmbH 2017

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