Home > Informatik > Einführung in die OOP > 10.Abstakte Klassen/Interfaces > 10.3 Interfaces

10.3 Interfaces

Das Thema "Interfaces" oder "Schnittstellen" soll auf dieser Seite an einem schönen Beispiel aus dem Buch von Sharan und Davis erläutert werden, "Beginning Java 17 Fundamentals" von 2022. Die Java-Quelltext wurden diesem Buch entnommen und an manchen Stellen leicht verändert; vor allem für die Art der Einrückung wurde der Allman-Stil gewählt, während in dem Buch der weiter verbreitete 1TBS-Stil verwendet wird.

Die Quelltexte

Die Java-Quelltext zu dieser Seite finden Sie in der Abteilung "Quelltexte, Folge 10".

10.3.1 Das Sharan-Davis-Beispiel

Version 1

Beginnen wir mit der einfachen Klasse Person:

public class Person 
{
    private String name;

    public Person(String name) 
    {
        this.name = name;
    }

    public void walk() 
    {
        System.out.println(name + " (a person) is walking.");
    }
}

Eine Klasse Walkables sieht dann so aus:

public class Walkables 
{
    public static void letThemWalk(Person[] list) 
    {
        for (Person person : list) 
        {
            person.walk();
        }
    }
}

Die Klasse enthält nur eine Methode - letThemWalk() die ein Objekt-Array mit Person-Objekten entgegennimmt und dann mit einer for-each-Schleife für jedes Person-Objekt die walk()-Methode ausführt.

Schließlich enthält das Beispiel eine Testklasse WalkablesTest:

public class WalkablesTest 
{
    public static void main(String[] args) 
    {
        Person[] persons = new Person[3];
        persons[0] = new Person("Jack");
        persons[1] = new Person("Jeff");
        persons[2] = new Person("John");
        
        // Let everyone walk
        Walkables.letThemWalk(persons);
    }
}

Hier wird ein Array aus drei Personen erzeugt und dann der letThemWalk()-Methode aus Walkables übergeben.

Die Konsolen-Ausgabe des kleinen Testprogramms sieht dann so aus:

Jack (a person) is walking.
Jeff (a person) is walking.
John (a person) is walking.

Version 2

Die beiden Klassen Person und Walkables wurden nicht verändert. Neu hinzugekommen ist hier die Klasse Duck:

public class Duck 
{
    private String name;

    public Duck(String name) 
    {
        this.name = name;
    }

    public void walk() 
    {
        System.out.println(name + " (a duck) is walking.");
    }
}

Die Klasse ist genau so aufgebaut wie die Klasse Person. Schauen wir uns nun die neue Testklasse an:

public class WalkablesTest 
{
    public static void main(String[] args) 
    {
        Person[] list = new Person[3];
        list[0] = new Person("Jack");
        list[1] = new Duck("Jeff");   // A compile-time error
        list[2] = new Person("John");
        
        // Let everyone walk
        Walkables.letThemWalk(list);
    }
}

Die Testklasse kann nicht mehr kompiliert werden, denn die Ente Jeff ist kein Objekt der Klasse Person, in dem Array list können aber nur Objekte dieser Klasse gespeichert werden.

Wie kann man dieses Problem lösen? Wir wollen hier drei verschiedene Lösungen besprechen, die beiden ersten Lösungen bezeichnen wir als Version 3A und Version 3B. In der dritten Lösung - Version 5 - werden wir dann die Interface-Technik kennenlernen.

Version 3A (noch keine Vererbung)

Die Klassen Person und Duck werden nicht verändert, die Klasse Walkables wird um eine Methode ergänzt:

public class Walkables 
{
    public static void letPersonsWalk(Person[] list) 
    {
        for (Person person : list) 
        {
            person.walk();
        }
    }
    
    public static void letDucksWalk(Duck[] list) 
    {
        for (Duck duck : list) 
        {
            duck.walk();
        }
    }    
}

Hier wird vorausgesetzt, dass für jede der beiden Klassen Person und Duck ein eigenes Objekt-Array übergeben wird.

Entsprechend sieht dann die Testklasse aus:

