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 Die Klassenbibliothek
14 Einführung in die nebenläufige Programmierung
15 Einführung in Datenstrukturen und Algorithmen
16 Einführung in grafische Oberflächen
17 Einführung in Dateien und Datenströme
18 Einführung ins Datenbankmanagement mit JDBC
19 Einführung in <XML>
20 Testen mit JUnit
21 Bits und Bytes und Mathematisches
22 Die Werkzeuge des JDK
A Java SE Paketübersicht
Stichwortverzeichnis

Download:
- Beispielprogramme, ca. 20,0 MB
- Übungsaufgaben, ca. 1,8 MB
- Musterlösungen, ca. 0,8 MB

Buch bestellen
Ihre Meinung?

Spacer
<< zurück
Java ist auch eine Insel von Christian Ullenbloom
Das umfassende Handbuch
Buch: Java ist auch eine Insel

Java ist auch eine Insel
Rheinwerk Computing
1306 Seiten, gebunden, 11. Auflage
49,90 Euro, ISBN 978-3-8362-2873-2
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 Typanpassung
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 Konstruktor-Aufrufen *
Pfeil 6.5.5 Eine letzte Spielerei mit Javas dynamischer Bindung und überschatteten 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 noch 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.

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

UML-Diagramm der Schnittstelle Buyable

Abbildung 6.9UML-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.47com/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.

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

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.

 
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.48com/tutego/insel/game/vk/Chocolate.java, Chocolate

public class Chocolate implements Buyable {
@Override public double price() {
return 0.69;
}
}
Die Klassen Magazine und Chocolate implementieren Buyable.

Abbildung 6.10Die Klassen Magazine und Chocolate implementieren Buyable.

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.49com/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.

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

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, wird die Ansicht umgedreht, dann stehen die Obertypen unten, was den Vorteil hat, dass auch die implementierte Schnittstelle unter den Obertypen ist.

 
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, da sich über Schnittstellen ganz unterschiedliche Sichten auf ein Objekt beschreiben lassen. 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ächtigeres Objekt verwendet wird, obwohl je nach Kontext nur die Methode der Schnittstellen erwartet wird.

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.50com/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.51com/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.52com/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[ 158 ]):

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

interface Buyable {
double price();
}

Listing 6.54com/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 später 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.11Die 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 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.55com/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 es natürlich mit unserer früheren Methode calculateSum(…) aufrufen, da jedes Magazine ja Buyable ist:

Listing 6.56com/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.57com/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.58com/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 Feld, 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 Methode vorschreiben würden, gäbe es keine zwei verschiedenen Implementierungen von Anwendungslogik. Die implementierende Klasse bekommt sozusagen zweimal die Aufforderung, die Operation zu implementieren. So wie bei folgendem Beispiel: Ein Politiker muss verschiedene Dinge vereinen: Er muss sympathisch sein, aber auch durchsetzungsfähig handeln können.

Listing 6.59Politician.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.12Eine Klasse erbt von zwei Schnittstellen die gleiche Operation.

 
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.

 
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]Beispiele

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.

[zB]Beispiel und Tipp

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

interface Vulcano {
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, einmal 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.60Colors.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 );// The field FlyingCarColors.WHITE is ambiguous
System.out.println( FlyingCarColors.GREY ); // The 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 überschattet 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 CarColors.BLACK 1 ergibt. Auch PlaneColor ü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 den Eintrag WHITE, BLACK und GREY.

  • Unterschnittstellen können aus zwei Oberschnittstellen die Attribute gleichen Namens übernehmen, auch wenn sie 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ändlicher 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.

Ab Java 8 lassen sich in Schnittstellen statische Methoden unterbringen und als Utility-Methoden neben Konstanten stellen. Als statische Methoden werden sie nicht dynamisch gebunden.

[zB]Beispiel

Im vorangehenden Kapitel hatten wir schone 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 einen Modifizierer tragen, wenn nicht, gelten sie als abstrakte Operationen.

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.2Erlaubte 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 automatisch 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 Schnittstellten zu Fehlern im Quellcode führen, sind einige Änderungen für die JVM in Ordnung. Wir nennen das Binär-Kompatibilität. Wenn zum Beispiel nur die Schnittstelle selbst neu übersetzt und in den Klassepfad 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är-kompatibel sind und zum 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 veröffentlicht 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.[ 159 ]

