Schnittstellen deklarieren in Java
Eine abstrakte Klasse, die nur abstrakte Methoden enthält, definiert nur Signaturen, jedoch keine Implementationen. Richtig angewendet wird eine solche abstrakte Klasse dann, wenn zu den enthaltenen Signaturen jeweils Spezifikationen vorhanden sind, so daß die Signaturen mit diesen Spezifikation zusammen Operationen bilden. Außerdem sollten alle Operation einer Klasse auch sinngemäß zusammengehören.
Einer solchen abstrakten Klasse ähnlich ist eine Schnittstelle, also eine Menge von Prototypen (Signaturen und Rückgabetypen), die durch eine Schnittstellendeklaration bestimmt werden kann.
Ein Lautsprecher eines Rechners soll beispielsweise durch Software steuerbar sein. Zur Modellierung solch eines Lautsprechers kann eine Klasse definiert werden, die es erlaubt, den Lautsprecher ein- und auszuschalten sowie die Lautstärke einzustellen. Die Lautstärke ist dabei eine Zahl zwischen 0 (einschließlich) und 1 (ausschließlich).
In dem UML-Diagramm "LoudSpeaker" werden die Operationen der Klasse "LoudSpeaker" durch Zwischenüberschriften untergliedert in "«Switchable-Methods»" zum Ein- und Ausschalten sowie "«Volume-Methods»" für die Lautstärke.
LoudSpeaker [UML class diagram]
.-----------------------------------------------.
| LoudSpeaker |
|-----------------------------------------------|
|-----------------------------------------------|
| |
| «Switchable-Methods» |
| + switchOn() : boolean |
| + switchOff() : boolean |
| + isOn() : boolean |
| |
| «Volume-Methods» |
| + setVolume( double ) : double |
| + getVolume() : double |
'-----------------------------------------------'- Ein Lautsprecher mit zwei Schnittstellen
_------------------------.
_-' _-' |
.-------------------------. |
| | |
| Switchable | |
| .-----. | |
| |.----| | |
| '-----' | |
| | |
| Volume | |
| .-----. | |
| |.----| | |
| '-----' | |
| | |
| | -'
| |.'
'-------------------------'
Nun bildet ein Teil der Operationen des Lautssprechers eine Schnittstelle, wie sie auch ein Schalter hat: Der Lautsprecher kann mit der Wirkoperation "switchOn()" eingeschaltet und mit der Wirkoperation "switchOff()" ausgeschaltet werden, außerdem kann mit der Prädikat-Operation "isOn()" die Aussage formuliert werden, daß er eingeschaltet ist. Diese Schnittstelle kann ausdrücklich als Schnittstelle "Switchable" angegeben werden, die von der Klasse "LoudSpeaker" implementiert wird.
Switchable [UML static structure diagram]
.-----------------------------------------------.
| «interface» |
| Switchable |
|-----------------------------------------------|
|-----------------------------------------------|
| switchOn() : boolean |
| switchOff() : boolean |
| isOn() : boolean |
'-----------------------------------------------'
^
/_\
| «implements»
|
|
.-----------------------------------------------.
| LoudSpeaker |
|-----------------------------------------------|
|-----------------------------------------------|
| |
| «Switchable-Methods» |
| + switchOn() : boolean |
| + switchOff() : boolean |
| + isOn() : boolean |
| |
| «Volume-Methods» |
| + setVolume( double ) : void |
| + getVolume() : double |
'-----------------------------------------------'
Es ist möglich, daß Algorithmen auf der Schnittstelle arbeiten, die nur deren Operationen voraussetzen. Sie sind universell einsetzbar, weil sie nur etwas Schaltbares benötigen, aber nicht unbedingt einen Lautsprecher. So könnte es beispielsweise einen Algorithmus "turnOn" geben, der etwas Schaltbares einschaltet, falls es nicht schon eingeschaltet ist.
Switchable [diagram]
.------------------------. .-------------------------------.
| «algorithm» | | «interface» |
| turnOn | | Switchable |
|- - - - - - - - - - - - | |-------------------------------|
| !isOn() && switchOn() |---->|-------------------------------|
| | | switchOn() : boolean |
| | | switchOff() : boolean |
| | | isOn() : boolean |
'------------------------' '-------------------------------'
^
/_\
«implements» |
|
|
.-------------------------------.
| LoudSpeaker |
|-------------------------------|
|-------------------------------|
| |
| «Switchable-Methods» |
| + switchOn() : boolean |
| + switchOff() : boolean |
| + isOn() : boolean |
| |
| «Volume-Methods» |
| + setVolume( double ) : void |
| + getVolume() : double |
'-------------------------------'
Die Schnittstellendeklaration
Für das hier beschriebene Beispiel läßt sich eine Schnittstellendeklaration in Java schreiben, die ähnlich wie die Deklaration einer abstrakten Klasse mit nur abstrakte Methoden aussieht. Anstelle des Schlüsselwortes "class" wird bei der Schnittstellendeklaration das Schlüsselwort "interface" verwendet. Da in einer Schnittstelle grundsätzlich alle Anwendungstypen öffentlich und abstrakt sind, entfällt aber die Notwendigkeit zur Verwendung des entsprechenden Modifizierer "public" bzw. des Modifizierers "abstract".
Zu einer Schnittstelle gehören aus der Sicht von Java nur „formale“ Prototypen, aber keine „semantischen“ Operationen. Da semantisch bestimmte Operationen aber wünschenswert sind, werden diese annähernd geschaffen, indem wenigstens eine informelle Spezifikation in Dokumentationskommentaren gegeben wird.
Switchable.java [Interface]
/** Interface for objects which can be switched on or off.
* @version 1.00 05/29/03
* @author Stefan Ram */
interface Switchable
{ /** Try to switch on the object.
* @return true if the object is on after the operation, otherwise false. */
boolean switchOn();
/** Try to switch off the object.
* @return false if the object is off after the operation, otherwise true. */
boolean switchOff();
/** Determine whether the switch is on.
* @return true if the object is on, otherwise false. */
boolean isOn(); }
Die Deklarationen in der Schnittstelle "Switchable" sind Deklarationen abstrakter Methoden, sie haben also ein Semikolon ";" anstelle eine Methodenrumpfs (anstelle einer Block-Anweisung).
Falls die Schnittstelle in anderen Paketen sichtbar sein soll, muß ihrer Deklaration der Schnittstellenmodifizierer "public" vorangestellt werden, es wäre in der Quelldatei "Switchable.java" also beispielsweise der Quelltext "public interface" statt des Quelltexts "interface" zu schreiben. Die meisten Schnittstellenmodifizierer entsprechen also entsprechenden Klassenmodifizierern.
Benennung
Der Name einer Schnittstelle sollte mit einem großen Buchstaben beginnen.
Der Name sollte ein die Schnittstelle beschreibendes Nomen oder eine die Schnittstelle beschreibende Nominalphrase sein, der paßt, wenn die Schnittstelle verwendet wird. Der Name kann auch Adjektiv sein, welches das Verhalten beschreibt, das durch die Schnittstelle ermöglicht wird und dementsprechend oft mit "able" endet, wie in "Switchable" (schaltbar).
Die Schnittstellenimplementierung
Nachdem die Schnittstelle "Switchable" definiert wurde, kann nun beispielsweise eine Lautsprecherklasse "Loudspeaker" geschrieben werden, die sie implementiert.
Loudspeaker.java
class Loudspeaker implements Switchable
{ boolean on; double volume;
public boolean switchOn(){ on = true; return on; }
public boolean switchOff(){ on = false; return on; }
public boolean isOn(){ return on; }
double setVolume( final double v )
{ volume = v; return volume; }
double getVolume(){ return volume; }}
Durch eine Implementation einer Schnittstelle wird keine Implementation übernommen, wie bei der Erweiterung einer Basisklasse. Da die Erweiterung aber verschiedene Probleme mit sich bringt, wie die zu starke Kopplung an eine Basisklasse, ist die Implementation einer Schnittstelle oft eine gute Alternative zu einer Erweiterung. Auf die Übernahme von Diensten anderer Klassen muß dabei nicht verzichtet werden, weil ja auch ohne Erweiterung einer Basisklasse Dienste anderer Klassen genutzt werden können, indem deren Exemplare verwendet werden.
Aber die Anwendungstypen sind nicht alles, was durch die Implementation einer Schnittstelle übernommen wird. Da die Semantik der Operationen der Schnittstelle ja schon mit der Schnittstelle dokumentiert sind, kann auch auf deren Wiederholung verzichtet werden und es müssen nur noch die Abweichungen oder Verfeinerungen von der Spezifikation der Operation in der Schnittstelle niedergeschrieben werden.
Zugriff über die Schnittstelle
Auf eine Klasse, die eine Schnittstelle implementiert, kann von einem Klienten über diese Schnittstelle zugegriffen werden. Wenn ein Exemplar dieser Klasse als Exemplar mit dieser Schnittstelle interpretiert werden soll, dann kann der Typ eines Bezeichners diese Schnittstelle sein. Von dieser Möglichkeit macht die Deklaration "final Switchable switcher = new Loudspeaker();" Gebrauch. Danach hat der Ausdruck "switch" den Typ "Switchable" und referenziert ein Objekt der Klasse "Loudspeaker".
Die Referenzierung eines Objektes der Klasse "Loudspeaker" über einen Ausdruck "switcher" vom Typ "Switchable" kann als eine Verallgemeinerung (upcast ) angesehen werden, die in dem Programm "UseSwitch.java" an der Stelle des Gleichheitszeichens "=" erfolgt.
UseSwitch.java
class Algorithms
{ static boolean turnOn( final Switchable s )
{ return !s.isOn() && s.switchOn(); }}
public class UseSwitch
{ public static void main( final String[] args )
{ final Switchable switcher = new Loudspeaker();
Algorithms.turnOn( switcher );
System.out.println( switcher.isOn() ); }}System.out
true
Schnittstellenerweiterungen
Wie Klassen können auch Schnittstellen erweitert werden. So erlaubt die Schnittstelle "SwitchablePlus" es auch zu testen, ob etwas Schaltbares ausgeschaltet ist, indem sie die Schnittstelle "Switchable" um eine Methode erweitert. Die Notation der Erweiterung entspricht der bei einer Klassenerweiterung: durch die extends -Klausel "extends Switchable" werden die Einträge der Schnittstelle "Switchable" übernommen.
SwitchablePlus.java [Interface]
/** Interface for objects which can be switched on or off.
* @version 1.00 05/29/03
* @author Stefan Ram */
interface SwitchablePlus extends Switchable
{ /** Determine whether the switch is off.
* @return true iff the object is off. */
boolean isOff(); }
Parameter mit dem Variablenmodifizierer »final«
Eine Implementation einer Operation einer Schnittstelle in einer Klasse kann einen Parameter mit dem Variablenmodifizierer »final« deklarieren oder nicht. Dies ist ein Implementationsdetail der implementierenden Methode, obwohl es im Kopf der Methode steht. Daher findet der Variablenmodifizierer »final« für Parameter von Schnittstellen grundsätzlich keine Anwendung.
Main.java
interface A { void o( int i ); }
class B implements A { public void o( final int i ){} }
class C implements A { public void o( int i ){} }
public class Main
{ public static void main( final java.lang.String[] args ){} }
Bedeutung für den Software-Entwurf
Beim Entwurf von Software wird ein großes komplexes System in Teile zerlegt, die nur über spezifizierte Schnittstellen miteinander kommunizieren. Mehrere verschiedene Klassen des Systems müssen sich an die Spezifikation einer Schnittstelle halten, wenn sie diese verwenden. Daher ist ein später Änderung einer Schnittstelle nur noch schwer möglich, weil dann alle von dieser Schnittstelle abhängigen Klassen angepaßt werden müssen.
Schnittstellenfestlegungen sind die wichtigsten und folgenschwersten Festlegungen beim Entwurf von Software.
Schnittstellenfestlegungen legen das Gerüst eines Programms fest, das dann noch mit Implementationen ausgebaut werden muß.
Ad-hoc -Implementation
In einem Exemplarerzeugungsausdruck kann nach dem Schlüsselwort "new" auch der Name einer Schnittstelle angegeben werden, für die dann ein Rumpf einer anonymen Klasse angegeben werden kann, welche diese Schnittstelle implementieren muß. Der Wert des Ausdrucks hat dann den Typ dieser Schnittstelle und ist eine Referenz auf ein Exemplar dieser anonymen Klasse. Dadurch kann bei Bedarf ohne eine äußere Klassendeklaration ad hoc ein mit einer Schnittstelle verträgliches Exemplar mit einer gerade benötigten passenden Klasse erzeugt werden,
- 〈ClassInstanceCreationExpression 〉 ::=
- "new" 〈InterfaceType 〉 "(" 〈ArgumentListopt 〉 ")" [〈ClassBody 〉]
In dem Programm "AdHoc.java" wird ein Objekt einer anonymen Klasse erzeugt, welche die Schnittstelle "Printer" implementiert. Die gewünschte Implementation der Methode "print" wird dabei gleich angegeben und etwas später verwendet, um eine Textdarstellung eines Objekts auszugeben.
AdHoc.java
public class AdHoc { public static void main( final String[] args )
{ final Printer printer = new Printer()
{ public void print( final Object o )
{ System.out.println( "Object = \"" + o + "\"" ); }};
printer.print( System.out ); }}
interface Printer { void print( Object o ); }System.out
Object = "java.io.PrintStream@7ced01"