public class WalkablesTest 
{
    public static void main(String[] args) 
    {
        Person[] pList = new Person[3];
        pList[0] = new Person("Jack");
        pList[1] = new Person("Jeff"); 
        pList[2] = new Person("John");
        
        // Let every Person walk
        Walkables.letPersonsWalk(pList);
        
        Duck[] dList = new Duck[3];
        dList[0] = new Duck("Donald");
        dList[1] = new Duck("Daisy"); 
        dList[2] = new Duck("Dagobert");
        
        // Let every Duck walk
        Walkables.letDucksWalk(dList);        
    }
}

Diese Lösung funktioniert zwar, ist aber weit entfernt von einer optimalen Lösung. Sobald eine weitere Klasse (beispielsweise Fish oder Bird) eingeführt wird, muss sowohl Walkables wie auch die Testklasse entsprechend erweitert werden.

Version 3B (mit Vererbung)

Ein vernünftig erscheinender Ansatz ist die Einführung einer Klassenhierarchie.

Die Gemeinsamkeiten von Person und Duck (und weiteren Klassen) werden in eine abstrakte Oberklasse Animal ausgelagert (Prinzip der Generalisierung).

public abstract class Animal 
{
    private String name;

    public Animal(String name) 
    {
        this.name = name;
    }
    
    public String getName()
    {
       return name;
    }

    public abstract void walk();
}

Die Klassen Person und Duck werden dann zu Unterklassen von Animal und müssen die abstrakte Methode walk() überschreiben:

public class Person extends Animal
{
    public Person(String name) 
    {
        super(name);
    }

    public void walk() 
    {
        System.out.println(getName() + " (a person) is walking.");
    }
}

public class Duck extends Animal
{
    public Duck(String name) 
    {
        super(name);
    }

    public void walk() 
    {
        System.out.println(getName() + " (a duck) is walking.");
    }
}

Die Klasse Walkables nimmt nun keine Person- oder Duck-Arrays mehr als Parameter entgegen, sondern ein Animal-Array:

public class Walkables 
{
    public static void letAnimalsWalk(Animal[] list) 
    {
        for (Animal animal : list) 
        {
            animal.walk();
        }
    }
}

Entsprechend wird jetzt auch die Testklasse angepasst:

public class WalkablesTest 
{
    public static void main(String[] args) 
    {
        Animal[] aList = new Animal[5];
        aList[0] = new Person("Jack");
        aList[1] = new Person("Jeff");
        aList[2] = new Duck("Donald");
        aList[3] = new Person("John");
        aList[4] = new Duck("Dagobert");
        
        
        // Let every Animal walk
        Walkables.letAnimalsWalk(aList);
   
    }
}

Das Programm funktioniert fehlerfrei:

Jack (a person) is walking.
Jeff (a person) is walking.
Donald (a duck) is walking.
John (a person) is walking.
Dagobert (a duck) is walking.

Version 4 (Klasse Fish)

Bevor wir jetzt zur Interface-Technik kommen, wollen wir das Projekt noch um die Klasse Fish erweitern:

public class Fish extends Animal
{
    public Fish(String name) 
    {
        super(name);
    }

    public void walk() 
    {
        System.out.println(getName() + " (a Fish) cannot walk.");
    }
}

Fische können bekanntlich nicht gehen, eine entsprechende Meldung wird dann von der walk()-Methode ausgegeben. Da die Klasse Fish eine Unterklasse von Animal ist, kann sie nach dem Prinzip der Ersetzbarkeit an die Stelle von Animal treten. Betrachten wir dazu das Testprogramm:

public class WalkablesTest 
{
    public static void main(String[] args) 
    {
        Animal[] aList = new Animal[5];
        aList[0] = new Person("Jack");
        aList[1] = new Fish("Nemo");
        aList[2] = new Duck("Donald");
        aList[3] = new Person("John");
        aList[4] = new Duck("Dagobert");
        
        
        // Let every Animal walk
        Walkables.letAnimalsWalk(aList);
   
    }
}

Das Programm läuft fehlerfrei, und "Nemo (a Fish) cannot walk." wird in der Konsole ausgegeben.

Version 5 (Interface Walkable)

