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."));
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;
}
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.
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);
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());
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());
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!
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
Das List-Interface
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!");
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.
Das Map-Interface
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));
}
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.
Das Set-Interface
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);
}
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.
Das Collection-Interface
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);
}
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.