Part 9

Interfaces

Interfaces können verwendet werden, um das Verhalten einer Klasse zu definieren, d. h. ihre Methoden. Sie werden auf die gleiche Weise definiert wie normale Java-Klassen, jedoch wird am Anfang der Klasse "public interface ..." anstelle von "public class ... " verwendet. Interfaces definieren Verhalten durch Methodennamen und deren Rückgabewerte. Sie enthalten jedoch nicht immer die tatsächlichen Implementierungen der Methoden. Ein Sichtbarkeitsattribut für Interfaces wird nicht explizit angegeben, da sie immer public sind. Betrachten wir das Interface Readable, das die Lesbarkeit beschreibt.

public interface Readable {
    String read();
}

Das Readable-Interface deklariert eine read()-Methode, die ein Objekt des Typs String zurückgibt. Readable definiert bestimmtes Verhalten: beispielsweise kann eine Textnachricht oder eine E-Mail lesbar sein.

Die Klassen, die das Interface implementieren, entscheiden wie die im Interface definierten Methoden implementiert werden. Eine Klasse implementiert das Interface, indem sie nach dem Klassennamen das Schlüsselwort implements hinzufügt, gefolgt von dem Namen des zu implementierenden Interfaces. Erstellen wir eine Klasse namens TextMessage, die das Readable-Interface implementiert.

public class TextMessage implements Readable {
    private String sender;
    private String content;

    public TextMessage(String sender, String content) {
        this.sender = sender;
        this.content = content;
    }

    public String getSender() {
        return this.sender;
    }

    public String read() {
        return this.content;
    }
}

Da die Klasse TextMessage das Interface Readable implementiert (public class TextMessage implements Readable), muss die Klasse TextMessage eine Implementierung der Methode public String read() enthalten. Implementierungen von Methoden, die im Interface definiert sind, müssen immer das Sichtbarkeitsattribut public haben.

Zusätzlich zur Klasse TextMessage erstellen wir eine weitere Klasse, die das Readable-Interface implementiert. Die Klasse Ebook ist eine elektronische Implementierung eines Buches, das den Titel und die Seiten eines Buches enthält. Das E-Book wird Seite für Seite gelesen, und der Aufruf der Methode public String read() gibt immer die nächste Seite als String zurück.

public class Ebook implements Readable {
    private String name;
    private ArrayList<String> pages;
    private int pageNumber;

    public Ebook(String name, ArrayList<String> pages) {
        this.name = name;
        this.pages = pages;
        this.pageNumber = 0;
    }

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

    public int pages() {
        return this.pages.size();
    }

    public String read() {
        String page = this.pages.get(this.pageNumber);
        nextPage();
        return page;
    }

    private void nextPage() {
        this.pageNumber = this.pageNumber + 1;
        if (this.pageNumber % this.pages.size() == 0) {
            this.pageNumber = 0;
        }
    }
}

Objekte können aus Klassen, die ein Interface implementieren, auf die gleiche Weise instanziiert werden wie bei normalen Klassen. Sie werden auch auf die gleiche Weise verwendet, beispielsweise als Typ für eine ArrayList.

TextMessage message = new TextMessage("ope", "It's going great!");
System.out.println(message.read());

ArrayList<TextMessage> textMessages = new ArrayList<>();
textMessages.add(new TextMessage("private number", "I hid the body."));
Beispielausgabe

It's going great!

ArrayList<String> pages = new ArrayList<>();
pages.add("Split your method into short, readable entities.");
pages.add("Separate the user-interface logic from the application logic.");
pages.add("Always program a small part initially that solves a part of the problem.");
pages.add("Practice makes the master. Try different out things for yourself and work on your own projects.");

Ebook book = new Ebook("Tips for programming.", pages);

int page = 0;
while (page < book.pages()) {
    System.out.println(book.read());
    page = page + 1;
}
Beispielausgabe

Split your method into short, readable entities. Separate the user-interface logic from the application logic. Always program a small part initially that solves a part of the problem. Practice makes the master. Try different out things for yourself and work on your own projects.

Loading

Interface als Variablentyp

Der Typ einer Variablen wird immer bei ihrer Einführung angegeben. Es gibt zwei Arten von Typen: die primitiven Typvariablen (int, double, ...) und die Referenztypvariablen (alle Objekte). Bisher haben wir die Klasse eines Objekts als Typ einer Referenztypvariablen verwendet.