Betrachten wir zunächst das Klassendiagramm der neuen Projekt-Version:

Klassendiagramm der Version 5
Autor: Ulrich Helmich 06/2026, Lizenz: Public domain

Neu ist die Klasse Walkable, die in dem Diagramm als <<interface>> gekennzeichnet ist. Schauen wir uns den Quelltext dieser Klasse an:

public interface Walkable
{
    void walk();
}

Das ist ein sehr kurzer Quelltext. Entscheidend ist hier, dass das Schlüsselwort class durch das Schlüsselwort interface ersetzt wurde. Interfaces oder Schnittstellen enthalten nur abstrakte Methoden.

Eine abstrakte Klasse kann im Gegensatz dazu auch nicht-abstrakte Methoden besitzen. Bei einem Interface wird aber auf das Schlüsselwort abstract vor dem Bezeichner der Methode verzichtet. Auch das Schlüsselwort public wird weggelassen, da grundsätzlich alle Methoden eines Interface öffentlich sind.

Damit sind Interfaces das perfekte Mittel, um das Design-by-contract-Prinzip einzuhalten: Das Interface gibt vor, welche Methoden implementiert werden müssen und legt auch die exakte Schnittstelle jede Methode fest.

Wie die einzelnen Methoden dann von den Klasse implementiert werden, die das Interface aufrufen, bleibt den jeweiligen Klassen überlassen. Wenn die Spezifikation des Interfaces genaue Vorbedingungen, Nachbedingungen und Invarianten festlegt, müssen sich die implementierenden Methoden natürlich genau an diese Vorgaben halten.

Das hier vorgestellte Interface ist noch recht einfach, es gibt nur vor, dass die einbindenden Klassen eine Methode walk() - ohne Parameter und ohne Rückgabetyp - implementieren müssen.

Schauen wir uns nun an, wie die Klassen Person, Duck und Fish dieses Interface Walkable implementieren:

public class Person extends Animal implements Walkable
{
    private String name;

    public Person(String name) 
    {
        super(name);
    }

    @Override
    public void walk() 
    {
        System.out.println(getName() + " (a person) is walking.");
    }
}

Ein Interface wird direkt in der Klassen-Signatur über das Schlüsselwort implements eingebunden. Vergisst man dann auch nur eine Methode der Schnittstelle zu implementieren, erzeugt der Compiler einen entsprechenden Fehler. Wie wir später noch sehen werden, kann eine Klasse nicht nur ein Interface einbinden, sondern auch zwei, drei oder noch mehr. Die Namen der Interfaces werden dann durch je ein Komma getrennt.

Die Klassen Duck und Fish sind genau so aufgebaut wie Person, nur der println()-Befehl unterscheidet sich etwas.

Interessant ist nun die Klasse Walkables:

public class Walkables
{
    public static void letThemWalk(Walkable[] list) 
    {
        for (Walkable w : list) 
        {
            w.walk();
        }
    }
}

Hier wird nicht mehr ein Array der Klasse Animal erwartet, sondern ein Array mit Walkable-Objekten. Das Interface wird hier also formal wie eine normale Klasse behandelt. Die dem Array list hinzugefügten Objekte sind also gleichzeitig Objekte von Animal (bzw. deren Unterklassen Person, Duck oder Fish) und von Walkable. Betrachten wir dazu noch die Klasse WalkablesTest:

public class WalkablesTest 
{
    public static void main(String[] args) 
    {
        Walkable[] w = new Walkable[5];
        w[0] = new Person("Jack");
        w[1] = new Person("Manfred");
        w[2] = new Duck("Donald");
        w[3] = new Person("John");
        w[4] = new Duck("Dagobert");
        
        // Let them walk
        Walkables.letThemWalk(w);
   }
}

Hier wird ein Array aus Walkable-Objekten erzeugt, dem drei Person-Objekte und zwei Duck-Objekte zugefügt werden.

Das Objekt w[0] ("Jack") ist hier gleichzeitig ein Objekt der Klasse Person (bzw. der Oberklasse Animal) und ein Verweis auf die Klasse Walkables. Von Animal wird die Methode getString() übernommen, und von Walkable die Methode walk(). Das erinnert stark an die Mehrfach-Vererbung, wie sie manche Programmiersprachen erlauben. Balzert spricht in seinem Buch in diesem Zusammenhang auch tatsächlich von Mehrfachvererbung [3].

