6.12 Schnittstellen
Eine Schnittstelle1(interface) enthält keine Implementierungen, sondern nur Namen und Signaturen der enthaltenen Methoden. Anders gesagt, sie sind vollständig abstrakte Klassen.
Beispiel Definiere die Schnittstelle Unterhaltend mit einer Funktion unterhaltungswert()
Listing 6.47 vd/Unterhaltend.java
package vd;
interface Unterhaltend
{
int unterhaltungswert();
}
Die von den Schnittstellen definierten Funktionen sind - wie auch bei abstrakten Methoden - mit einem Semikolon abgeschlossen und haben niemals eine Implementierung.
|
Die Definiten einer Schnittstelle erinnert an eine abstrakte Klasse mit abstrakten Methoden, nur anstatt class steht das Schlüsselwort interface, und bei den abstrakten Klassen muss der Modifizierer abstract nicht geschrieben werden; alle Methoden in Schnittstellen sind automatisch abstrakt und auch öffentlich. (Der Compiler akzeptiert natürlich redundante Angaben von abstract und public. Sie sollten aber nicht geschrieben werden.)
Eine Schnittstelle darf keinen Konstruktor definieren. Das ist auch klar, da Exemplare von Schnittstellen nicht erzeugt werden können.
Obwohl in einer Schnittstelle keine Funktionen ausprogrammiert werden und keine Objektvariablen deklariert werden dürfen, sind static final-Variablen (benannte Konstanten) in einer Schnittstelle erlaubt. Statische Funktionen sind jedoch nicht erlaubt.
Hier klicken, um das Bild zu Vergrößern
Existiert eine Klasse, in der Methoden in einer neuen Schnittstelle definiert werden sollen, lässt sich Refactor, Extract Interface... einsetzen. Es folgt ein Dialog, der uns Methoden auswählen lässt, die später in einer neuen Schnittstelle definiert 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.
Implementieren von Schnittstellen
Möchte eine Klasse eine Schnittstelle verwenden, so folgt hinter dem Klassennamen das Schlüsselwort implements.
Beispiel Lasse die Klasse Kirche die Schnittstelle Unterhaltend implementieren
Listing 6.48 vd/Kirche.java
package vd;
class Kirche extends Gebaeude implements Unterhaltend
{
int anzahlBeichtstühle;
int verfügbarerMesswein;
/**
* Liefert den Unterhaltungswert der Kirche.
*
* @return Unterhaltungswert.
*/
public int unterhaltungswert()
{
return anzahlBeichtstühle * verfügbarerMesswein * verfügbarerMesswein;
}
}
|
Hier klicken, um das Bild zu Vergrößern
Abbildung 6.10 Kirche und Disko sind Gebäude und implementieren Unterhaltend
Hinweis Bei einer Implementierung einer Schnittstelle müssen die Methoden in den Unterklassen öffentlich implementiert werden, da die Methoden in Schnittstellen immer automatisch public sind.
|
Eine Klasse kann mehrere Schnittstellen implementieren. Dann muss sie natürlich auch alle vorgeschriebenen Funktionen implementieren. Implementiert eine Schnittstelle nicht alle Funktionen aus den Schnittstellen, so erbt sie damit eine abstrakte Funktion und muss selbst wieder als abstrakt gekennzeichnet werden.
Hinweis zur Ausdrucksweise: Klassen werden vererbt und Schnittstellen implementiert.
|
Markierungsschnittstellen
Auch Schnittstellen ohne Methoden sind möglich. Diese leeren Schnittstellen werden Markierungsschnittstellen (engl. marker interface) genannt. Sie sind nützlich, da mit instanceof leicht überprüft werden kann, ob sie einen gewollten Typ einnehmen.
Beispiel Die Sun-Bibliothek bringt einige Markierungsschnittstellen schon mit. Eine davon ist java.io.Serializable.
interface Serializable
{
}
|
Was bringen Schnittstellen?
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. Denn über Schnittstellen lässt sich eine Sicht auf ein Objekt beschreiben. Jede Schnittstelle definiert solch eine Sicht, eine Art Rolle. Implementiert eine Klasse diverse Schnittstellen, so können ihre Exemplare in verschiedenen Rollen auftreten. Hier wird erneut das Substitutionsprinzip wichtig, bei dem ein mächtigeres Objekt verwendet wird, auch wenn weniger erwartet wird.
6.12.1 Ein Polymorphie-Beispiel mit Schnittstellen
An dieser Stelle sei noch einmal an die Möglichkeit erinnert, Funktionen auf Objekte auszuführen, die eine gemeinsame Basis haben. Wir haben Disko und Kirche als zwei Klassen, die Unterhaltend implementieren und somit über die Funktion unterhaltungswert() verfügen. Diese Unterhaltungs-Klassen wollen wir in eine Container-Klasse aufnehmen, dem Unterhaltungsprogramm. Die Klasse soll neue Unterhaltende »Dinge« aufnehmen können. Dabei ist es der Klasse ziemlich egal, ob die Elemente Diskotheken, Kirchen oder Unterwäsche sind - Hauptsache die Elemente implementieren die Schnittstelle Unterhaltend. Die interessante Funktion ist daher:
void fügeHinzu( Unterhaltend u )
Intern sollen die Unterhaltend-Objekte in einem Feld ebenfalls vom Typ Unterhaltend gespeichert werden.
Hier klicken, um das Bild zu Vergrößern
Listing 6.49 vc/Unterhaltungsprogramm.java
package vd;
public class Unterhaltungsprogramm
{
/**
* Feld für die Unterhaltungs-Objekte.
*/
private Unterhaltend programm[] = new Unterhaltend[ 10 ];
/**
* Aktuelle Anzahl von Unterhaltungs-Objekten.
*/
private int pos;
/**
* Fügt ein neues Unterhaltungs-Objekt in das Unterhaltungsprogramm ein.
*
* @param u Hinzuzufügendes Unterhaltungs-Objekt.
*/
public void fügeHinzu( Unterhaltend u )
{
if ( pos < 10 )
programm[ pos++ ] = u;
else
System.out.println( "Programm voll!" );
}
/**
* Liefert den Gesamtunterhaltungswert aller Unterhaltungs-Objekte.
*/
public double unterhaltungswert()
{
int uw = 0;
for ( int i = 0; i < pos; i++ )
uw += programm[ i ].unterhaltungswert();
return uw;
}
}
Die Polymorphie tritt in der Funktion unterhaltungswert() auf. Dort rufen wir auf jedem Objekt, was Unterhaltend implementiert, die Funktion unterhaltungswert() auf. Durch die Summation der unterschiedlichen Unterhaltungswerte bekommen wir den Unterhaltungswert des gesamten Unterhaltungsprogramms. Auch die Klasse Unterhaltungsprogramm könnte wiederum Unterhaltend implementieren. So implementiert sich schnell das so genannte Composite-Pattern.
Im Zusammenhang mit Schnittstellen bleibt zusammenfassend zu sagen, dass hier dynamisches Binden pur auftaucht.
6.12.2 Die Mehrfachvererbung bei Schnittstellen
Bei Klassen gibt es die Einschränkung, dass nur von einer direkten Oberklasse abgeleitet werden darf. Wird hingegen eine Schnittstelle implementiert, dann werden nicht mehr aus verschiedenen Quellen unterschiedliche Implementierungen für dieselbe Methode angeboten, was zu Problemen führen kann. Der Unterschied zu Klassen ist jetzt, dass mehrere Schnittstellen von einer einzigen Klasse implementiert werden können. Dies wird gelegentlich als »Mehrfachvererbung in Java« bezeichnet.
Eine Disko soll nicht nur die Methode unterhaltungswert() von Unterhaltend implementieren, sondern sich auch mit anderen Diskotheken vergleichen können. Dazu gibt es schon eine passende Schnittstelle von Sun: java.lang.Comparable. Wir fordern, dass unsere Disko auch die Methode compareTo(Object) implementiert.
Listing 6.50 ve/Disko.java
package ve;
public class Disko implements Unterhaltend, Comparable
{
int anzahlPersonen;
int quadratmeter;
public int unterhaltungswert()
{
return (int) (anzahlPersonen * Math.sqrt( quadratmeter ));
}
/**
* Vergleicht ein anderes Disko-Objekt mit unserem aufgrund des
* Unterhaltungswerts.
*
* @param other Die Disko, die sich mit uns vergleicht.
*
* @return Einen Wert kleiner Null, wenn wir einen kleinen Unterhaltungswert haben
* als die andere Disko. 0, wenn beide gleich unterhaltend sind. Einen
* Wert größer Null, wenn unsere Disko unterhaltender ist.
*
* @throws IllegalArgumentException wenn das Argument keine Disko ist.
*
* @see java.lang.Comparable
*/
public int compareTo( Object other )
{
if ( other instanceof Disko )
return this.unterhaltungswert() - ((Disko)other).unterhaltungswert();
throw new IllegalArgumentException( "Kann mich nur mit Diskos vergleichen" );
}
}
Durch diese Mehrfachvererbung bekommt Disko zwei Obertypen, so dass sich je nach Sichtweise schreiben lässt:
Object d1 = new Disko();
Unterhaltend d2 = new Disko();
Comparable d3 = new Disko();
Disko d4 = new Disko();
Dabei ist über d2 die Methode compareTo() und für d3 die Methode unterhaltungswert() nicht definiert. Für d4 sind dagegen alle Methoden sichtbar.
Ein kleines Beispiel zeigt abschließend die Anwendung der Funktion compareTo().
package ve;
public class DiskoKritiker
{
public static void main( String args[] )
{
Disko d1 = new Disko();
d1.quadratmeter = 1230; d1.anzahlPersonen = 891;
Disko d2 = new Disko();
d2.quadratmeter = 2390; d2.anzahlPersonen = 1091;
System.out.println( d1.unterhaltungswert() ); // 31248
System.out.println( d2.unterhaltungswert() ); // 53336
System.out.println( d1.compareTo(d2) ); // -22088
}
}
Keine Kollisionen bei Mehrfachvererbung
Da Dilemma bei der Mehrfachvererbung von Klassen wäre, dass zwei Oberklassen die gleiche Funktion mit zwei unterschiedlichen Implementierungen vererben könnte. 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 Funktion vorschreiben würden, gäbe es keine zwei verschiedenen Implementierungen von Anwendungslogik. Die implementierende Klasse bekommt sozusagen zweimal die Aufforderung, die Operation zu implementieren.
Beispiel Zwei Schnittstellen schreiben die gleiche Funktion vor. Eine Klasse implementiert beide Schnittstellen.
|
Listing 6.51 Saddam.java
interface Politisch
{
void geldHer();
}
interface Verbrecherisch
{
void geldHer();
}
public class Saddam implements Politisch, Verbrecherisch
{
public void geldHer()
{
}
}
|
6.12.3 Erweitern von Interfaces - Subinterfaces
Ein Subinterface ist die Erweiterung eines anderen Interfaces. Diese Erweiterung erfolgt - wie bei der Vererbung - durch das Schlüsselwort extends.
Beispiel Eine Schnittstelle erbt von einer anderen Schnittstelle.
interface Einschlafend extends Unterhaltend
{
double schlafwert();
}
|
Eine Klasse, die nun Einschlafend implementiert, muss die Methoden von beiden Schnittstellen implementieren, demnach die Methode unterhaltungswert() aus Unterhaltend sowie die Methode schlafwert(), die in Einschlafend selbst angegeben wurde.
6.12.4 Vererbte Konstanten bei Schnittstellen
Schnittstellen können Variablen besitzen, die jedoch, wie wir gesehen haben, immer automatisch statisch und final, also Konstanten, sind. Diese Konstanten können einer anderen Schnittstelle vererbt werden. Es gibt dabei einige kleine Einschränkungen.
Wir wollen an einem Beispiel sehen, wie sich die Vererbung auswirkt, wenn gleiche Bezeichner in den Unterschnittstellen erneut verwendet werden. Das nachfolgende Programm implementiert folgendes UML-Diagramm:
Hier klicken, um das Bild zu Vergrößern
Listing 6.52 VererbteSchnittstellen.java
interface Grundfarben
{
int ROT = 1;
int GRÜN = 2;
int BLAU = 3;
}
interface Sockenfarben extends Grundfarben
{
int SCHWARZ = 10;
int LILA = 11;
}
interface Hosenfarben extends Grundfarben
{
int LILA = 11;
int SCHWARZ = 20;
int BRAUN = 21;
}
interface Allefarben extends Sockenfarben, Hosenfarben
{
int BRAUN = 30;
}
public class VererbteSchnittstellen
{
public static void main( String args[] )
{
System.out.println( Sockenfarben.ROT ); // 1
System.out.println( Allefarben.ROT ); // 1
System.out.println( Hosenfarben.SCHWARZ ); // 20
// System.out.println( Allefarben.SCHWARZ );
// The field name "SCHWARZ" is an ambiguous name
//found in the types "Sockenfarben" and "Hosenfarben".
// System.out.println( Allefarben.LILA );
}
}
Die Definitionen der Schnittstellen können ohne Fehler übersetzt werden. Das Programm zeigt im Wesentlichen vier Eigenschaften:
1. |
Schnittstellen vererben ihre Eigenschaften an die Unterschnittstellen. Es erbt Sockenfarben das Attribut ROT aus Grundfarben. |
2. Erbt eine Schnittstelle von mehreren Oberklassen, die jeweils ein bestimmtes Attribut von einer gemeinsamen Oberklasse beziehen, so ist dies kein Fehler. Es erbt etwa Allefarben von Sockenfarben und Hosenfarben das Attribut ROT aus Grundfarben.
3. Konstanten dürfen überschrieben werden. Es überschreibt Hosenfarben die Farbe SCHWARZ aus Sockenfarben mit dem Wert 20. Auch LILA wird überschrieben. Obwohl die Konstante mit dem gleichen Wert belegt ist, handelt es sich um ein Überschreiben. Wird jetzt der Wert Hosenfarben.SCHWARZ verlangt, liefert die Umgebung den Wert 20.
4. Unterschnittstellen können aus zwei Oberschnittstellen die Attribute gleichen Namens übernehmen, auch wenn sie den gleichen Wert haben. Dann muss nur bei der Benutzung ein qualifizierter Name verwendet werden, der deutlich macht, welches Attribut gemeint ist, also zum Beispiel Sockenfarben.SCHWARZ. Das zeigt sich an den beiden Beispielen Allefarben.SCHWARZ und Allefarben.LILA. Die schwarze Farbe ist in den Oberschnittstellen Sockenfarben und Hosenfarben unterschiedlich initialisiert. Ähnliches gilt für die Farbe LILA. Obwohl LILA in beiden Fällen den Wert 11 trägt, ist das nicht erlaubt. Das ist ein guter Schutz gegen Fehler, denn wenn der Compiler dies durchlassen würde, könnte sich im Nachhinein die Belegung von LILA in Sockenfarben oder Hosenfarben 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. Zudem kann und sollte der Compiler für alle Konstanten die Werte direkt einsetzen.
6.12.5 Vordefinierte Methoden einer Schnittstelle
Der Typ eines Objekts bei der Deklaration einer Referenz kann entweder ein Objekt oder eine Schnittstelle sein. Ist die Referenz vom Typ einer Schnittstelle, dann ist es bemerkenswert, dass der Compiler erlaubt, Methoden der Klasse Object für diese Referenz aufzurufen.
Beispiel Wir definieren eine Schnittstelle ohne besondere Methoden und eine Klasse, die keine besonderen Methoden hinzufügt.
Listing 6.53 InterfaceObjectReference.java
interface I
{
}
class C implements I
{
}
|
Erzeugen wir ein Objekt vom Typ C, so kennt C automatisch alle Methoden aus C (keine) und zusätzlich, aufgrund des impliziten extends Object, auch die Methoden aus Object. Daher lässt sich schreiben:
C ref = new C();
ref.toString();
Die Schnittstelle spielt hier noch keine Rolle. Was jedoch auch funktioniert ist Folgendes:
I ref = new C();
ref.toString();
Es ist zu erwarten, dass ref nur Methoden aus I nutzen kann und das sind keine! Da allerdings jede Klasse automatisch von Object erbt und damit die Methoden besitzt, erlaubt der Compiler Zugriff auf diese Eigenschaften. So lässt sich vereinfacht sagen, dass alle Methoden von Object erlaubt sind, auch wenn ein Interface selbst diese Methoden nicht besitzt. Jede Schnittstelle ist somit eine indirekte Erweiterung von Object.
Schnittstellenmethoden, die nicht implementiert werden müssen
Bis auf eine Ausnahme muss eine Klasse, zu der Exemplare erzeugt werden sollen, alle Methoden der Schnittstellen implementieren. Eine Ausnahme ergibt sich wieder aus der Tatsache, dass jede Schnittstelle die Methoden von Object annimmt. Sehen wir uns den Programmcode der Schnittstelle Comparator an, die im Paket java.util definiert ist:
package java.util;
public interface Comparator
{
int compare( Object o1, Object o2 );
boolean equals( Object obj );
}
Wir entdecken, dass dort die equals()-Methode vorgeschrieben wird. Der erste Gedanke ist, nun eine Klasse zu schreiben, die compare() und equals() implementieren muss. Dies ist hier allerdings nicht nötig. Denn equals() ist schon eine Methode, die jedes Objekt besitzt. Daraus ergibt sich, dass nicht alle Methoden ausprogrammiert werden müssen. (Eventuell überschreiben wir equals(), wenn uns die Semantik von equals() in Object nicht gefällt.) Weiter lässt sich eine Schnittstelle angeben, die die Methoden von Object auflistet. Auch dann müsste keine Methode implementiert werden. Bleibt die Frage, warum denn Comparator eine equals()-Methode vorschreibt, wenn diese doch nicht implementiert zu werden braucht. Um uns zu verwirren? Nein. Der Sinn besteht einfach in der genauen Angabe der Funktionsweise in der Dokumentation. Eine Java-Dokumentation kann nur generiert werden, wenn auch eine Funktion im Quellcode vorhanden ist. Die Entwickler wollten bei equals() in der Schnittstelle Comparator noch einmal bewusst auf die Funktion hinweisen, dass equals() zwei Comparator-Objekte daraufhin vergleicht, ob beide die gleiche Sortierfolge verwenden, und nicht, wie wir annehmen könnten, zwei Objekte auf Gleichheit testet.
6.12.6 CharSequence als Beispiel einer Schnittstelle
Bisher kennen wir die Klassen String und StringBuffer, um Zeichenketten zu speichern und weiterzugeben. Ein String ist ein Wertobjekt und ein wichtiges Hilfsmittel in Programmen, da durch diesen unveränderliche Zeichenkettenwerte repräsentiert werden, während StringBuffer veränderliche Zeichenfolgen umfasst. Aber wie sieht es aus, wenn eine Teilzeichenkette gefordert ist, bei der es egal sein soll, ob das Original als String- oder StringBuffer-Objekt vorliegt?
Eine Lösung ist, alles in ein String-Objekt zu konvertieren. Möchte ein Programm eine Teilfolge liefern, auf die jemand lesend zugreifen möchte, die er aber nicht verändern können soll, ist ein String zu träge. Aus den beliebigen Zeichenfolgen müsste zuerst ein String-Objekt konstruiert werden. Daher haben die Entwickler seit dem SDK 1.4 die Schnittstelle CharSequence eingefügt, die eine unveränderliche, nur lesbare Sequenz von Zeichen realisiert. Die Schnittstelle wird von StringBuffer und String implementiert, so dass sich alle Zeichenketten dieser Klassen als CharSequence auszeichnen. Funktionen müssen sich also nicht mehr für String oder StringBuffer entscheiden, sondern können einfach ein CharSequence-Objekt als Parameter akzeptieren. Ein String und ein StringBuffer-Objekt können zwar mehr als CharSequence vorschreibt, aber beide lassen sich als CharSequence einsetzen, wenn das »Mehr« an Funktionalität nicht benötigt wird.
Hier klicken, um das Bild zu Vergrößern
interface java.lang.CharSequence
|
|
char charAt( int index )
Liefert das Zeichen an der Stelle index. |
|
int length()
Gibt die Länge der Zeichensequenz zurück. |
|
CharSequence subSequence( int start, int end )
Liefert eine neue CharSequence von start bis end. |
|
String toString()
Gibt einen String der Sequenz zurück. Die Länge des toString()-Strings entspricht genau der Länge der Sequenz. |
Beispiel Soll eine Methode eine Zeichenkette bekommen und die Herkunft ist egal, so schreiben wir etwa:
void giveMeAText( CharSequence s )
{
...
}
anstatt
void giveMeAText( String s )
{
...
}
void giveMeAText( StringBuffer s )
{
void giveMeAText( new String(s) ); // oder Ähnliches
}
|
Anwendung von CharSequence in String
In den Klassen String und StringBuffer existiert eine Methode subSequence(), die ein CharSequence-Objekt liefert. Die Signatur ist in beiden Fällen die gleiche. Die Funktion macht im Prinzip nichts anderes als ein substring(begin, end).
class java.lang.String implements CharSequence, Serializable
class java.lang.StringBuffer implements CharSequence, Serializable
|
|
CharSequence subSequence( int beginIndex, int endIndex )
Liefert eine neue Zeichensequenz von String beziehungsweise StringBuffer. |
Die Implementierung sieht so aus, dass mit substring() ein neuer Teilstring zurückgeliefert wird. Das ist eine einfache Lösung, aber nicht unbedingt die schnellste. Für String-Objekte ist das Erzeugen von Substrings ziemlich schnell, da die Methode speziell optimiert ist. Da Strings unveränderlich sind, wird einfach das gleiche char-Feld wie im Original-String verwendet, nur eine Verschiebung und ein Längenwert werden angepasst.
1 Äquivalent zu denen in Objective-C
|