String string = "string-object";
TextMessage message = new TextMessage("ope", "many types for the same object");

Der Typ eines Objekts kann auch ein anderer als seine Klasse sein. Beispielsweise ist der Typ der Ebook-Klasse, die das Readable-Interface implementiert, sowohl Ebook als auch Readable. Ebenso hat die Textnachricht mehrere Typen. Da die TextMessage-Klasse das Readable-Interface implementiert, hat sie zusätzlich zum Typ TextMessage auch den Typ Readable.

TextMessage message = new TextMessage("ope", "Something cool's about to happen");
Readable readable = new TextMessage("ope", "The text message is Readable!");
ArrayList<String> pages = new ArrayList<>();
pages.add("A method can call itself.");

Readable book = new Ebook("Introduction to Recursion", pages);

int page = 0;
while (page < book.pages()) {
    System.out.println(book.read());
    page = page + 1;
}

Da ein Interface als Typ verwendet werden kann, ist es möglich, eine Liste zu erstellen, die Objekte des Interface-Typs enthält.

ArrayList<Readable> readingList = new ArrayList<>();

readingList.add(new TextMessage("ope", "never been programming before..."));
readingList.add(new TextMessage("ope", "gonna love it i think!"));
readingList.add(new TextMessage("ope", "give me something more challenging! :)"));
readingList.add(new TextMessage("ope", "you think i can do it?"));
readingList.add(new TextMessage("ope", "up here we send several messages each day"));

ArrayList<String> pages = new ArrayList<>();
pages.add("A method can call itself.");

readingList.add(new Ebook("Introduction to Recursion.", pages));

for (Readable readable : readingList) {
    System.out.println(readable.read());
}

Beachten Sie, dass, obwohl die Klasse Ebook, die das Readable-Interface implementiert, immer den Interface-Typ hat, nicht alle Klassen, die das Readable-Interface implementieren, vom Typ Ebook sind. Sie können ein aus der Ebook-Klasse erstelltes Objekt einer Readable-Typ-Variablen zuweisen, aber ohne separate Typumwandlung funktioniert dies nicht umgekehrt.

Readable readable = new TextMessage("ope", "TextMessage is Readable!"); // funktioniert
TextMessage message = readable; // funktioniert nicht

TextMessage castMessage = (TextMessage) readable; // funktioniert nur, wenn `readable` vom Typ TextMessage ist

Typumwandlung gelingt, wenn und nur wenn die Variable den Typ hat, in den sie umgewandelt werden soll. Typumwandlung wird nicht als gute Praxis angesehen, und eine der wenigen Situationen, in denen ihre Verwendung angemessen ist, ist die Implementierung der Methode equals.

Interface als Methodenparameter

Die wahren Vorteile von Interfaces werden deutlich, wenn sie als Typ des Parameters verwendet werden, der einer Methode übergeben wird. Da ein Interface als Variablentyp verwendet werden kann, kann es auch als Parametertyp in Methodenaufrufen verwendet werden. Beispielsweise erhält die Methode print in der Klasse Printer der Klasse unten eine Variable des Typs Readable.

public class Printer {
    public void print(Readable readable) {
        System.out.println(readable.read());
    }
}

Der Wert der Methode print der Klasse Printer liegt in der Tatsache, dass ihr jede Klasse, die das Readable-Interface implementiert, als Parameter übergeben werden kann. Wenn wir die Methode mit einem Objekt aufrufen, das aus einer Klasse instanziiert wurde, die die Readable-Klasse implementiert, funktioniert die Methode wie gewünscht.

TextMessage message = new TextMessage("ope", "Oh wow, this printer knows how to print these as well!");

ArrayList<String> pages = new ArrayList<>();
pages.add("Values common to both {1, 3, 5} and {2, 3, 4, 5} are {3, 5}.");
Ebook book = new Ebook("Introduction to University Mathematics.", pages);

Printer printer = new Printer();
printer.print(message);
printer.print(book);
Beispielausgabe

Oh wow, this printer knows how to print these as well! Values common to both {1, 3, 5} and {2, 3, 4, 5} are {3, 5}.

Erstellen wir eine weitere Klasse namens ReadingList, zu der wir interessante Dinge zum Lesen hinzufügen können. Die Klasse hat eine ArrayList-Instanz als Instanzvariable, in der die zu lesenden Dinge hinzugefügt werden. Das Hinzufügen zur Leseliste erfolgt über die Methode add, die ein Readable-Typ-Objekt als Parameter erhält.

