Interfaces
Motivation: Interfaces
Wozu Interfaces?
- Eine öffentliche Schnittstelle (public interface) stellt einen Vertrag zwischen einer Klasse und ihren Verwendern dar
- Eine Klasse kann mehrere, verschiedene Verträge eingehen
- Nicht immer stammt die Schnittstelle von den Eltern
- Manche Verträge bestehen außerhalb der Vererbungshierarchie
- Compiler soll sicherstellen, dass der Vertrag eingehalten wird
Ein Interface in Java erlaubt, einen öffentlichen Vertrag außerhalb der Vererbungsbeziehungen zu definieren und zu implementieren
Ein Interfaces wird, in Abgrenzung zu einer Klasse, durch das Schlüsselwort interface
gekennzeichnet.
Beispiel: Java Interface
public interface Flieger {
public void starten();
public void landen();
public void fliegen();
}
public class Flugzeug implements Flieger {
public void fliegen() { // ...
}
public void landen() { // ...
}
public void starten() { // ..
}
}
Abgrenzung zu abstrakten Klassen
Java-Interfaces sind ähnlich zu abstrakten Basisklassen, mit folgenden Unterschieden
- Alle Methoden sind abstrakt
- Keine Attribute
- Keine Konstruktoren
- Mehrfachvererbung ist möglich
- Interfaces stehen neben der Vererbungshierarchie
- Von einem Interface erbt man nicht, man implementiert es
- Eine Klasse kann beliebig viele Interfaces implementieren
Interfaces sind in ihrem Zweck ähnlich abstrakten Basisklassen: sie definieren einen Vertrag, den die Klassen, die das Interface implementieren, einhalten müssen. Trotzdem gibt es einige Unterschiede zwischen abstrakten Basisklassen und Interfaces:
- Ein Interface kann keine Implementierung enthalten, d. h. alle Methoden eines Interfaces sind immer abstrakt und daher ohne Rumpf. Mit Java 8 gibt es allerdings eine Ausnahme zu dieser Regel, die weiter unten beschrieben wird.
- Ein Interface kann keine Daten tragen, d. h. es enthält keiner Attribute. Durch das Implementieren eines Interfaces werden also der implementierenden Klasse keine neuen Felder hinzugefügt. Einzige Ausnahme von dieser Regel ist, dass Interfaces Konstanten enthalten dürfen, also static-final-Felder, die später noch genauer betrachtet werden.
- Interfaces besitzen keine Konstruktoren, da man von ihnen keine Objekte erzeugen kann und sie neben der Vererbungshierarchie stehen.
- Zwischen Interfaces kann es (im Gegensatz zu Klassen) Mehrfachvererbung geben. D. h. ein Interface kann selbst von mehreren Interfaces abgeleitet sein. Dies ist insofern problemlos, als bei Interfaces weder Membervariablen noch Implementierungen zugelassen sind. Hierdurch werden die Hauptproblemquellen von Mehrfachvererbung ausgeschlossen.
- Interfaces stehen neben der Vererbungshierarchie: neben der Hierarchie der Klassen gibt es eine weitere Vererbungshierarchie zwischen den Interfaces. Klassen implementieren daher Interfaces und erben nicht von ihnen. Die Klassen können dann beliebige (und beliebig viele) Interfaces implementieren.
- Es gibt keine Beschränkung, wie viele Interfaces eine Klasse implementieren kann.
Besonderheiten beim Syntax
- Methoden eines Interfaces sind immer
public abstract
- Konstanten sind immer
public static final
public interface Flieger {
int CONSTANT = 12;
void starten();
void landen();
void fliegen();
}
public interface Flieger {
public static final int CONSTANT = 12;
public abstract void starten();
public abstract void landen();
public abstract void fliegen();
}
Da ein Interface einen öffentlichen Vertrag definiert, sind alle Methoden des Interfaces immer public
und da es keine Implementierung enthalten darf sind sie immer auch abstrakt. Aus diesem Grund kann man das public abstract
an den Methoden auch weglassen, der Compiler fügt es automatisch hinzu.
Da ein Interface keine Membervariablen enthalten kann, werden alle Variablendeklarationen vom Compiler explizit mit einem public static final
versehen.
Verwendung von Interfaces
Interfaces sind genauso Typen wie Klassen
- Referenzvariablen können auch vom Typ eines Interfaces sein
- Objekte einer Klasse, die ein Interface implementiert, können über eine Variable vom Interface-Typ adressiert werden (Polymorphie)
- Über den Interface-Typ sind nur die Methoden des Interfaces sichtbar
public class Flughafen {
public static void main(String[] args) {
Flieger flieger = new Flugzeug();
flieger.landen();
}
}
Interfaces verhalten sich genauso wie abstrakte Basisklassen. Objekte einer Klasse, die ein Interface implementiert, können über Variablen vom Typ des Interfaces referenziert werden. Die Polymorphie bezieht sich also nicht nur auf die Vererbungshierarchie, sondern auf die Implementierungsbeziehungen.
Beispiel
Das Diagramm zeigt ein Interface und drei Klassen, die das Interface implementieren. Zwischen den Klassen besteht keine Gemeinsamkeit: Eine Ente ist ein Tier, ein Flugzeug ist eine Maschine und Superman ist ein Außerirdischer, der vom Planeten Krypton auf die Erde gekommen ist, als sein Planet zerstört wurde. Da Superman der einzige Überlebende von Krypton ist, handelt es sich bei ihm um ein Singleton – eine Klasse, von der es nur ein Objekt geben darf. (Details zu Singletons folgen später.)
Trotzdem haben alle drei Klassen eine Gemeinsamkeit, nämlich, dass sie die Fähigkeit besitzen, zu fliegen. Dies drücken sie dadurch aus, dass sie gemeinsam ein Interface Flieger
implementieren.
public interface Flieger {
void starten();
void landen();
void fliegen();
}
public class Superman implements Flieger {
public void fliegen() { ... }
public void landen() { ... }
public void starten() { ... }
public void weltRetten() { ... }
}
Beispiel: Interfaces und Vererbung
Die Klassen können neben der Implementierungsbeziehung zum Interface von anderen Klassen erben. Dass Interface darf aber erst auf der Ebene der Vererbungshierarchie implementiert werden, ab der klar ist, dass alle Subklassen sicher die Fähigkeit zu fliegen besitzen. Daher implementiert erst Ente
das Interface, da es Vögel gibt, die nicht fliegen können.
public abstract class Tier {
public void schlafen() { ... }
}
public abstract class Vogel extends Tier {
public abstract void eierLegen();
}
public class Ente extends Vogel implements Flieger {
public void eierLegen() { ... }
public void schwimmen() { ... }
public void fliegen() { ... }
public void landen() { ... }
public void starten() { ... }
}
Auch nach der Implementierung des Interfaces können noch weitere Klassen abgeleitet werden. Die Tatsache, dass das Interface von der Elternklasse (hier Flugzeug
) implementiert wurde, wird an alle Subklassen vererbt. Also implementieren hier auch die Klassen WasserFlugzeug
und Airbus
das Interface Flieger
.
Weitere Interfaces können eingeführt werden, hier z. B. Schwimmer
, das von allen Klassen implementiert wird, die schwimmen können.
public class Ente extends Vogel
implements Schwimmer, Flieger {
public void eierLegen() { ... }
public void schwimmen() { ... }
public void fliegen() { ... }
public void landen() { ... }
public void starten() { ... }
}
public class Wasserflugzeug extends
Flugzeug implements Schwimmer {
public void schwimmen() { ... }
}
Weitere Eigenschaften von Interfaces
- Man kann mit
instanceof
auf Interfaces prüfen - Die
implements
-Beziehung wird weitervererbt - Klassen, die ein Interface implementieren,
- implementieren alle Methoden des Interfaces oder
- implementieren nur einen Teil der Methoden, werden dann aber selbst abstrakt
Über die Regel, dass ein Interface entweder vollständig implementiert wird oder die Klasse wird abstrakt, wird eine Verbindung zwischen Interfaces und abstrakten Klassen hergestellt.
Beispiel: Alles auf einmal
Beispiel: Implements-Vererbung
A a = new A();
B b = new B();
System.out.println(a instanceof I1); // --> true
System.out.println(a instanceof I2); // --> false
System.out.println(b instanceof A); // --> true
System.out.println(b instanceof I1); // --> true
System.out.println(b instanceof I2); // --> true
public interface I1 {
public abstract void m1();
}
public interface I2 {
public abstract void m2();
}
public class A implements I1 {
public void m1() { /* leer */ }
}
public class B extends A implements I2 {
public void m2() { /* leer */ }
}
A a = new A();
B b = new B();
System.out.println(a instanceof I1); // --> true
System.out.println(a instanceof I2); // --> false
System.out.println(b instanceof A); // --> true
System.out.println(b instanceof I1); // --> true
System.out.println(b instanceof I2); // --> true
Mehrfachvererbung und Konstanten
public interface I1 {
public static final int KONSTANTE = 42;
}
public interface I2 {
public static final int KONSTANTE = 23;
}
public class A implements I1, I2 {
public static void main(String[] args) {
System.out.println(KONSTANTE); // FEHLER!!
}
}
pr2/mehrfachvererbung/A.java:9: reference to KONSTANTE is
ambiguous, both variable KONSTANTE in pr2.mehrfachvererbung.I1
and variable KONSTANTE in pr2.mehrfachvererbung.I2 match
System.out.println(KONSTANTE);
Der Compiler kann in der Klasse A nicht wissen, welche Variable mit dem Namen KONSTANTE gemeint ist. Daher kommt es wegen der Mehrdeutigkeit zu einem Compiler-Fehler. Dieses Problem kann man leicht auflösen, indem man explizit angibt, welche Konstante man meint.
public class A implements I1, I2 {
public static void main(String[] args) {
System.out.println(I1.KONSTANTE);
System.out.println(I2.KONSTANTE);
}
}
Praxis-Tipp: Interfaces und Konstanten
Interfaces sollten nicht zur Definition von Konstanten verwendet werden
So nicht…
public interface Konstanten {
public static final double PI = 3.14159265;
}
public class Mathe implements Konstanten {
public double kreisFlaeche(double radius) {
return PI*radius*radius;
}
}
sondern so…
public final class Konstanten {
private Konstanten() {}
public static final double PI = 3.14159265;
}
import static pr2.Konstanten.PI;
public class Mathe {
public double kreisFlaeche(double radius) {
return PI*radius*radius;
}
}
Ein früher häufig propagiertes Muster war die Verwendung von Interfaces zur Definition von Konstanten. Da man die Konstanten dann mit einem einfachen implements
in der Klasse bekannt machen konnte, konnte man die Angabe der Klasse, aus der man die Konstanten verwenden wollte, vermeiden. Dieses Vorgehen ist aber spätestens seit der Einführung von static
-Imports ohne Vorteil, hat aber signifikante Nachteile, da es einen weiteren Typ einführt und suggeriert, dass der Implementierende des Interfaces einen besonderen Kontrakt einhält.
Da es sich hier um einen Missbrauch von Interfaces handelt und man dasselbe Ergebnis auf einem anderen Weg erreichen kann, ist von der Verwendung von Interfaces für das Einführen von Konstanten dringend abzuraten.
Adapter-Klassen
Adapter stellen eine Basisimplementierung eines Interfaces zur Verfügung
Gerade im Bereich der grafischen Oberflächen muss man häufig Interfaces implementieren, um auf Ereignisse zu reagieren (z. B. dass ein Knopf gedrückt wurde). Die Interfaces haben viele Methoden, man braucht aber oft nur einen Bruchteil davon. Dies hat zur Folge, dass man viele Methoden leer implementiert, weil man alle Methoden des Interfaces implementieren muss.
Eine Lösung für dieses Problem ist die Verwendung von Adapter-Klassen. Eine solche Klasse implementiert das Interface vollständig, wenn auch leer, d. h. die Methoden machen nichts. Anstatt das Interface zu implementieren, erbt man dann von der Adapter-Klasse und überschreibt nur noch die relevanten Methoden.
Der Nachteil einer Adapter-Klasse ist, dass man bei ihrer Verwendung, wegen der Einfachvererbung, nicht mehr von einer anderen Klasse erben kann.
Comparable
public class Telefonnummer implements Comparable {
private int vorwahl; private int nummer;
public int compareTo(Object o) {
Telefonnummer n = (Telefonnummer) o;
if (vorwahl > n.vorwahl) {
return 1;
} else if (vorwahl < n.vorwahl) {
return -1;
} else { // vorwahl == n.vorwahl
if (nummer > n.nummer) {
return 1;
} else if (nummer < n.nummer) {
return -1;
}
}
return 0;
}
}
Die Java-Klassenbibliothek enthält bereits eine ganze Reihe von Interfaces, z. B. Comparable
. Comparable
erlaubt es, über die compareTo
-Methode für Klassen eine Sortierreihenfolge zu definieren.
Die hier vorgestellte Implementierung ist nicht optimal:
Zum einen sollte man seit Java 1.5 generische Typen verwenden, d. h. statt Comparable
lieber die generische Variante Comparable<Telefonnummer>
implementieren. Generics werden aber erst später eingeführt, sodass sie hier noch nicht verwendet werden konnten.
Weiterhin ist das Speichern einer Telefonnummer als int
sicherlich nicht korrekt, wenn es in dem jeweiligen Land führende Nullen in der Telefonnummer gibt, oder wenn die Nummern länger als die mit einem int
darstellbare Anzahl von Stellen werden kann.
Telefonnummer[] nummern = {
new Telefonnummer(6221, 72643),
new Telefonnummer(7262, 5558912),
new Telefonnummer(6221, 71263),
new Telefonnummer(7261, 5558912) };
Arrays.sort(nummern);
System.out.println(Arrays.asList(nummern).toString());
[(6221) 71263, (6221) 72643, (7261) 5558912, (7262) 5558912]
Callbacks
public interface Funktion {
public abstract int apply(int o1, int o2);
}
public class Addition implements Funktion {
public int apply(int o1, int o2) {
return o1 + o2;
}
}
public class Subtraktion implements Funktion {
public int apply(int o1, int o2) {
return o1 - o2;
}
}
Das Beispiel hier verwendet das Strategy-Pattern.
public class Berechnung {
public static int berechne(int input1, int input2, Funktion funktion) {
int ergebnis = funktion.apply(input1, input2);
return ergebnis;
}
public static void main(String[] args) {
Funktion sub = new Subtraktion();
Funktion add = new Addition();
System.out.println(berechne(5, 3, sub));
System.out.println(berechne(1, 7, add));
}
}
Bei einem Callback geht es letztendlich darum, einer Methode eine andere Methode zu übergeben, die diese dann aufrufen kann. Da der Aufrufer die Methode mitgibt, ruft die aufgerufene Methode sozusagen zum Aufrufen zurück, daher der Name Callback.
Methoden sind in Java, anders als in funktionalen Programmiersprachen, keine eigenständigen Konstrukte. Dies bedeutet, man kann eine Methode nicht einzeln übergeben oder referenzieren.
Bis zur Einführung von Lambdas war daher die einzige Möglichkeit eine Methode zu übergeben, der Umweg über ein Objekt, in dessen Klasse sich die gewünschte Methode befand. Um die nötige Flexibilität zu bekommen, die Methode auszugestalten, ist man üblicherweise wie folgt vorgegangen:
- Man definiert ein Interface, das eine Callback-Methode mit den gewünschten Parametern und Rückgabetyp enthält. Im Beispiel das Interface
Funktion
mit der Callback-Methodeapply
. - Man definiert Empfängermethode einen Parameter vom Typ des Interfaces. Im Beispiel
berechne
. - In der Empfängermethode ruft man die Callback-Methode aus dem Interface auf.
- Der Verwender implementiert das Interface in einer eigenen Klasse und liefert somit seine Implementierung der Callback-Methode. Im Beispiel
Addition
undSubtraktion
. - Der Verwender erzeugt ein Objekt von der Klasse und übergibt es der Empfängermethode. Im Beispiel in der
main
-Methode zu sehen.
Default-Methoden: Motivation
- Interfaces können nach der Veröffentlichung nicht mehr geändert werden
- Erweiterung führt zu Compiler-Fehlern bei den Verwendern
public interface Flieger {
public abstract void starten();
public abstract void landen();
}
public class Flugzeug implements Flieger {
public void starten() { /* ... */ }
public void landen() { /* ... */ }
}
Ein gängiges Problem bei Interfaces ist, dass man es nach der Veröffentlichung nicht mehr ändern kann. Der Grund liegt darin, dass jede Änderung des Interfaces, vor allem das Hinzufügen von Methoden, dazu führt, dass alle Verwender nicht mehr kompilieren. Deswegen muss man sehr viel Sorgfalt in den Entwurf der Schnittstellen stecken und trotzdem kommt es noch häufig vor, dass man ein Interface trotzdem später anpassen muss. Als mögliche Lösung kam bisher nur infrage, ein neues Interface anzulegen, dass vom ursprünglichen erbt und es erweitert.
public interface Flieger {
public abstract void starten();
public abstract void landen();
public abstract void sinken();
public abstract void steigen();
}
The type Flugzeug must implement the inherited abstract method
Flieger.sinken()
Flugzeug.java line 3
Das Interface Flieger
im Beispiel kann man nicht mehr erweitern, ohne alle Verwender (hier z. B. Flugzeug
) anzupassen. Würde man die Methoden steigen()
und sinken()
hinzufügen, käme es zu Fehlern in Flugzeug
.
Lösung: Default-Methoden
Interfaces können Default-Methoden (default methods) enthalten
- Methoden mit Implementierung, durch Schlüsselwort
default
gekennzeichnet - Werden an die Klasse weitergegeben, die das Interface implementiert
- Können von der Klasse mit eigener Implementierung überschrieben werden
- Interfaces mit Default-Methoden heißen erweiterte Schnittstellen
- Default-Methoden können
public
oderprivate
sein.
public interface Flieger {
public abstract void starten();
public abstract void landen();
public default void sinken() {
System.out.println("sinke");
}
public default void steigen() {
System.out.println("steige");
}
}
Default-Methoden werden manchmal als virtual extension methods oder defender methods bezeichnet. Die Bezeichnung defender methods macht klar, dass es hier darum geht, vorhandene Implementierungen der Interfaces gegen Änderungen der Interfaces zu verteidigen.
Im vorliegenden Beispiel sind die Methoden sinken
und steigen
Default-Methoden. Wenn man das Interface implementiert und keine eigenen Implementierungen angibt, dann wird die Implementierung aus dem Interface übernommen.
Der Hauptgrund, warum man Default-Methoden eingeführt hat, waren die Erweiterungen am Collection-API, die für die Lambda-Funktionen nötig waren. Hätte man keine Default-Methoden eingeführt, wären alle existierenden Verwender des Collection-APIs auf Probleme gestoßen. Collections werden in einem späteren Kapitel noch ausführlich behandelt.
Dass Default-Methoden in einem Interface private
sein können, wirkt auf den ersten Blick sinnlos, da sie ja so nicht an die implementierende Klasse weitergegeben werden. Man kann sie aber dazu benutzen, die Implementierung von Default-Methoden in einem Interface auf mehrere, kleinere Methoden aufzuteilen und so ein besseres Design zu realisieren.
public interface Flieger {
public abstract void starten();
public abstract void landen();
private void aktion(String text) {
System.out.println(text);
}
public default void sinken() {
aktion("sinke");
}
public default void steigen() {
aktion("steige");
}
}
Bei einer privaten Default-Methode in einem Interface entfällt das Schlüsselwort default
. Sie sind ab Java 9 verfügbar.
Flugzeug flugzeug = new Flugzeug();
flugzeug.starten();
flugzeug.steigen();
flugzeug.sinken();
flugzeug.landen();
starte
steige
sinke
lande
Die Klasse Flugzeug
verwendet das Interface Flieger
in seiner ursprünglichen Form und implementiert nur die beiden Methoden starten()
und landen()
. Würde man das Interface einfach um zwei weitere abstrakte Methoden sinken()
und steigen()
erweitern, würde es einen Compile-Fehler geben, da Flugzeug
die Methoden nicht implementiert.
Die Verwendung von Default-Methoden erlaubt es nun, das Interface zu erweitern, ohne die Klasse Flugzeug
zu stören. Flugzeug
„erbt“ jetzt einfach die beiden Methoden sinken()
und steigen()
aus dem Interface, sodass man sie ganz normal aufrufen kann.
Mehrfachvererbung und Default-Methoden
Implementiert Klasse zwei Interfaces mit gleichnamigen Default-Methoden so muss sie die Methode überschreiben
public interface IA {
public default void m() { }
}
public interface IB {
public default void m() { }
}
public class Impl implements IA, IB {
}
Duplicate default methods named m with the parameters () and () are
inherited from the types IB and IA
Impl.javal line 3
Interfaces erlauben Mehrfachvererbung und mehrfache Implementierung. Wie wird der berühmte Deadly-Diamond vermieden? Beim Deadly-Diamond kommen in einer Klasse durch die Mehrfachvererbung zwei Implementierungen einer Methode zusammen, sodass man beim Aufruf der Methode nicht weiß, welche der beiden Implementierungen genutzt werden soll.
Java löst dies, indem verlangt wird, dass die Klasse, in der die beiden Default-Methoden zusammenkommen diese überschreiben muss.
Im vorliegenden Beispiel liegt die Lösung also darin, dass Impl
die Methode m()
überschreiben muss.
public class Impl implements IA, IB {
public void m() { }
}
Vererbung von Default-Methoden
Wenn Interface von einem Interface mit Default-Methoden erbt, kann es
- die Methode ignorieren
→ Methode wird vererbt - die Methode ohne
default
angeben
→ Methode wird normale abstrakte Methode - die Methode mit
default
angeben
→ Methode wird überschrieben
→ Zugriff auf ursprüngliche Methode mitTyp.super.Methode
- abstrakte Methode durch Default-Methoden ersetzen
→ Implementierende Klasse muss sie nicht mehr implementieren
public interface ExtFlieger extends Flieger {
@Override
public default void steigen() {
// ersetzt die steigen Methode aus Flieger
System.out.println("Schnell steigen");
// Ursprüngliche Methode aufrufen
Flieger.super.steigen();
}
public abstract void sinken(); // Methode ist jetzt abstrakt
public default void starten() {
// Methode ist jetzt nicht mehr abstrakt
System.out.println("Kommt");
}
}
Bei einem Konflikt haben die Methoden einer Klasse Vorrang vor den default
-Methoden der Interfaces
Das Beispiel lässt sich mit folgendem Source-Code nachstellen.
public interface I1 {
public default void methode() {
System.out.println("I1");
}
}
public interface I2 extends I1 {
@Override
public default void methode() {
System.out.println("I2");
}
}
public class A implements I1 {
public void methode() {
System.out.println("A");
}
}
public class B extends A {
@Override
public void methode() {
System.out.println("B");
}
}
public class Main {
public static void main(String[] args) {
C c = new C();
c.methode();
}
}
Die Ausgabe ist dann:
B