Version 6 (Interface Swimmable)

Wenn wir den Quelltext der Testklasse folgendermaßen ändern:

public class WalkablesTest 
{
    public static void main(String[] args) 
    {
        Walkable[] w = new Walkable[5];
        w[0] = new Person("Jack");
        w[1] = new Person("Manfred");
        w[2] = new Duck("Donald");
        w[3] = new Fish("John");
        w[4] = new Duck("Dagobert");
        
        // Let them walk
        Walkables.letThemWalk(w);
   }
}

dann ist es nicht mehr möglich, den Quelltext zu kompilieren. Die Fehlermeldung "Inkompatible Typen: Fish kann nicht in Walkable konvertiert werden" erscheint. Der Grund liegt darin, dass die Klasse Fish das Interface Walkable nicht implementiert, was ja auch Sinn macht, weil Fische nicht gehen können.

Fische können aber schwimmen, daher liegt es nahe, ein zweites Interface namens Swimmable in das Projekt zu integrieren:

public interface Swimmable
{
    void swim();
}

Die Klasse Fish implementiert dieses neue Interface:

public class Fish extends Animal implements Swimmable
{
    public Fish(String name) 
    {
        super(name);
    }

    @Override // Interface Swimmable
    public void swim() 
    {
        System.out.println(getName() + " (a fish) is swimming.");
    }
}

Interessant ist nun die Klasse Duck. Enten können nicht nur gehen, sondern auch schwimmen. Kann die Klasse Duck gleichzeitig zwei Interfaces einbinden? Der Versuch verläuft positiv:

public class Duck extends Animal implements Walkable, Swimmable
{
    public Duck(String name) 
    {
        super(name);
    }

    @Override // interface Walkable
    public void walk() 
    {
        System.out.println(getName() + " (a duck) is walking.");
    }
    
    @Override // interface Swimmable
    public void swim() 
    {
        System.out.println(getName() + " (a duck) is swimming.");
    }    
}

Die folgende Testklasse integriert die for-each-Schleifen der bisherigen Walkables- und Swimmables-Klassen, so dass wir in dieser Version 6 darauf verzichten können, damit die Zahl der Klassen nicht unübersichtlich groß wird.

public class TestInterfaces
{
    private Duck donald   = new Duck("Donald Duck");
    private Duck dagobert = new Duck("Dagobert Duck");
    
    public void walkingAnimals()
    {
        Walkable[] w = new Walkable[5];
        w[0] = new Person("Jack");
        w[1] = new Person("Manfred");
        w[2] = donald;
        w[3] = new Person("John");
        w[4] = dagobert;
        
        for (int i=0; i < w.length; i++)
            w[i].walk();   
    }

    public void swimmingAnimals()
    {
        Swimmable[] s = new Swimmable[5];
        s[0] = new Fish("Nemo");
        s[1] = new Fish("Wanda");
        s[2] = donald;
        s[3] = new Fish("Flipp");
        s[4] = dagobert;

        for (int i=0; i < s.length; i++)
            s[i].swim();
    }

    public static void main(String[] args)
    {
        TestInterfaces test = new TestInterfaces();
        test.walkingAnimals();
        test.swimmingAnimals();
    }
}

Die beiden Enten donald und dagobert werden als Instanzvariablen festgelegt, um zu zeigen, dass ein- und dasselbe Objekt tatsächlich Methoden von zwei verschiedenen Interfaces besitzt.

Die eine Test-Methode kümmert sich um die gehenden Tiere, die andere um die schwimmenden. Die Frage ist, ob man diese Klasse nicht vereinfachen kann. Eine Alternative sähe so aus:

public class TestInterfaces2
{
    private Animal[] animals;

    public TestInterfaces2()
    {
        animals = new Animal[5];
        animals[0] = new Person("Jack");
        animals[1] = new Fish("Nemo");
        animals[2] = new Duck("Donald");
        animals[3] = new Fish("Wanda");
        animals[4] = new Duck("Dagobert");
    }