public class ReadingList {
    private ArrayList<Readable> readables;

    public ReadingList() {
        this.readables = new ArrayList<>();
    }

    public void add(Readable readable) {
        this.readables.add(readable);
    }

    public int toRead() {
        return this.readables.size();
    }
}

Leselisten sind in der Regel lesbar, daher lassen wir die Klasse ReadingList das Interface Readable implementieren. Die read-Methode der Leseliste liest alle Objekte in der readables-Liste und fügt sie dem von der Methode read() zurückgegebenen String nacheinander hinzu.

public class ReadingList implements Readable {
    private ArrayList<Readable> readables;

    public ReadingList() {
        this.readables = new ArrayList<>();
    }

    public void add(Readable readable) {
        this.readables.add(readable);
    }

    public int toRead() {
        return this.readables.size();
    }

    public String read() {
        String read = "";

        for (Readable readable : this.readables) {
            read = read + readable.read() + "\n";
        }

        // Sobald die Leseliste gelesen wurde, leeren wir sie
        this.readables.clear();
        return read;
    }
}
ReadingList jonisList = new ReadingList();
jonisList.add(new TextMessage("arto", "have you written the tests yet?"));
jonisList.add(new TextMessage("arto", "have you checked the submissions yet?"));

System.out.println("Joni's to-read: " + jonisList.toRead());
Beispielausgabe

Joni's to-read: 2

Da ReadingList vom Typ Readable ist, können wir ReadingList-Objekte zur Leseliste hinzufügen. Im folgenden Beispiel hat Joni viel zu lesen. Zum Glück kommt Verna zur Hilfe und liest die Nachrichten im Namen von Joni.

ReadingList jonisList = new ReadingList();
int i = 0;
while (i < 1000) {
    jonisList.add(new TextMessage("arto", "have you written the tests yet?"));
    i = i + 1;
}

System.out.println("Joni's to-read: " + jonisList.toRead());
System.out.println("Delegating the reading to Verna");

ReadingList vernasList = new ReadingList();
vernasList.add(jonisList);
vernasList.read();

System.out.println();
System.out.println("Joni's to-read: " + jonisList.toRead());
Beispielausgabe

Joni's to-read: 1000 Delegating the reading to Verna

Joni's to-read: 0

Die Methode read, die auf Vernas Liste aufgerufen wird, durchläuft alle Readable-Objekte und ruft die Methode read auf ihnen auf. Wenn die Methode read auf Vernas Liste aufgerufen wird, wird auch Jonis Leseliste, die in Vernas Leseliste enthalten ist, durchlaufen. Jonis Leseliste wird durch Aufrufen ihrer Methode read durchlaufen. Am Ende jedes read-Methodenaufrufs wird die gelesene Liste geleert. Auf diese Weise wird Jonis Leseliste geleert, wenn Verna sie liest.

Wie Sie sehen, enthält das Programm bereits viele Referenzen. Es ist eine gute Idee, den Zustand des Programms Schritt für Schritt auf Papier zu zeichnen und zu skizzieren, wie der Methodenaufruf read des vernasList-Objekts fortschreitet!

Loading

Interface als Rückgabewert einer Methode

Interfaces können wie reguläre Variablentypen als Rückgabewerte in Methoden verwendet werden. Im nächsten Beispiel gibt es eine Klasse Factory, die gefragt werden kann, verschiedene Objekte zu konstruieren, die das Packable-Interface implementieren.

import java.util.Random;

public class Factory {

    public Factory() {
        // Hinweis: Ein leerer Konstruktor ohne Parameter muss nicht geschrieben werden,
        // wenn die Klasse keine anderen Konstruktoren hat.
        // In diesen Fällen erstellt Java automatisch einen Standardkonstruktor
        // für die Klasse, der ein leerer Konstruktor ohne Parameter ist.
    }

    public Packable produceNew() {
        // Das hier verwendete `Random`-Objekt kann verwendet werden, um Zufallszahlen zu ziehen.
        Random ticket = new Random();
        // Zieht eine Zahl aus dem Bereich [0, 4). Die Zahl wird 0, 1, 2 oder 3 sein.
        int number = ticket.nextInt(4);

        if (number == 0) {
            return new CD("Pink Floyd", "Dark Side of the Moon", 1973);
        } else if (number == 1) {
            return new CD("Wigwam", "Nuclear Nightclub", 1975);
        } else if (number == 2) {
            return new Book("Robert Martin", "Clean Code", 1);
        } else {
            return new Book("Kent Beck", "Test Driven Development", 0.7);
        }
    }
}