Seit Java 8 gibt es eine weitere Möglichkeit, Operationen in Schnittstellen hinzuzufügen, so genannte Default-Methoden. Sie erweitern die Schnittstelle, bringen aber gleich schon eine vorgefertigte Implementierung mit, sodass Unterklassen nicht zwingend eine Implementierung anbieten müssen.

 
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. Java 8 bringt dafür eine Sprachänderung mit, die es Entwicklern erlaubt, 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[ 160 ]. 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.[ 161 ]

  • Statt eines Semikolons markiert bei einer Default-Methode ein Block mit der Implementierung in geschweiften Klammen 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, 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 bedauern Entwickler Designentscheidungen. 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. Weiterhin 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.[ 162 ] 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, wenn 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, um die Schnittstellen nicht so codelastig werden zu lassen. 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 TrueLoveNotPricedPriced.

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 ein 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 Buyable. 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, das größer als 0 ist. PeanutsBuyable dagegen nutzt eine erweiterte Definition und implementiert daher das Default-Verhalten neu. Nach den berühmten kopperschen Peanuts[ 163 ] 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 eine interessante Eigenschaft, dass Untertypen den Status von »haben Implementierung« auf »habe keine 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 weiterhin 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 vom Compiler 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 Buyable nimmt diese wieder weg, sodass erweiternde Buyable-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 soweit 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 sie 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 alleine 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 Java 8 gibt es aber eine ganz neue Möglichkeit, und zwar mit den erweiterten Schnittstellen: Sie bilden die Bausteine, von denen Klassen Funktionalität bekommen können. Andere Programmiersprachen bieten so etwas Ähnliches, und das Konzept wird dort Mixin oder Trait genannt.[ 164 ] 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 weiter gedacht

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: interface Equals { boolean equals( Object that ) default { 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«[ 165 ] 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, und zudem soll weiterhin eine neue Methode sort() 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 besitzt Methoden, die die Klasse implementieren muss, die dem Sortierer die Daten geben. Im Quellcode sieht das so aus:

Listing 6.61SortableDemo.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 );
};
}

Damit sort() an die Daten kommt, erwartet Sortable von den implementieren 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.62SortableDemo.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.63SortableDemo.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 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.«[ 166 ]

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 sie 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 { // Compilerfehler: "Interfaces can't have static initializers"
DEFAULT_PROPERTIES.setProperty( "date", new Date().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", new Date().toString() );
} };

Properties getProperties();
}

Ein Beispielprogramm zeigt die Nutzung:

Listing 6.64SystemPropertyReaderDemo.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.65PropertyReader.java

import java.util.*;

public interface PropertyReader {

Properties DEFAULT_PROPERTIES = new Properties();

$$ $ = new $$();

static final class $$ {
static {
DEFAULT_PROPERTIES.setProperty( "date", new Date().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.66PropertyReader2.java

import java.util.*;

public interface PropertyReader2 {

Properties DEFAULT_PROPERTIES = $$.$();

static class $$ {
static Properties $() {
Properties p = new Properties();
p.setProperty( "date", new Date().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 sie einen gewollten Typ einnehmen.

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 17, »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.16Abstrakte Klassen und Schnittstellen im Vergleich Zur vorigen ÜberschriftZur nächsten Überschrift

Eine abstrakte Klasse und eine Schnittstelle sind sich sehr ähnlich: Beide schreiben den Unterklassen bzw. den implementierten Klassen Operationen vor, die sie implementieren 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 zusätzlichen Programmcode enthalten, was Schnittstellen nicht können. Auch nachträgliche Änderungen an Schnittstellen sind nicht einfach: Einer abstrakten Klasse kann eine konkrete Methode mitgegeben werden, was zu keiner Quellcodeanpassung für Unterklassen führt.

Ein Beispiel: Ist eine Schnittstelle oder eine abstrakte Klasse besser, um folgende Operation zu deklarieren?

abstract class Timer { interface Timer {
abstract long getTimeInMillis(); long getTimeInMillis();
} }

Eine abstrakte Klasse hätte den Vorteil, dass später einfacher eine Methode wie getTimeInSeconds() eingeführt werden kann, die konkret sein darf. Würde diese angenehme Hilfsoperation in einer Schnittstelle vorgeschrieben, so müssten alle Unterklassen diese Implementierung immer neu einführen, wobei sie doch schon in der abstrakten Oberklasse einfach programmiert werden könnte:

abstract class Timer {

abstract long getTimeInMillis();

long getTimeInSeconds() {
return getTimeInMillis() / 1000;
}
}

 


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 SE 8 Standard-Bibliothek
Java SE 8 Standard-Bibliothek


Zum Katalog: Professionell entwickeln mit Java EE 7
Professionell entwickeln mit Java EE 7


Zum Katalog: Schrödinger programmiert Java
Schrödinger programmiert Java


Zum Katalog: Einführung in Java
Einführung in Java


Zum Katalog: Programmieren lernen mit Java
Programmieren lernen mit Java


Zum Katalog: Apps entwickeln für Android 5
Apps entwickeln für Android 5


Zum Katalog: Apps entwickeln mit Android Studio
Apps entwickeln mit Android Studio


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo

 
 


Copyright © Rheinwerk Verlag GmbH 2016
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.

 
Nutzungsbestimmungen | Datenschutz | Impressum

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