    public void walkOrSwimm()
    {
        for (int i = 0; i < animals.length; i++)
        {
            Animal a = animals[i];

            if (a instanceof Walkable w)
                w.walk();

            if (a instanceof Swimmable s)
                s.swim();
        }
    }
	 
    public static void main(String[] args)
    {
        TestInterfaces2 test = new TestInterfaces2();
        test.walkOrSwim();
    }
}

Hier wird wieder ein Animal-Array erzeugt, in dem alle Tiere gespeichert werden können, sowohl die gehenden wie auch die schwimmenden.

Erst bei der "Verwendung" der Animal-Objekte wird dann geprüft, ob sie das Interface Walkable oder das Interface Swimmable implementiert haben. Das geschieht mithilfe des Operators instanceof. Seit Java 16 kann man die im Quelltext dargestellte einfache Form verwenden. Vor Java 16 hätte man noch ein explizites Typecasting verwenden müssen:

            if (a instanceof Walkable)
                ((Walkable) a).walk();

            if (a instanceof Swimmable)
                ((Swimmable) a).swim();

Der instanceof-Operator prüft hier nicht, ob das Objekt zu einer bestimmten Klasse gehört, sondern ob es eine bestimmte Fähigkeit besitzt - hier walk() oder swimm().

Zusammenfassung

Was sollten Sie jetzt aus diesem Projekt gelernt haben, das von dem Sharan und Davis - Buch angeregt wurde?

1. Vererbung

Gemeinsame Eigenschaften und Methoden (z. B. der Name und die Methode getName()) werden nur einmal in der Oberklasse implementiert und von allen Unterklassen übernommen.

2. Abstrakte Klassen

Die Klasse Animal dient als gemeinsame Oberklasse für verschiedene Tier- und Personenklassen. Von ihr werden keine Objekte erzeugt; sie beschreibt lediglich gemeinsame Eigenschaften aller Unterklassen.

3. Interface

Die Interfaces Walkable und Swimmable beschreiben Fähigkeiten, indem sie die Signaturen von Methoden wie walk() und swim() vorgeben.

4. Mehrfache Interfaces

Eine Klasse (hier Duck) kann mehrere Interfaces gleichzeitig implementieren. Eine Ente kann sowohl laufen als auch schwimmen. Java erlaubt zwar keine Mehrfachvererbung von Klassen, aber eine Klasse kann beliebig viele Interfaces implementieren.

5. Polymorphie

In einem Array vom Typ Walkable[] können Objekte verschiedener Klassen gespeichert werden:

Walkable[] w = new Walkable[5];
w[0] = new Person("Jack");
w[2] = new Duck("Donald");

Obwohl sich hinter den Referenzen unterschiedliche Klassen verbergen, kann auf allen Objekten dieselbe Methode aufgerufen werden:

w[i].walk();

Welche konkrete Methode ausgeführt wird, entscheidet Java erst zur Laufzeit.

6. Ein Objekt kann mehrere Typen besitzen

Ein Duck-Objekt ist gleichzeitig ein Animal-, ein Walkable- und ein Swimmable-Objekt.

7. Verwendung von instanceof

Mit diesem Operator kann geprüft werden, ob ein Objekt ein bestimmtes Interface implementiert.

8. Interfaces beschreiben Fähigkeiten statt Klassenhierarchien
  • Vererbung beschreibt, was ein Objekt ist.
  • Interfaces beschreiben, was ein Objekt kann.

Ein Duck-Objekt ist ein Animal-Objekt und kann laufen und schwimmen.

10.3.2 Das Interface ActionListener

Dieses Thema wird im zugehörigen Skript behandelt.

10.3.3 Das Interface ComparableContent

Auch dieses Thema wird im zugehörigen Skript behandelt.

Quellen:

  1. Sharan, Davis: Beginning Java 17 Fundamentals, APress 2022.
  2. Lahres et al.: Objektorientierte Programmierung, Rheinwerk Computing 2021.
  3. Balzert, Arinir: Objektorientiert Programmieren, 4. Auflage, Springer-Verlag Berlin 2025.

Seitenanfang