Die Factory kann verwendet werden, ohne genau zu wissen, welche verschiedenen Packable-Klassen existieren. Im nächsten Beispiel gibt es eine Klasse Packer, die eine Box mit Dingen bereitstellt. Ein Packer definiert eine Factory, die verwendet wird, um die Dinge zu erstellen:

public class Packer {
    private Factory factory;

    public Packer() {
        this.factory = new Factory();
    }

    public Box giveABoxOfThings() {
         Box box = new Box(100);

         int i = 0;
         while (i < 10) {
             Packable newThing = factory.produceNew();
             box.add(newThing);

             i = i + 1;
         }

         return box;
    }
}

Da der Packer die Klassen, die das Packable-Interface implementieren, nicht kennt, kann man neue Klassen hinzufügen, die das Interface implementieren, ohne den Packer zu ändern. Im nächsten Beispiel wird eine neue Klasse erstellt, die das Packable-Interface ChocolateBar implementiert. Die Factory wurde so geändert, dass sie zusätzlich zu Büchern und CDs auch Schokoladenriegel erstellt. Die Klasse Packer funktioniert ohne Änderungen mit der aktualisierten Version der Factory.

public class ChocolateBar implements Packable {
    // Da der automatisch generierte Standardkonstruktor von Java ausreicht,
    // benötigen wir keinen Konstruktor.

    public double weight() {
        return 0.2;
    }
}
import java.util.Random;

public class Factory {
    // Da der automatisch generierte Standardkonstruktor von Java ausreicht,
    // benötigen wir keinen Konstruktor.

    public Packable produceNew() {

        Random ticket = new Random();
        int number = ticket.nextInt(5);

        if (number == 0) {
            return new CD("Pink Floyd", "Dark Side of the Moon", 1973);
        } else if (number == 1) {
            return new CD("Wigwam", "Nuclear Nightclub", 1975);
        } else if (number == 2) {
            return new Book("Robert Martin", "Clean Code", 1);
        } else if (number == 3) {
            return new Book("Kent Beck", "Test Driven Development", 0.7);
        } else {
            return new ChocolateBar();
        }
    }
}

Eingebaute Interfaces

Java bietet eine beträchtliche Anzahl von eingebauten Interfaces. Hier werden wir uns mit vier häufig verwendeten Interfaces vertraut machen: List, Map, Set und Collection.

Das List-Interface

Das List-Interface definiert die grundlegende Funktionalität im Zusammenhang mit Listen. Da die Klasse ArrayList das List-Interface implementiert, kann sie auch über das List-Interface verwendet werden.
List<String> strings = new ArrayList<>();
strings.add("string objects inside an arraylist object!");
Wie wir aus der Java-API von List sehen können, gibt es viele Klassen, die das List-Interface implementieren. Eine Liste, die Informatikern bekannt ist, ist eine verkettete Liste. Eine verkettete Liste kann über das List-Interface genau so verwendet werden wie ein Objekt, das aus einer ArrayList erstellt wurde.
List<String> strings = new LinkedList<>();
strings.add("string objects inside a linkedlist object!");

Aus der Perspektive des Benutzers funktionieren beide Implementierungen des List-Interfaces auf die gleiche Weise. Das Interface abstrahiert ihre innere Funktionalität. Die internen Strukturen von ArrayList und LinkedList unterscheiden sich jedoch erheblich. ArrayList speichert Objekte in einem Array, bei dem das Abrufen eines Objekts mit einem bestimmten Index sehr schnell ist. Andererseits erstellt LinkedList eine Liste, bei der jedes Element eine

Referenz auf das nächste Element in der Liste enthält. Wenn man in einer verketteten Liste nach einem Objekt nach Index sucht, muss man die Liste von Anfang an durchgehen, bis man den Index erreicht.

