In Java kann eine abgeleitete Klasse nur genau eine Vaterklasse haben. Das Erben von mehr als einer Superklasse (Mehrfachvererbung) ist nicht möglich. Den Entwicklern der Programmiersprache Java war Mehrfachvererbung bekannt und auch die Nachteile die sie mit sich bringt. Ganz auf Mehrfachvererbung verzichten wollten sie aber nicht und schafften einen Mittelweg: das Interface.
Was ist ein Interface in Java?
Ein Interface ist eine Schnittstelle, über die einer Klasse bestimmte Funktionen zur Verfügung gestellt werden. Um die Funktionen nutzen zu können, müssen sie aber erst von der Klasse implementiert werden. Das Interface gibt nur den Rahmen (die Methodendeklarationen) vor.
Interfaces können als eine besondere Form einer Klasse angesehen werden. Sie enthalten ausschließlich Konstanten und abstrakte Methoden. Die abstrakten Methoden müssen von der Klasse implementiert werden, der das Interface zugewiesen wird.
Die Deklaration von Interfaces ist einfach, es muss nur das Schlüsselwort class
durch interface
ersetzt werden. Die Methoden in einem Interface sind implizit public
(öffentlich) und abstract
. Variablen und Konstruktoren sind nicht erlaubt, Konstanten können jedoch in Interfaces definiert werden.
Wie werden Interfaces in Java verwendet
Um den Nutzen von Interfaces zu verdeutlichen, möchten wir eine Anwendung programmieren, die das Interface Transportierbar verwendet.
Der folgende Quellcode definiert das Transportierbar-Interface:
public interface Transportierbar {
public final float MAX_GEWICHT_PRO_FLAECHE = 29.99F;
float gewicht();
float laenge();
float breite();
float hoehe();
boolean zerbrechlich();
String beschriftung();
}
Unser oben definiertes Interface Transportierbar ähnelt einer abstrakten Klasse. Wir wollen es als Schnittstelle nutzen, über die auf wichtige Transportdetails von realen Objekten zugegriffen werden kann. Dazu müssen die im Interface deklarierten abstrakten Methoden von den Klassen implementiert werden, da das Interface die Funktionalität lediglich beschreibt, aber nicht umsetzt (implementiert).
Alle Klassen die dieses Interface nutzen wollen, müssen es vollständig implementieren. Dazu wird an dem Namen der jeweiligen Klasse das implements
-Schlüsselwort und der Name des zu implementierenden Interface angefügt.
Werden nicht alle, von dem Interface deklarierten Methoden, implementiert, liefert der Java-Compiler eine Fehlermeldung zurück. Es ist aber möglich diese zu umgehen, dazu muss die Klasse als abstract
deklariert werden. Dann können aber auch keine Objekte von dieser abstrakten Klasse instanziert werden.
In dem folgenden Quellcode wollen wir nun unser Transportierbar-Interface mit Hilfe der Klasse Tisch implementieren:
public class Tisch implements Transportierbar {
public String kennzeichnung;
public boolean zerbrechlich;
public float gewicht, laenge, hoehe, breite;
public Tisch (String name, boolean zerbrechlich, float gewicht, float laenge, float breite, float hoehe) {
kennzeichnung = name;
this.zerbrechlich = zerbrechlich;
this.gewicht = gewicht;
this.laenge = laenge;
this.breite = breite;
this.hoehe = hoehe;
}
public float gewicht() {
return gewicht;
}
public float laenge() {
return laenge;
}
public float breite() {
return breite;
}
public float hoehe() {
return hoehe;
}
public boolean zerbrechlich() {
return zerbrechlich;
}
public String beschriftung() {
String text = "Tisch " + kennzeichnung;
return text;
}
}
In dem oberen Quellcode werden alle (abstrakten) Methoden des Interfaces Transportierbar von der Klasse Tisch implementiert, somit ist es möglich Tisch-Objekte zu instanzieren (konkret werden zu lassen).
Ein Interface ist genau dann besonders sinnvoll, wenn es Eigenschaften einer Klasse beschreibt (vorgibt), die nicht direkt in der Vererbungshierachie abgebildet werden können.
Um dieses Merkmal bei unserem Interface Transportierbar optimal zu nutzen, werden wir eine zweite Klasse definieren, welche nicht in die Vererbungshierarchie der Klasse Tisch passt.
Die folgende Klasse Schaf steht der Klasse Tisch aus Vererbungssicht nicht sehr nahe, daher würden sich beide Klassen nur umständlich in einer gemeinsamen Vererbungshierarchie abbilden lassen. Da die Klasse Schaf aber auch über bestimmte Eigenschaften der Klasse Tisch verfügen soll, ist hier die Verwendung eines gemeinsamen Interfaces der richtige Weg.
Das Interface Transportierbar beschreibt dabei für beide implementierenden Klassen die wichtigen Transportdetails, die natürlich noch in jeder der beiden Klassen entsprechend implementiert werden müssen. Das Interface Transportierbar liefert nur die Methodendeklarationen als gemeinsame Schnittstelle.
In dem folgenden Quellcode wollen wir nun unser Transportierbar-Interface für die Klasse Schaf implementieren.
public class Schaf implements Transportierbar {
public String name;
public boolean zerbrechlich;
public float gewicht, laenge, hoehe, breite;
public Schaf (String name, boolean zerbrechlich, float gewicht, float laenge, float breite, float hoehe) {
this.name = name;
this.zerbrechlich = zerbrechlich;
this.gewicht = gewicht;
this.laenge = laenge;
this.breite = breite;
this.hoehe = hoehe;
}
public float gewicht() {
return gewicht;
}
public float laenge() {
return laenge;
}
public float breite() {
return breite;
}
public float hoehe() {
return hoehe;
}
public boolean zerbrechlich() {
return zerbrechlich;
}
public String beschriftung() {
String text = "Lebewesen: Schaf " + name;
return text;
}
}
Jetzt haben wir unser Interface Transportierbar für die beiden Klassen Tisch und Schaf implementiert. Dadurch sind die für den Transport wichtigen Methoden gewicht
, laenge
, breite
, hoehe
, zerbrechlich
und beschriftung
für beide Klassen jeweils in der passenden Implementierung verfügbar und das unabhängig von der jeweiligen Vererbungshierarchie.
Um das Interface zu testen, benötigen wir noch eine Testklasse. Diese wollen wir als nächstes definieren:
public class InterfaceTest {
public static boolean transportMachbar (Transportierbar tDetails) {
float gewicht = tDetails.gewicht();
float laenge = tDetails.laenge();
float breite = tDetails.breite();
float gewichtProFlaeche = gewicht/(laenge*breite);
if (gewichtProFlaeche < tDetails.MAX_GEWICHT_PRO_FLAECHE) {
return true;
}
return false;
}
public static float berechneVolumen (Transportierbar tDetails) {
float volumen = tDetails.laenge()*tDetails.breite()*tDetails.hoehe();
return volumen;
}
public static String erstelleBeschriftung (Transportierbar tDetails) {
String text = tDetails.beschriftung();
if (tDetails.zerbrechlich()) {
return "-Zerbrechlich- " + text;
}
return text;
}
public static void main (String[] args) {
Tisch myTisch = new Tisch("2014.AE Esstisch", false, 27.3F, 3.0F, 2.2F, 1.3F);
Schaf mySchaf = new Schaf("Cloud", true, 42.9F, 1.1F, 0.82F, 0.55F);
System.out.println("Transportdetails für den Tisch:");
System.out.println("Volumen: " + berechneVolumen(myTisch) + " m^3");
System.out.println("Verpackungsaufdruck: " + erstelleBeschriftung(myTisch));
System.out.println("Transport machbar: " + transportMachbar(myTisch));
System.out.println("nTransportdetails für das Schaf:");
System.out.println("Volumen: " + berechneVolumen(mySchaf) + " m^3");
System.out.println("Verpackungsaufdruck: " + erstelleBeschriftung(mySchaf));
System.out.println("Transport machbar: " + transportMachbar(mySchaf));
}
}
Unsere Testanwendung erzeugt zu Beginn zwei Objekte (myTisch
und mySchaf
), die beide unser Transportierbar-Interface implementieren. Daher können beide Objekte an die Methoden berechneVolumen
, erstelleBeschriftung
und transportMachbar
übergeben werden, obwohl sie Instanzen verschiedener Klassen sind.
Dies ist möglich, da eine Interface-Variable kompatibel zu allen Objekten ist, deren Klassen dieses Interface implementieren.
Es muss aber das Objekt an die Methode als Interface-Variable übergeben werden, so wie es bei der Methode berechneVolumen
der Fall ist. Die Methode erwartet als Argument ein Objekt vom Typ Transportierbar
, somit können ihr alle Objekte übergeben werden, deren Klassen das Interface Transportierbar
implementieren. In unserem Fall sind das Objekte vom Typ Tisch
und Schaf
.
Da an die Methode berechneVolumen
nur Objekte des Typs Transportierbar
übergeben werden können, ist sichergestellt, dass die übergebenen Objekte auch alle Methoden des Interfaces Transportierbar
implementiert haben.
Hinweis: Hierbei ist schön zu erkennen, dass durch das Interface ein neuer Datentyp entstanden ist, nämlich der Typ Transportierbar
. Alle Objekte, deren Klassen dieses Interface implementieren, sind somit sowohl vom Typ ihrer eigenen Klasse als auch vom Typ des Interfaces.
Auf Interface-Namen kann auch der instanceof
-Operator angewendet werden. So liefern die folgenden Anweisungen myTisch instanceof Transportierbar
und myTisch instanceof Tisch
beide den Wert true
zurück.
Starten wir nun die Beispielanwendung.