Man kann bei ausreichend großen Listen deutliche Leistungsunterschiede zwischen den Listenimplementierungen feststellen. Die Stärke einer verketteten Liste liegt darin, dass das Hinzufügen immer schnell ist. ArrayList hingegen wird von einem Array unterstützt, das jedes Mal vergrößert werden muss, wenn es voll ist. Das Vergrößern des Arrays erfordert das Erstellen eines neuen Arrays und das Kopieren der Werte aus dem alten Array in das neue. Auf der anderen Seite ist die Suche nach Objekten nach Index in einer ArrayList im Vergleich zu einer verketteten Liste viel schneller.

Für die Probleme, denen Sie während dieses Kurses begegnen, sollten Sie fast immer ArrayList wählen. "Interface-Programmierung" ist jedoch von Vorteil: Implementieren Sie Ihre Programme so, dass Sie die Datenstrukturen über die Interfaces verwenden.

Loading

Das Map-Interface

Das Map-Interface definiert das grundlegende Verhalten im Zusammenhang mit Hashtabellen. Da die HashMap-Klasse das Map-Interface implementiert, kann sie auch über das Map-Interface verwendet werden.
Map<String, String> maps = new HashMap<>();
maps.put("ganbatte", "good luck");
maps.put("hai", "yes");

Die Schlüssel zur Hashtabelle werden mit der Methode keySet abgerufen.

Map<String, String> maps = new HashMap<>();
maps.put("ganbatte", "good luck");
maps.put("hai", "yes");

for (String key : maps.keySet()) {
    System.out.println(key + ": " + maps.get(key));
}
Beispielausgabe

ganbatte: good luck hai: yes

Die Methode keySet gibt eine Menge von Elementen zurück, die das Set-Interface implementieren. Sie können eine for-each-Schleife verwenden, um eine Menge durchzugehen, die das Set-Interface implementiert. Die Hash-Werte können aus der Hashtabelle mit der Methode values abgerufen werden. Die Methode values gibt eine Menge von Elementen zurück, die das Collection-Interface implementieren. Werfen wir einen kurzen Blick auf die Set- und Collection-Interfaces.

Loading

Das Set-Interface

Das Set-Interface beschreibt Funktionalitäten im Zusammenhang mit Mengen. In Java enthalten Mengen immer entweder 0 oder 1 Exemplare eines bestimmten Objekts. Beispielsweise implementiert das Set-Interface die Klasse HashSet. Hier ist, wie man die Elemente einer Menge durchläuft.
Set<String> set = new HashSet<>();
set.add("one");
set.add("one");
set.add("two");

for (String element: set) {
    System.out.println(element);
}
Beispielausgabe

one two

Beachten Sie, dass HashSet in keiner Weise die Reihenfolge einer Menge von Elementen annimmt. Wenn Objekte, die aus benutzerdefinierten Klassen erstellt wurden, dem HashSet-Objekt hinzugefügt werden, müssen sowohl die Methoden equals als auch hashCode definiert sein.

Loading

Das Collection-Interface

Das Collection-Interface beschreibt Funktionalitäten im Zusammenhang mit Sammlungen. Unter anderem werden Listen und Mengen in Java als Sammlungen kategorisiert – sowohl das List- als auch das Set-Interface implementieren das Collection-Interface. Das Collection-Interface bietet beispielsweise Methoden zur Überprüfung der Existenz eines Elements (contains-Methode) und zur Bestimmung der Größe einer Sammlung (size-Methode).

Das Collection-Interface legt auch fest, wie die Sammlung durchlaufen wird. Jede Klasse, die das Collection-Interface direkt oder indirekt implementiert, erbt die Funktionalität, die für eine for-each-Schleife erforderlich ist.

Erstellen wir eine Hashtabelle und durchlaufen ihre Schlüssel und Werte.

Map<String, String> translations = new HashMap<>();
translations.put("ganbatte", "good luck");
translations.put("hai", "yes");

Set<String> keys = translations.keySet();
Collection<String> keyCollection = keys;

System.out.println("Keys:");
for (String key: keyCollection) {
    System.out.println(key);
}

System.out.println();
System.out.println("Values:");
Collection<String> values = translations.values();

for (String value: values) {
    System.out.println(value);
}
Beispielausgabe

Keys: ganbatte hai

Values: yes good luck

In der nächsten Übung erstellen wir Funktionen im Zusammenhang mit E-Commerce und üben den Umgang mit Klassen über ihre Interfaces.

Loading
Sie haben das Ende dieses Abschnitts erreicht! Weiter zum nächsten Abschnitt: