Previous Page TOC Index Next Page See Page

16

Pakete, Schnittstellen und interne Klassen

Von
Laura Lemay, Charles L. Perkins
und Michael Morrison

Pakete und Schnittstellen sind zwei Möglichkeiten, mit denen sich das Design aufeinander bezogener Klassen flexibler und übersichtlicher gestalten läßt. Pakete bieten die Gelegenheit, Gruppen von Klassen zu kombinieren und zu steuern, welche dieser Klassen für die Außenwelt zur Verfügung stehen sollen. Mit Schnittstellen lassen sich abstrakte Methodendefinitionen gruppieren und für verschiedenen Klassen zur Verfügung stellen, die diese Methoden nicht automatisch durch Erben erhalten.

Sie erfahren heute, wie sich Pakete und Schnittstellen verwenden und selbst erstellen lassen. Zu den im folgenden behandelten Themen gehört:

Programmieren im Großen und Programmieren im Kleinen

Bei der Durchsicht der Merkmale einer neuen Sprache sollten wir uns zwei Fragen stellen:

Den ersten Aspekt nennt man auch Programmierung im Großen, den zweiten Programmierung im Kleinen. Bill Joy, Mitbegründer von SUN Microsystems behauptet immer, daß sich Java beim Programmieren im Kleinen wie C und im Großen wie Smalltalk verhält. Er meint damit, daß Java beim Codieren von einzelnen Zeilen vertraut und leistungsstark wie C ist, beim Design aber die Erweiterbarkeit und Ausdrucksfähigkeit einer rein objektorientierten Sprache wie Smalltalk aufweist.

Die Trennung von Design und Codierung ist eine der wichtigsten Fortschritte der letzten Jahrzehnte in der Programmierung und objektorientierte Sprachen wie Java implementieren eine starke Form dieser Trennung. Der erste Teil dieser Trennung wurde bereits in den vorherigen Lektionen beschreiben: Wenn Sie ein Java-Programm entwickeln, entwerfen Sie zuerst die Klassen und bestimmen die Beziehungen zwischen diesen Klassen. Dann implementieren Sie den Java-Code nach Bedarf für jede Methode in Ihrem Design. Wenn Sie in beiden Prozessen sorgfältig arbeiten, können Sie verschiedene Designaspekte ändern, ohne daß sich das auf die lokalen Teile Ihres Java-Codes auswirkt. Und Sie können die Implementierung einer Methode jederzeit ändern, ohne daß sich das auf den Rest des Design auswirkt.

Je mehr Sie sich mit der Java-Programmierung befassen, um so deutlicher wird, daß dieses einfache Modell zu viele Einschränkungen auferlegt. Heute untersuchen wir diese Einschränkungen bei der Programmierung im Großen und im Kleinen, um die Notwendigkeit von Paketen und Schnittstellen zu verdeutlichen. Wir beginnen mit den Paketen.

Was sind Pakete?

Pakete sind, wie bereits einige Male erwähnt, eine Möglichkeit, Gruppen von Klassen zu organisieren. Ein Paket enthält eine beliebige Anzahl von Klassen, die jeweils nach Sinn, Verwendung oder auf der Grundlage der Vererbung zusammengefaßt werden.

Wozu sind Pakete notwendig? Wenn Ihre Programme klein sind und nur eine beschränkte Anzahl von Klassen verwenden, fragen Sie sich eventuell, warum Sie sich überhaupt mit Paketen befassen sollen. Aber je mehr Java-Programmierungen Sie vornehmen, desto mehr Klassen werden Sie verwenden. Und obwohl diese Klassen im einzelnen ein gutes Design aufweisen, einfach wieder zu verwenden sind, eingekapselt sind und über spezielle Schnittstellen zu anderen Klassen verfügen, stehen Sie vor der Notwendigkeit, eine größere Organisationseinheit zu verwenden, die es ermöglicht, Ihre Pakete zu gruppieren.

Pakete sind aus den folgenden Gründen sinnvoll:

Obwohl ein Paket im allgemeinen aus einer Sammlung von Klassen besteht, können Pakete wiederum auch andere Pakete enthalten und damit eine Hierarchieform bilden, die der Vererbungs-Hierarchie nicht unähnlich ist. Jede »Ebene« stellt dabei meist eine kleinere und noch spezifischere Gruppe von Klassen dar. Die Java-Klassenbibliothek selbst ist anhand dieser Struktur definiert. Die oberste Ebene trägt den Namen java; die nächste Ebene enthält Namen wie io, net, util und awt, die letzte und niedrigste Ebene enthält dann z.B. das Paket image.


Nach der geltenden Konvention gibt die erste Ebene der Hierarchie den (global eindeutigen) Namen der Firma, die das bzw. die Java-Pakete entwickelt hat, an. Die Klassen von SUN Microsystems beispielsweise, die nicht Teil der Java-Standardumgebung sind, beginnen alle mit dem Präfix sun. Klassen, die Netscape zusammen mit der Implementation einfügt, sind im Paket netscape enthalten. Das Standardpaket java bildet eine Ausnahme von dieser Regel, da es so grundlegend ist und eventuell eines Tages auch von vielen anderen Firmen implementiert wird. Nähere Informationen zu den Namenskonventionen bei Paketen erhalten Sie später, wenn Sie eigene Pakete erstellen.

Pakete verwenden

Sie haben in diesem Buch bereits mehrfach Pakete verwendet. Jedesmal, wenn Sie den Befehl import benutzt haben und immer dann, wenn Sie einen Bezug zu einer Klasse anhand des kompletten Paketnamens (z.B. java.awt.Color) hergestellt haben, haben Sie ein Paket verwendet. Im folgenden erfahren Sie, wie Sie Klassen aus anderen Paketen in eigenen Programmen benutzen können. Damit soll dieses Thema vertieft und sichergestellt werden, daß Sie es verstanden haben.

Um eine Klasse zu verwenden, die in einem Paket enthalten ist, können Sie eine der drei folgenden Techniken verwenden:

Was ist mit Ihren eigenen Klassen in Ihren Programmen, die nicht zu irgendeinem Paket gehören? Die Regel besagt, daß eine nicht exakt für ein bestimmtes Paket definierte Klasse in einem unbenannten Standardpaket plaziert wird. Den Bezug zu diesen Klassen stellen Sie her, indem Sie den Klassennamen an einer beliebigen Position im Code angeben.

Komplette Paket- und Klassennamen

Um den Bezug zur Klasse in einem andere Paket herzustellen, können Sie dessen kompletten Namen verwenden: Der Klassenname steht vor den Paketnamen. Sie müssen die Klassen oder Pakete nicht importieren, um sie auf diese Art zu verwenden.

java.awt.Font f=new.java.awt.Font()

Wenn Sie eine Klasse in einem Programm nur ein- oder zweimal verwenden, sollten Sie den kompletten Namen angeben. Wenn Sie eine bestimmte Klasse jedoch häufig benötigen oder der Paketname selbst wirklich lang ist und viele Unterpakete enthält, lohnt es sich, diese Klasse zu importieren, um Zeit bei der Eingabe des Namens zu sparen.

Der Befehl import

Sie können Klassen mit dem Befehl import importieren, wie Sie dies in den Beispielen dieses Buches bereits durchgeführt haben. Mit der folgenden Eingabe importieren Sie eine einzelne Klasse:

import java.util.Vector;

oder Sie importieren ein komplettes Klassenpaket, indem Sie einen Asterisk (*) anstelle der einzelnen Klassennamen verwenden:

import java.awt.*


Um technisch korrekt zu sein: Dieser Befehl importiert nicht alle Klassen in einem Paket – er importiert nur jene Klassen, die mit public als öffentlich erklärt wurden und selbst hierbei werden nur jene Klassen importiert, auf welche sich der Code selbst bezieht. Zu diesem Thema erhalten Sie später in dieser Lektion weitere Informationen.

Beachten Sie, daß der Asterisk (*) in diesem Beispiel nicht wie Sie das eventuell gewohnt sind, in der Befehlszeile verwendet wird, um die Inhalte eines Verzeichnisses zu definieren oder mehrere Dateien anzugeben. Wenn Sie z.B. den Inhalt des Verzeichnisses classes/java/awt/* auflisten lassen möchten, enthält diese Liste alle .class-Dateien und Unterverzeichnisse, wie image und peer. Wenn Sie importjava.awt.* schreiben, werden alle öffentlichen Klassen in diesem Paket importiert, aber keine Unterpakete wie image und peer. Um alle Klassen in einer komplexen Pakethierarchie zu importieren, müssen Sie jede Ebene dieser Hierarchie explizit manuell importieren. Sie können ferner keine partiellen Klassennamen angeben (z.B. L*, um alle Klassen zu importieren, die mit dem Buchstaben L beginnen). Entweder importieren Sie alle Klassen in einem Paket oder eine einzelne Klasse.

Die import-Anweisungen in Ihrer Klassendefinition sollten am Anfang der Datei stehen, vor allen Klassendefinitionen (aber nach der Paketdefinition – siehe den nächsten Abschnitt).

Empfiehlt es sich, die Klassen einzeln zu importieren oder sollten diese besser als Gruppe importiert werden? Dies hängt davon ab, wie speziell Sie verfahren möchten. Wenn Sie eine Gruppe von Klassen importieren, wird dadurch das Programm nicht verlangsamt oder »aufgebläht«; es werden nur diejenigen Klassen geladen, die aktuell vom Code verwendet werden. Wenn Sie Pakete importieren, ist das Lesen des Codes für andere allerdings etwas komplizierter, denn es liegt dann nicht mehr auf der Hand, woher die Klassen stammen. Ob Sie die Klassen einzeln oder als Pakete importieren ist überwiegend eine Frage des eigenen Programmierstils.


Der import-Befehl in Java ist dem Befehl #include in keiner Weise ähnlich. Daraus ergibt sich ein enormer Code, der über deutlich mehr Zeilen verfügt als das Originalprogramm aufwies. Der import-Befehl von Java agiert mehr als eine Art Verbindungsstück. Damit wird dem Java-Compiler und dem Interpreter mitgeteilt, wo (in welchen Dateien) die Klassen, Variablen, Methodennamen und die Methodendefinitionen zu finden sind. Damit wird nichts im aktuellem Java-Programm bewirkt.

Namenskonflikte

Nachdem Sie eine Klasse oder ein Paket von Klassen importiert haben, können Sie sich im allgemeinen auf eine Klasse einfach dadurch beziehen, indem Sie den Namen ohne Paket-Identifikation angeben. Ich sage »im allgemeinen« weil es einen Fall gibt, der ausdrücklicher definiert werden muß: wenn mehrere Klassen desselben Namens in unterschiedlichen Paketen vorhanden sind.

Im folgenden finden Sie ein Beispiel. Angenommen, Sie importieren die Klassen aus zwei Paketen von den beiden verschiedenen Programmierern (Joe und Eleanor):

Import joesclasses.*;

Import eleanorsclasses.*;

Innerhalb von Joes Paket befindet sich eine Klasse Name. Leider enthält auch Eleanors Paket eine Klasse mit dem Namen Name, die eine komplett andere Bedeutung und Implementation hat. Sie würden fragen, auf welche Version der Name-Klasse sich Ihr Programm bezieht, wenn Sie dies eingeben:

Name myName = new Name("Susan");

Doch dies ist nicht der Fall. Der Java-Compiler würde sich über den Namenskonflikt beschweren und die Kompilierung des Programms verweigern. In diesem Fall müssen Sie, trotz der Tatsache, daß Sie beide Klassen importiert haben, einen Bezug zur betreffenden name-Klasse anhand des vollständigen Paketnamens einfügen:

Name myName = new joesclasses.Name("Susan");

Anmerkung zu CLASSPATH und zur Position von Klassen

Ehe ich erkläre, wie Sie eigene Klassenpakete erstellen, möchte ich eine Anmerkung darüber machen, wie Java Pakete und Klassen findet, wenn es Ihre Klassen kompiliert und ausführt.

Damit Java eine Klasse verwenden kann, muß es diese im Dateisystem finden können. Andernfalls erhalten Sie eine Fehlermeldung, die besagt, daß die Klasse nicht existiert. Java verwendet zwei Elemente, um eine Klasse zu finden: den Namen des Pakets selbst und die Verzeichnisse, die in der Variablen CLASSPATH aufgelistet sind.

Zunächst zu den Paketnamen. Paketnamen entsprechen den Verzeichnisnamen im Dateisystem, d.h. die Klasse java.applet.Applet ist im Verzeichnis applet zu finden, welches wiederum im Verzeichnis java liegt (also java/applet/Applet.class).

Java sucht nach jenen Verzeichnissen innerhalb derjenigen Verzeichnisse, die in der Variablen CLASSPATH aufgelistet sind. Wenn Sie sich an den 1. Tag erinnern, als Sie JDK installiert haben, wissen Sie noch, daß Sie eine Variable CLASSPATH eingerichtet haben, um auf die verschiedenen Positionen zu verweisen, an denen sich die Java-Klassen befinden. CLASSPATH verweist im allgemeinen auf das Verzeichnis java/lib in Ihrer JDK-Version, ein Klassenverzeichnis in Ihrer Entwicklungsumgebung (falls vorhanden), eventuell einige Browser spezifischer Klassen und auf das aktuelle Verzeichnis. Wenn Java nach einer Klasse sucht, auf die Sie in der Quelle Bezug genommen haben, wird nach dem Paket- und Klassennamen in einem dieser Verzeichnisse gesucht und eine Fehlermeldung ausgegeben, falls die Klassendatei nicht gefunden werden kann. Die meisten Fehlermeldungen für nicht ladbare Klassendateien werden durch nicht vorhandene CLASSPATH -Variablen erzeugt.


Wenn Sie mit einer Macintosh-Version von JDK arbeiten, fragen Sie sich vermutlich, wovon hier die Rede ist. Das Mac-JDK verwendet die CLASS-PATH -Variable nicht; es weiß genug, um die Standardklassen und die Klassen aus dem aktuellen Verzeichnis zu finden. Wenn Sie jedoch umfangreiche Java-Entwicklungen durchführen, kann es sein, daß Sie andere Klassen und Pakete in anderen Verzeichnissen bearbeiten müssen. Der Java-Compiler enthält das Dialogfeld Preferences, mit dem Sie Verzeichnisse in den Suchpfad von Java einfügen können.

Eigene Pakete erstellen

Das Erstellen eigener Pakete ist ein komplexer, sehr schwieriger Vorgang, der viele Zeilen Code beansprucht, lange Nächte mit viel Kaffee erfordert und das rituelle Opfer einiger Ziegen – Spaß beiseite. Um ein Paket mit Klassen zu erstellen, müssen Sie drei grundlegende Schritte ausführen, die in den folgenden Abschnitten erläutert werden.

Paketnamen wählen

Der erste Schritt besteht darin, zu entscheiden, welchen Namen das Paket erhalten soll. Welcher Name für ein Paket gewählt werden soll, hängt davon ab, wie Sie die darin befindlichen Klassen verwenden möchten. Eventuell möchten Sie dem Pakete Ihren eigenen Namen geben, oder dieses nach einem bestimmten Teil des Java-Systems benennen, an dem Sie gearbeitet haben (z.B. graphics oder hardware-interfaces). Wenn Sie beabsichtigen, Ihr Paket im Netz weit zu verbreiten oder als Teil eines kommerziellen Produkts zu vertreiben, sollten Sie einen Paketnamen wählen (oder einen Satz von Paketnamen), der sowohl Sie als auch Ihre Organisation in einmaliger Weise kennzeichnet.

Eine Konvention für die Benennung von Paketen, die von SUN empfohlen wurde, ist, die Elemente des Internet-Domain-Namen zu vertauschen. Wenn SUN also seinem eigenen Rat folgen würde, müßten deren Pakete den Namen com.sun.java anstatt nur java verwenden. Wenn Ihr Internet-Domain-Name fooblitzky.eng.nonsense.edu lautet, könnte der Paketname sein: edu.nonsense.eng.fooblitzky (und Sie könnten daran noch weitere Paketnamen anhängen, die sich auf das Produkt oder Sie selbst beziehen).

Die Grundidee ist, daß ein Paketname einmalig sein sollte. Obwohl Pakete Klassen verbergen können, deren Namen in Konflikt geraten, ist dies auch schon der letzte Schutzmechanismus. Es gibt keine Möglichkeit sicherzustellen, daß das Paket mit dem Paket einer anderen Person in Konflikt gerät, die eventuell denselben Paketnamen verwendet.

Paketnamen beginnen laut Konvention mit einem kleingeschriebenen Buchstaben, um diese von Klassennamen zu unterscheiden. Im kompletten Namen der vordefinierten String-Klasse, java.lang.String, ist der Paketname visuell einfach vom Klassennamen zu unterscheiden. Diese Konvention trägt dazu bei, Namenskonflikte zu reduzieren,

Verzeichnisstruktur definieren

Der zweite Schritt für das Erstellen von Paketen besteht darin, eine Verzeichnisstruktur auf Ihrem Datenträger zu erstellen, die dem Paketnamen entspricht. Wenn das Paket nur einen Namen (mypackage) enthält, müssen Sie für diesen Namen ein Verzeichnis erstellen. Für das Beispiel des Paketnamens edu.nonsense.eng.fooblitzky müssen Sie das Verzeichnis edu erstellen, ein Verzeichnis nonsense innerhalb von edu, ein Verzeichnis eng innerhalb von nonsense und ein Verzeichnis fooblitzky innerhalb von eng. Die Klassen- und Quelldateien können dann in das Verzeichnis fooblitzky eingefügt werden.

Mit package Klassen in ein Paket einfügen

Der letzte Schritt besteht darin, die Klasse in die Pakete einzufügen; dies geschieht mit dem Befehl package in den Quelldateien. Der Befehl package sagt »diese Klasse soll in diesem Paket plaziert werden«. Er wird wie folgt verwendet:

package myclasses;

package edu.nonsense.eng.fooblitzky;
package java.awt;

Ein einzelner package-Befehl muß in die erste Zeile des Codes der Quelldatei eingefügt werden, nach den Kommentaren oder Leerzeilen und vor den import-Befehlen.

Wie bereits erwähnt, befindet sich eine Klasse, falls diese nicht über einen package-Befehl verfügt, im Standardpaket und läßt sich von anderen Klassen verwenden. Wenn Sie jedoch einmal damit begonnen haben, Pakete zu verwenden, sollten Sie sicherstellen, daß alle Klassen zu einem Paket gehören, um Verwirrungen über die Zugehörigkeit von Klassen zu vermeiden.

Pakete und Klassenschutz

Gestern haben Sie alles über Schutztechniken erfahren und darüber, wie diese den Methoden und Variablen zugeordnet sind bzw. ihre Beziehung zu anderen Klassen kennengelernt. Wenn Sie sich auf Klassen und deren Beziehung zu anderen Klassen in einem Paket beziehen möchten, müssen Sie nur folgende zwei Elemente im Auge behalten: package und public.

Standardmäßig verfügen Klassen über einen Paketschutz, d.h. daß die Klasse auch allen anderen Klassen in diesem Paket zur Verfügung steht, aber außerhalb und von Subpaketen nicht zu sehen oder verfügbar ist. Sie läßt sich nicht anhand des Namens importieren oder für einen Bezug verwenden. Klassen mit Paketschutz werden innerhalb desjenigen Pakets verborgen, in dem sie sich befinden.

Der Paketschutz findet statt, wenn Sie eine Klasse wie gewöhnlich definieren:

class TheHiddenClass extends AnotherHiddenClass {

...
}

Um eine Klasse auch außerhalb des betreffenden Pakets zur Verfügung zu stellen, können Sie diese mit einem öffentlichen Schutz versehen, indem Sie public in deren Definition einfügen:

public class TheVisibleClass {

...
}

Klassen, die mit public definiert sind, lassen sich von anderen Klassen außerhalb des Pakets importieren.

Beachten Sie, daß bei der Verwendung einer import-Anweisung mit einem Asterisk, lediglich die öffentlichen Klassen aus diesem Paket importiert werden. Verborgene Klassen bleiben verborgen und können nur von Klassen innerhalb dieses Pakets verwendet werden.

Warum soll eine Klasse in einem Paket verborgen werden? Aus demselben Grund, aus dem Sie auch Variablen und Methoden innerhalb einer Klasse verbergen: damit Sie Hilfsklassen und Verhalten zur Verfügung haben, die ausschließlich für die Implementierung notwendig sind. Damit läßt sich die Schnittstelle Ihres Programms auf die notwendigen Änderungen beschränken. Wenn Sie Ihre Klassen entwerfen, sollten Sie das gesamte Paket im Blick haben und entscheiden, welche Klasse public deklariert werden und welche Klasse verborgen sein soll.

Listing 16.1 zeigt zwei Klassen, die diesen Punkt darstellen. Die erste ist eine öffentliche Klasse, die eine verknüpfte Liste implementiert, die zweite ist eine privater Knoten dieser Liste.

Listing 16.1: Die public-Klasse LinkedList

 1: package collections;

2:
3: public class LinkedList {
4: private Node root;
5:
6: public void add(Object o) {
7: root = new Node(o, root);
8: }
9: . . .
10: }
11:
12: class Node { // nicht public
13: private Object contents;
14: private Node next;
15:
16: Node(Object o, Node n) {
17: contents = o;
18: next = n;
19: }
20: . . .
21: }


Beachten Sie, daß ich hier zwei Klassendefinitionen in eine Datei eingefügt habe. Ich habe es bereits einmal erwähnt, aber es soll auch an dieser Stelle noch einmal gesagt werden: Sie können in eine Datei beliebig viele Klassendefinitionen einfügen, von diesen kann aber nur eine public deklariert werden. Und dieser Dateiname muß denselben Namen haben wie die öffentliche Klasse. Wenn Java die Datei kompiliert, wird für jede Klassendefinition innerhalb der Datei eine eigene .class-Datei erstellt. In der Realität ist die Eins-zu-Eins-Entsprechung von Klassendefinition zu Datei einfach zu handhaben, weil Sie nicht lange nach der Definition einer Klasse suchen müssen.

Mit der öffentlichen Linked.List-Klasse soll eine Reihe nützlicher public-Methoden (z.B. add()) für andere Klassen bereitgestellt werden. Diese anderen Klassen benötigen keine Informationen über andere Hilfsklassen, die Linked.List für seine Aufgabe verwendet. Node, das eine dieser Hilfsklassen ist, wird deshalb ohne einen public-Modifier deklariert und erscheint nicht als Teil der öffentlichen Schnittstelle des Collection-Pakets.


Weil Node nicht public ist, bedeutet dies nicht, daß LinkedList keinen Zugang dazu hat, sobald es in eine andere Klasse importiert ist. Ein Schutz verbirgt nie die gesamten Klassen, sondern dient zur Prüfung der Erlaubnis, ob eine bestimmte Klasse andere Klassen, Variablen und Methoden verwenden kann. Wenn Sie LinkedList importieren und verwenden, wird auch die Node-Klasse in Ihr System geladen, aber nur die Instanzen von LinkedList haben die Erlaubnis, diese zu verwenden.

Es zählt zu den größten Stärken von verborgenen Klassen, daß auch bei deren Verwendung für die Einführung umfasssender Komplexität in die Implementierung einiger public-Klassen, diese gesamte Komplexitiät verborgen ist, sobald die Klasse importiert und verwendet wird. Deshalb gehört zur Erstellung eines guten Pakets auch die Definition eines kleinen, sauberen Satzes von public-Klassen und Methoden, die von anderen Klassen verwendet werden können. Diese sollten dann durch einige verborgene Hilfsklassen implementiert werden.

Was sind Schnittstellen?

Schnittstellen enthalten, ebenso wie die gestern erläuterten abstrakten Klassen und Methoden, Vorlagen für Verhalten, das andere Klassen implementieren sollen. Schnittstellen bieten jedoch ein bei weitem größeres Spektrum an Funktionalität für Java und für das Klassen- und Objektdesign als einfache abstrakte Klassen und Methoden. Der Rest dieser Lektion erforscht die Schnittstellen: Was sind sie, warum sind sie für eine effektive Nutzung der Sprache Java wichtig und wie lassen sie sich implementieren und verwenden.

Das Problem der einfachen Vererbung

Wenn man erstmals mit dem Design objektorientierter Programme beginnt, erscheint einem die Klassenhierarchie fast wie ein Wunder. Innerhalb dieses einzelnen Baumes können viele verschiedene Elemente ausgedrückt werden. Nach längeren Überlegungen und größerer praktischer Design-Erfahrung, entdecken Sie jedoch vermutlich, daß die reine Simplizität der Klassenhierarchie einschränkend ist, insbesondere, wenn Sie einige Verhalten verwenden, die von den Klassen in verschiedenen Verzweigungen derselben Struktur verwendet werden.

Lassen Sie uns einige Beispiele betrachten, die diese Problem verdeutlichen. Am 2. Tag, als Sie die Klassenhierarchie erstmals kennengelernt haben, wurde die Vehicle-Hierarchie erläutert, siehe Abb. 16.1.

siehe Abbildung

Abbildung 16.1:
Die Vehicle-
Hierarchie

Dieser Hierarchie sollen nun die Klassen BritishCars und BritishMotorcycle jeweils unterhalb von Car und unterhalb von Motorcycle hinzugefügt werden. Das Verhalten, das ein Auto oder ein Motorrad britisch macht (das eventuell Methoden für leakOil() oder electrical SystemFailure()enthält) ist diesen beiden Klassen gemeinsam, aber das sie in verschiedenen Bereichen der Klassenhierarchie angesiedelt sind, läßt sich für beide keine gemeinsame Superklasse erstellen. Sie können das British-Verhalten in der Hierarchie auch nicht heraufsetzen, weil dieses Verhalten allen Motorrädern und Autos gemeinsam ist. Wenn Sie das Verhalten zwischen diesen beiden Klassen nicht physikalisch kopieren möchten (und damit die Regeln der objektorientierten Programmierung (OOP) für die Wiederverwendung von Codes und gemeinsames Verhalten brechen), wie können Sie dann eine solche Hierarchie erstellen?

Lassen Sie uns einen Blick auf ein schwierigeres Beispiel werfen. Angenommen Sie haben eine biologische Hierarchie mit Tiere am Anfang erstellt und darunter befinden sich die Klassen Säugetiere und Vögel. Zu den Merkmalen, die ein Säugetier definieren, gehören das Gebären von lebenden Jungen und ein Fell. Das wesentliche Kennzeichen von Vögeln ist, daß Sie einen Schnabel haben und Eier legen. Soweit, so gut. Wie können Sie nun eine Klasse für ein Schnabeltier erstellen, das sowohl Fell als auch Schnabel hat und Eier legt? Sie müßten das Verhalten von zwei Klassen kombinieren, um die Schnabeltier-Klasse zu erstellen. Da Klassen in Java aber nur eine unmittelbare Superklasse haben können, läßt sich diese Art von Problemen nicht elegant lösen.

Andere OOP-Sprachen enthalten eine breiter gefächerte Vererbung, mit der sich solche Probleme lösen lassen. Bei mehrfacher Vererbung kann eine Klasse von mehr als einer Superklasse erben und das Verhalten und die Attribute von allen seinen Superklassen gleichzeitig übernehmen. Bei mehrfacher Vererbung könnten Sie das gemeinsame Verhalten von BritishCar und BritishMotorcycle in einer einzigen Klasse (BritishThing) zusammenfassen und dann neue Klassen erstellen, die sowohl von der primären Superklasse als auch von der British-Klasse erben.

Das Problem der Mehrfachvererbung besteht darin, daß eine Programmiersprache dadurch äußerst komplex wird, dies betrifft das Lernen, die Verwendung und die Implementierung. Die Fragen zum Aufruf von Methoden und zur Organisation der Klassenhierarchie werden bei einer Mehrfachvererbung deutlich komplizierter. Zweideutigkeiten und Verwirrungen sind dann Tür und Tor geöffnet. Deshalb beschloß man dieses Element zu Gunsten einer Einfachvererbung auszuschließen.

Wie läßt sich also das Problem von allgemeinem Verhalten lösen, das nicht in den strengen Rahmen der Klassenhierarchie paßt? Java, in Anlehnung an Objective-C, verwendet eine weitere Hierarchie, die aber von der Hauptklassen-Hierarchie verschieden ist – eine Hierarchie für gemischtes Klassenverhalten. Wenn Sie dann eine neue Klasse erstellen, verfügt diese zwar über nur eine direkte Superklasse, kann aber verschiedenes Verhalten aus anderen Hierarchien übernehmen.

Diese andere Hierarchie ist die Schnittstellen-Hierarchie. Eine Java-Schnittstelle ist eine Sammlung von abstraktem Verhalten, das sich in jeder beliebigen Klasse mischen läßt, um jenes Klassenverhalten hinzuzufügen, das von deren Superklassen nicht unterstützt wird. Genau genommen enthält eine Java-Schnittstelle nichts anderes als abstrakte Methodendeklaration und Konstanten – keine Instanzvariablen und keine Methodenimplementierungen.

Schnittstellen werden in der Klassenbibliothek von Java implementiert und verwendet, wann immer ein Verhalten wahrscheinlich von einigen anderen Klassen implementiert werden soll. Die Java-Klassenhierarchie definiert und verwendet z.B. die Schnittstellen java.lang.Runnable, java.util.Enumeration, java.util.Observable, java.awt.image.ImageConsumer und java.awt.imageProducer. Einige dieser Schnittstellen haben Sie bereits kennengelernt, andere werden Sie später in diesem Buch noch entdecken. Und wieder andere sind für Ihre Programme eventuell sinnvoll, weshalb Sie in der API nachschlagen sollten, was hier für Sie zur Verfügung steht.

Abstraktes Design und konkrete Implementierung

In diesem Buch haben Sie bereits einen Eindruck von den Unterschieden zwischen Design und Implementierung bei der objektorientierten Programmierung erhalten. Das Design ist eine Art abstrakter Darstellung und deren Implementierung ist das konkrete Gegenstück zum Design. Sie konnten dies bei Methoden verfolgen, deren Methodensignaturen den Verwendungszweck definieren, aber die Methodenimplementierung kann an einer beliebigen Position in der Klassenhierarchie stattfinden. Sie konnten dies bei abstrakten Klassen verfolgen, in denen das Klassendesign eine Vorlage für Verhalten bietet, aber das Verhalten selbst erst weiter unten in der Hierarchie implementiert wird.

Die Trennung zwischen Design und Implementierung einer Klasse oder einer Methode zählt zu den entscheidenden Punkten der Theorie der objektorientierten Programmierung. Wenn Sie beim Organisieren von Klassen an das Design denken, erhalten Sie ein alles umfassendes Bild und müssen sich nicht mit den Details der Implementierung befassen. Ist das allgemeine Design einmal erstellt, wenn Sie mit der tatsächlichen Implementierung beginnen, können Sie sich ferner in Ruhe auf die Details der jeweiligen Klassen konzentrieren. Das Programmierungskonzept »Global denken, lokal handeln« bietet eine leistungsstarke Möglichkeit, an das allgemeine Design von Klassen und Programmen zu denken, deren Organisation zu beachten und die jeweiligen Bezüge im Auge zu behalten.

Eine Schnittstelle besteht aus einem Satz von Methodensignaturen ohne Implementierungen und verkörpert das pure Design. Wenn Sie eine Schnittstelle mit Ihrer Klasse mischen, passen Sie dieses Design in Ihre Implementierung ein. Das Design läßt sich dann sicher an andere Positionen der Klassenhierarchie einfügen, da es keine klassenspezifischen Details dafür gibt, wie sich eine Schnittstelle verhält – nichts zu überschreiben, nichts zu verfolgen, es sind nur der Name und die Argumente der Methode erforderlich.

Was ist mit den abstrakten Klassen? Bieten diese nicht dasselbe Verhalten? Ja und nein. Abstrakte Klassen und die abstrakten Methoden darin, sehen zwar auch eine Trennung von Design und Implementierung vor und ermöglichen es, ein allgemeines Verhalten in eine abstrakte Superklasse einzuführen. Aber abstrakte Klassen können – und dies tun sich auch häufig – einige konkrete Daten enthalten (wie Instanzvariablen) und Sie können eine abstrakte Superklasse sowohl mit abstrakten als auch mit herkömmlichen Methoden ausstatten – dadurch wird die Trennung verschwommen.

Selbst eine rein abstrakte Klasse mit ausschließlich abstrakten Methoden kann nicht so leistungsstark sein wie eine Schnittstelle. Eine abstrakte Klasse ist einfach nur eine andere Klasse; sie erbt von einer bestimmten Klasse und hat ihren festen Platz in der Hierarchie. Abstrakte Klassen können keine allgemeine Basis für verschiedene Bereiche einer Klassenhierarchie bilden, wie dies bei Schnittstellen möglich ist, und sich lassen sich auch nicht mit anderen Klassen mischen, die deren Verhalten ebenfalls benötigen. Um diese Art von Flexibilität zu erlangen, müssen Sie mit Schnittstellen arbeiten.

Sie können sich den Unterschied zwischen dem Design und der Implementierung einer beliebigen Java-Klasse ebenso vorstellen wie den Unterschied zwischen Schnittstellen-Hierarchie und Design-Hierarchie. Die einfach vererbende Klassenhierarchie enthält die Implementierungen, wobei die Beziehungen zwischen den Klassen und dem Verhalten rigid definiert sind. Die mehrfach vererbende Schnittstellen-Hierarchie enthält jedoch das Design und läßt sich überall dort frei verwenden, wo es für die Implementierung notwendig ist. Dies ist eine leistungsstarke Möglichkeit, das Programm zu organisieren und obwohl diese Technik eventuell ein gewöhnungsbedürftig ist, ist sie doch äußerst empfehlenswert.

Schnittstellen und Klassen

Klassen und Schnittstellen haben – trotz ihrer unterschiedlichen Definition – viele Gemeinsamkeiten. Schnittstellen werden ebenso wie Klassen in Quelldateien deklariert, eine Schnittstelle in einer Datei. Ebenso wie Klassen können Sie auch mit dem Java-Compiler in .class-Dateien kompiliert werden. Und in den meisten Fällen können Sie anstelle von Klassen auch eine Schnittstelle verwenden.

In beinahe allen Beispielen aus diesem Buch werden Klassennamen verwendet, die sich durch einen Schnittstellen-Namen ersetzen lassen. Java-Programmierer sprechen sogar häufig von »Klassen«, wenn sie eigentlich »Klassen oder Schnittstellen« meinen. Schnittstellen ergänzen das Leistungsvermögen von Klassen und bauen dieses weiter aus. Beide lassen sich beinahe auf dieselbe Weise behandeln. Einer der wenigen Unterschiede besteht allerdings darin, daß eine Schnittstelle nicht als Instanz verwendet werden kann: new kann nur eine Instanz für eine Klasse erstellen.

Schnittstellen implementieren und verwenden

Sie wissen nun, was Schnittstellen sind und warum sie so leistungsstark sind (der Teil »Programmieren im Großen«). Im folgenden soll der Blick auf die einzelnen Codierungen geworfen werden (der Teil »Programmieren im Kleinen«). Mit Schnittstellen lassen sich im wesentlichen auf zwei Arten verwenden: Sie können diese in Ihren eigenen Klassen benutzen oder eigene Schnittstellen definieren. Zunächst soll die erste Variante erläutert werden.

Das Schlüsselwort implements

Um eine Schnittstelle zu verwenden, fügen Sie das Schlüsselwort implements als Teil der Klassendefinition ein. Sie haben dies bereits am 11. Tag durchgeführt, als Sie über Threads informiert wurden und die Schnittstelle Runnable in Ihre Applet-Definition eingefügt haben:

// java.applet.Applet ist die Superklasse 

public class Neko extends java.applet.Applet
implements Runnable { // zusätzlich verfügt die Klasse über das Runnable-
// Verhalten
...
}

Da Schnittstellen nichts anderes als abstrakte Methoden-Deklarationen enthalten, müssen Sie diese Methoden dann in Ihre eigenen Klassen implementieren, indem Sie dieselben Methodensignaturen der Schnittstelle verwenden. Beachten Sie, daß für eine einmal eingefügte Schnittstelle, alle darin enthaltenen Methoden implementiert werden müssen – Sie können nicht nur jene Methoden auswählen, die Sie benötigen. Indem Sie eine Schnittstelle implementieren, teilen Sie den Benutzern Ihrer Klasse mit, daß Sie die gesamte Schnittstelle unterstützen (auch dies ist ein Unterschied zwischen Schnittstellen und abstrakten Klassen).

Nachdem Ihre Klasse ein Schnittstelle implementiert hat, können die Subklassen dieser Klasse diese neuen Methoden erben (und diese überschreiben oder überladen), ebenso als wären diese in der Superklasse definiert. Wenn Ihre Klasse von einer Superklasse erbt, die eine bestimmte Schnittstelle implementiert, müssen Sie das Schlüsselwort implements nicht in die eigene Klassendefinition einfügen.

Lassen Sie uns ein einfaches Beispiel verwenden und die neue Klassse Orange erstellen. Angenommen, Sie haben bereits die Klasse Fruit und eine Schnittstelle Fruitlike erstellt, die darstellt, was Fruits im allgemeinen durchführen können soll. Sie möchten zum einen, daß Orange eine Fruit ist, aber es soll auch ein kugelförmiges Objekt sein, daß sich drehen und wenden läßt. Im folgenden sehen Sie, wie sich dies alles ausdrücken läßt (beachten Sie die Definitionen für diese Schnittstellen im Augenblick nicht; Sie erfahren später mehr darüber):

interface  Fruitlike {

void decay();
void squish();
. . .
}

class Fruit implements Fruitlike {
private Color myColor;
private int daysTilIRot;
. . .
}

interface Spherelike {
void toss();
void rotate();
. . .
}

class Orange extends Fruit implements Spherelike {
. . . // toss() könnte squish() aufrufen
}

Beachten Sie, daß die Klasse Orange nicht mit den Worten implements Fruitlike versehen sein muß, weil Fruit bereits darüber verfügt. Es gehört zu den vorteilhaften Errungenschaften dieser Struktur, daß Sie Ihre Ansicht darüber, wohin sich die Klasse Orange erstrecken soll (wenn z.B. plötzlich eine großartige Raumklasse eingeführt wird) jederzeit ändern können. Dennoch wird die Klasse Orange dieselben beiden Schnittstellen verstehen:

private float  radius;

. . .
}

class Orange extends Sphere implements Fruitlike {
. . . // Die Benutzer von Orange müssen von dieser Veränderung nichts
// wissen!
}

Mehrere Schnittstellen implementieren

Im Gegensatz zur einfach vererbbare Klassenhierarchie können Sie beliebig viele Schnittstellen in Ihre eigenen Klassen einfügen. Die Klassen implementieren das kombinierte Verhalten aus allen einbezogenen Schnittstellen. Um mehrere Schnittstellen in eine Klasse einzufügen, trennen Sie deren Namen durch Kommas:

public class Neko extends java.applet.Applet 

implements Runnable, Eatable, Sortable, Observable {
...
}

Beachten Sie, daß sich aus der Implementierung mehrerer Schnittstellen Komplikationen ergeben können, wenn zwei verschiedene Schnittstellen jeweils dieselbe Methode definieren. Es gibt drei Möglichkeiten, dies zu lösen:

Andere Verwendungen für Schnittstellen

Vergegenwärtigen Sie sich, daß Sie beinahe überall anstelle einer Klasse auch eine Schnittstelle verwenden können. Sie können also eine Variable als Schnittstellentyp deklarieren:

Runnable aRunnableObject = new MyAnimationClass()

Wenn eine Variable als Schnittstellentyp deklariert ist, bedeutet dies, daß von jedem Objekt, auf welches sich die Variable bezieht, angenommen wird, es habe diese Schnittstelle implementiert – d.h. es wird also davon ausgegangen, daß es alle Methoden versteht, die von der Schnittstelle angegeben sind. Es wird vorausgesetzt, daß das Versprechen zwischen dem Designer der Schnittstelle und dessen potentiellen Implementatoren gehalten worden ist. In diesem Fall wird also davon ausgegangen, daß Sie aRunnableObject.run() aufrufen können, weil aRunnableObject ein Objekt des Typs Runnable enthält.

Es ist wichtig zu wissen, daß obwohl von aRunnableObject eine run()-Methode erwartet wird, der Code bereits geschrieben werden kann, lange bevor Klassen erstellt und implementiert werden. In der traditionellen, objektorientierten Programmierung ist man gezwungen, eine Klasse mit »Stub«-Implementierungen (leere Methoden oder Methoden, die sinnlose Meldungen ausgeben) zu erstellen, um die gleiche Wirkung zu erzielen.

Sie können Objekte auch für eine Schnittstelle bereitstellen, ebenso wie Sie Objekte für Klassen bereitstellen können. Lassen Sie uns für dieses Beispiel zur Definition der Orange-Klasse zurückkehren, die sowohl die Fruitlike-Schnittstelle (durch deren Superklasse Fruit) als auch die Spherelike-Schnittstelle implementiert. Hier werden die Instanzen von Orange für beide Klassen und Schnittstellen definiert:

Orange      anOrange    = new Orange();

Fruit aFruit = (Fruit)anOrange;
Fruitlike aFruitlike = (Fruitlike)anOrange;
Spherelike aSpherelike = (Spherelike)anOrange;


aFruit.decay(); // fruits decay()
aFruitlike.squish(); // und squish()

aFruitlike.toss(); // Dinge, die Fruitlike implementieren implementieren toss()nicht;
aSpherelike.toss() // die Dinge, die Spherelike implemenieren, implementieren toss
anOrange.decay(); // oranges können das alles
anOrange.squish();
anOrange.toss();
anOrange.rotate();

In diesem Beispiel wird eine Orange durch Deklarationen auf die Fähigkeiten einer Frucht oder Kugel eingeschränkt.

Beachten Sie schließlich, daß sich Schnittstellen zwar im allgemeinen mit dem Verhalten andere Klassen (Methodenunterschrift) mischen lassen, sich aber auch mit allgemein sinnvollen Konstanten mischen lassen. Wenn also zum Beispiel eine Schnittstelle als Satz von Konstanten definiert ist, und mehrere Klassen dann diese Konstanten verwendet haben, könnten die Werte dieser Konstanten global geändert werden ohne viele Klassen einzeln ändern zu müssen. Dies ist auch ein weiteres Beispiel dafür, wo sich die Verwendung von Schnittstellen zur Trennung zwischen Design und Implementierung einen Code verallgemeinern und einfacher gestalten läßt.

Schnittstellen definieren und ausdehnen

Wenn Sie einige Zeit mit Schnittstellen gearbeitet haben, besteht der nächste Schritt darin, eigene Schnittstellen zu definieren. Schnittstellen sind den Klassen sehr ähnlich und sie werden beinahe in derselben Weise deklariert und in einer Hierarchie angeordnet, aber für die Deklaration von Schnittstellen gibt es Regeln, die befolgt werden müssen.

Neue Schnittstellen

Um eine neue Schnittstelle zu erstellen, deklarieren Sie folgendes:

public interface Growable {
...
}

Die ist im Grunde dasselbe wie eine Klassendefinition, wobei das Wort interface das Wort class ersetzt. Innerhalb der Schnittstellen-Definition befinden sich die Methoden und Konstanten. Die Methodendefinitionen innerhalb einer Schnittstelle sind public- und abstract-Methoden. Sie können diese explizit als solche deklarieren oder sie werden in public- und abstract-Methoden verwandelt, wenn Sie diese Modifier nicht einfügen. Eine Methode innerhalb einer Schnittstelle läßt sich nicht als private oder protected deklarieren. Im folgenden Beispiel ist die Growable-Schnittstelle sowohl public als auch abstract (growIt()) und eine ist implizit als solche deklariert (growItBigger()).

public interface Growable {

public abstract void growIt(); //explizit public und abstract
void growItBigger(); // effektiv public und abstract
}

Beachten Sie, daß ebenso wie bei abstrakten Methoden in Klassen, auch die Methoden innerhalb von Schnittstellen keinen Rumpf haben. Eine Schnittstelle ist Design in Reinform; es gibt keine Implementierungen.

Neben den Methoden können Schnittstellen auch Variablen enthalten, aber diese Variablen müssen public, static, und final deklariert sein. Ebenso wie bei Methoden können Sie eine Variable explizit als public, static und final deklarieren oder diese implizit als solche definieren, wenn keiner diese Modifier verwendet wird. Im folgenden finden Sie dieselbe Growable-Definition mit zwei neuen Variablen:

public interface Growable {

public static final int increment = 10;
long maxnum = 1000000; // wird public static und final

public abstract void growIt(); //explizit public und abstract
void growItBigger(); // effektiv public und abstract
}

Schnittstellen müssen entweder über einen öffentlichen oder einen Paket-Schutz verfügen. Beachten Sie jedoch, daß Schnittstellen ohne public-Modifier ihre Methoden nicht automatisch in public und abstract konvertieren und auch deren Konstanten nicht in public konvertiert werden. Eine nicht-öffentliche Schnittstelle verfügt auch über nicht-öffentliche Methoden und Konstanten, die sich nur von Klassen und anderen Schnittstellen desselben Pakets verwenden lassen.

Schnittstellen können ähnlich wie Klassen zu einem Paket gehören, wenn in der ersten Zeile der Klassendatei die package-Anweisung eingefügt wird. Schnittstellen können auch andere Schnittstellen und Klassen aus anderen Paketen importieren, ebenso wie dies bei Klassen möglich ist.

Methoden innerhalb von Schnittstellen

Zu Methoden innerhalb von Schnittstellen ist folgender Trick anzumerken: Diese Methoden sollten abstrakt sein und einer beliebigen Klasse zugeordnet werden können, aber wie lassen sich die Parameter für diese Methoden definieren? Sie wissen ja nicht, wleche Klasse sie verwendet!

Die Antwort liegt in der Tatsache, daß Sie einen Schnittstellennamen überall dort verwenden können, wo Sie einen Klassennamen benutzen – wie Sie bereits gelernt haben. Indem sie Ihre Methodenparameter als Schnittstellentypen definieren, erzeugen Sie generische Parameter, die sich allen Klassen zuweisen lassen, die diese Schnittstelle eventuell verwenden.

Als Beispiel dient die Schnittstelle Fruitlike, die Methoden (ohne Argumente) für decay() und squish() definiert. Hier könnte es auch die Methode für germinateSeeds() geben, die ein Argument hat: die Frucht selbst. Welchem Typus sollte dieses Argument angehören? Es kann nicht einfach Fruit sein, weil es eine Klasse wie Fruitlike (welche die Schnittstelle Fruitlike implementiert) geben könnte, die aber keine Frucht ist. Die Lösung besteht darin, einfach das Argument als Fruitlike in der Schnittstelle zu deklarieren:

public interface Fruitlike {

public abstract germinate(Fruitlike self) {
...
}
}

In der tatsächlichen Implementierung für diese Methode in einer Klasse können Sie das generische Argument Fruitlike aufgreifen und an das geeignete Objekt weiterreichen:

public class Orange extends Fruit {


public germinate(Fruitlike self) {
Orange theOrange = (Orange)self;
...
}
}

Schnittstellen ableiten

Schnittstellen sind ebenso wie Klassen in einer Hierarchie organisiert. Wenn eine Schnittstelle von einer anderen Schnittstelle erbt, übernimmt diese untergeordnete Schnittstelle alle Methodendefinitionen und Konstanten der »Superschnittstelle«. Um eine Schnittstelle abzuleiten, verwenden Sie das Schlüsselwort extends ebenso wie in einer Klassendefinition:

public interface Fruitlike extends Foodlike { 

...
}

Beachten Sie, daß die Schnittstellenhierarchie im Gegensatz zur Klassenhierarchie kein Äquivalent für die Object-Klasse besitzt; diese Hierarchie verzweigt sich nicht bis zu irgendeinem Punkt. Schnittstellen können entweder ganz selbständig bestehen oder von anderen Schnittstellen abgeleitet sein.

Ein weiterer Unterschied zur Klassenhierarchie besteht darin, daß die Vererbungshierarchie eine mehrfache Vererbung beinhaltet. Eine einfache Schnittstelle kann sich also auf die benötigte Anzahl von Klassen erstrecken (durch Kommas im extends-Teil der Definition getrennt) und die neue Schnittstelle enthält eine Kombination aller übergeordneten Methoden und Konstanten. Im folgenden finden Sie eine Schnittstellen-Definition für eine Schnittstelle namens BusyInterface, die von allen anderen Schnittstellen erbt:

public interface BusyInterface extends Runnable, Growable, Fruitlike, Observable {

...}

Bei mehrfach vererbenden Schnittstellen gelten dieselben Regeln für Namenskonflikte wie bei Klassen, die mehrere Schnittstellen verwenden. Methoden, deren Return-Typ sich unterscheidet, erzeugen einen Compiler-Fehler.

Ein Beispiel: Verbundene Listen aufzählen

Um die heutige Lektion abzuschließen, finden Sie im folgenden ein Beispiel, das Pakete und Paketschutz verwendet und eine Klasse definiert, die die Enumeration-Schnittstelle implementiert (Teil des java.util-Pakets). Listing 16.2 zeigt den Code.

Listing 16.2: Pakete, Klassen und Schnittstellen

 1:package  collections;

2:
3: public class LinkedList {
4: private Node root;
5:
6: . . .
7: public Enumeration enumerate() {
8: return new LinkedListEnumerator(root);
9: }
10: }
11:
12: class Node {
13: private Object contents;
14: private Node next;
15:
16: . . .
17: public Object contents() {
18: return contents;
19: }
20:
21: public Node next() {
22: return next;
23: }
24: }
25:
26: class LinkedListEnumerator implements Enumeration {
27: private Node currentNode;
28:
29: LinkedListEnumerator(Node root) {
30: currentNode = root;
31: }
32:
33: public boolean hasMoreElements() {
34: return currentNode != null;
35: }
36:
37: public Object nextElement() {
38: Object anObject = currentNode.contents();
39:
40: currentNode = currentNode.next();
41: return anObject;
42: }
43: }

Im folgenden finden Sie eine typische Verwendung für die Aufzählung:

Collections.LinkedList aLinkedList = createLinkedListe();

Java.util.Enumeration e =aLinkedList.enumerate();
While (e.hasMOreElements() {
Object anObject = e.nextElement();
//stellen Sie mit dem Objekt etwas Sinnvolles an
}

Beachten Sie, daß wir Enumeration e zwar so benutzen, als wüßten wir, was das ist – wir wissen es aber nicht. Es handelt sich um eine Instanz einer verborgenen Klasse (LinkedListEnumeration), die man nicht direkt sehen oder benutzen kann. Durch eine Kombination von Paketen und Schnittstellen gelingt es der Linked-List-Klasse, eine transparente public-Schnittstelle für eines ihrer wichtigsten Verhalten (über die bereits definierte Schnittstelle java.util.Enumeration) bereitzustellen, während ihre zwei Implementierungsklassen nach wie vor gekapselt (verborgen) sind.

Die Weitergabe eines Objekts auf diese Art nennt man Vending. Meist gibt der »Vendor« ein Objekt weiter, das der Empfänger nicht selbst erstellen kann, aber weiß, wie es zu benutzen ist. Durch Zurückgeben des Objekts an den Vendor kann der Empfänger beweisen, daß er gewisse Fähigkeiten hat und verschiedene Aufgaben ausführen kann – und das alles, ohne viel über das weitergegebene Objekt zu wissen. Das ist ein leistungsstarkes Konzept, das in vielen Situationen anzuwenden ist.

Interne Klassen

Die meisten Java-Klassen wurden auf der Paketebene definiert, das bedeutet, daß jede Klasse ein Mitglied eine speziellen Pakets ist. Selbst wenn Sie keine explizite Verbindung zwischen einem Paket und einer Klasse herstellen, wird das Standardpaket vorausgesetzt. Klassen, die auf der Paketebene definiert sind, werden Top-Level-Klassen genannt. Vor Java 1.1 waren Top-Level-Klassen die einzigen Klassentypen, die unterstützt wurden. Doch Java 1.1 hat einen offeneren Zugang zu den Klassendefinitionen. Java 1.1 unterstützt interne Klassen; dies sind Klassen, die sich für jeden beliebigen Zweck definieren lassen. Das heißt, eine Klasse kann als Mitglied einer anderen Klasse definiert werden oder innerhalb eines Anweisungsblocks bzw. anonym in einem Ausdruck.

Interne Klassen scheinen nur eine geringfügige Erweiterung der Sprache Java zu sein, aber wenn Sie bedenken, daß die internen Klassen die einzige wirkliche Änderung sind, die an Java 1.1 selbst vorgenommen wurde, wird deren Ausmaß vielleicht deutlich. Alle anderen Erweiterungen in der Version 1.1 von Java treten in Form von APIs auf. Warum wurde die Sprache selbst wegen einer scheinbar so abstrakten Sache wie interne Klassen geändert? Die Antwort auf diese Frage ist nicht ganz einfach. Da eine ausführliche Diskussion das Ziel dieses Buches überschreiten würde, soll die Notwendigkeit von internen Klassen wenigstens wie folgt zusammengefaßt werden: Das neue Ereignismodell 1.1 von Java AWT benötigte einen speziellen Mechanismus wie die internen Klassen, um funktionieren zu können.

Die Regeln, welche den Zweck von internen Klassen bestimmen, entsprechen der Verwaltung von Variablen. Der Name einer internen Klasse ist außerhalb seines Bereichs nicht sichtbar, es sei denn in einem vollständig qualifizierten Namen, der bei der Strukturierung von Klassen innerhalb eines Pakets hilft. Der Code für eine interne Klasse kann einfache Namen von einschließenden Bereichen verwenden, Klassen- und Mitglieder-Variablen von einschließenden Klassen und lokale Variablen einschließender Blöcke einfügen. Darüber hinaus können Sie eine Top-Level-Klasse als statisches Mitglied einer anderen Top-Level-Klasse definieren. Im Gegensatz zu einer internen Klasse kann eine Top-Level-Klasse die Instanzvariablen anderer Klassen nicht direkt verwenden. Die Fähigkeit, Klassen auf diese Weise zu verschachteln, ermöglicht es jeder Top-Level-Klasse, für eine Paket-Organisation logisch aufeinander bezogener Klassen zweiter Ebene zu sorgen.


Die Unterstützung für interne Klassen in Java 1.1 wird komplett vom Java-Compiler durchgeführt und erforderte keine Änderungen an der Java Virtual Machine (VM). Dies war ein wichtiger Grund, warum die Java-Architekten einer Unterstütung von internen Klassen in Java zugestimmt haben – Sie wußten, daß die VM dadurch nicht zusätzlich belastet würde.

Weitere Informationen zu internen Klassen finden Sie auf der WebSite von Sun: http://www.sun.com.

Zusammenfassung

Heute haben Sie gelernt, wie Pakete benutzt werden können, um Klassen in aussagefähige Gruppen zusammenzufassen. Pakete werden in einer Hierarchie angeordnet. Dadurch kann nicht nur der Programmierer seine Programme besser organisieren, sondern Millionen von Java-Programmierern erhalten eine Möglichkeit, ihre Projekte im Internet eindeutig zu benennen und gemeinsam zu nutzen.

Sie haben ferner gelernt, wie Pakete, – sowohl Ihre eigenen als auch die vordefinierten aus der Klassenbibliothek von Java, verwendet werden.

Anschließend haben Sie erfahren, wie man Schnittstellen deklariert und benutzt. Das ist ein starker Mechanismus zur Erweiterung der traditionellen Einfachvererbung von Java-Klassen und zum Trennen von Design und Implementierung. Schnittstellen werden vorwiegend benutzt, um gemeinsame Methoden aufzurufen, wenn die jeweilige Klasse nicht bekannt ist. Sie lernen morgen und übermorgen noch mehr über Schnittstellen.

Schließlich haben Sie gelernt, daß Pakete und Schnittstellen kombiniert werden können und damit nützliche Abstraktionen bieten, z.B. LinkedList, die einfach erscheinen, dennoch aber einen Großteil der Implementierung vor den Benutzern verbergen. Das ist eine leistungsstarke Technik.

Fragen und Antworten

F Kann ich auch import some.package.B* angeben, um alle Klassen in diesem Paket, die mit B beginnen zu importieren?

A Nein, der import-Asterisk (*) ist nicht zu verwechseln mit dem Asterisk aus der Befehlszeile.

F Was bedeutet dann eigentlich import mit einem Asterisk?

A Damit werden alle public-Klassen importiert, die sich direkt in dem genannten Paket befinden, nicht in einem der Unterpakete. (Sie können nur diese Klassen oder eine explizit benannte Klasse aus einem bestimmten Paket importieren.) Übrigens, Java »lädt« nur die Informationen für die Klasse, wenn Sie in Ihrem Code auf diese Klasse hinweisen, deshalb ist die *-Form von import nicht weniger effizient als die Benennung der einzelnen Klassen.

F Ist die Mehrfachvererbung derart komplex, daß sie nicht in Java übernommen wurde?

A Der Grund ist nicht unbedingt die Komplexität, aber die Sprache wird dadurch übermäßig kompliziert. Wie Sie am letzten Tag noch lernen werden, kann dies dazu beitragen, daß größere Systeme an Sicherheit einbüßen. Nehmen wir z.B. einmal an, daß in Ihrem Code von zwei verschiedenen Eltern geerbt wird, die jeweils eine Instanzvariable mit dem gleichen Namen haben. Sie wären gezwungen, den Konflikt zuzulassen und genau zu erklären, wie sich die gleichen Referenzen auf diesen Variablennamen in beiden Superklassen unterscheiden. Anstatt in der Lage zu sein, Methoden aus Superklasen aufzurufen, um ein abstrakteres Verhalten zu erzielen, müßten Sie sich immer Gedanken machen, welche der (möglicherweise vielen) identischen Methoden tatsächlich in welchen Superklassen aufzurufen sind. Das wirkt sich natürlich auch auf die Laufzeit Ihres Programms aus. Außerdem stellen viele Leute im Internet Klassen zur Wiederverwendung bereit. Während Sie die hier genannte Schwierigkeit in Ihrem eigenen Programm gerade noch bewältigen könnten, würde angesichts der Beiträge durch Millionen von Benutzern ein unsagbares Chaos entstehen. In künftigen Java-Versionen wird die Mehrfachvererbung eventuell implementiert, vorläufig genügen die Java-Fähigkeiten aber für 99% aller Programme.

F abstract-Klassen müssen nicht alle Methoden in einer Schnittstelle selbst implementieren. Müssen das aber alle Subklassen dieser Klassen?

A Eigentlich nicht. Aufgrund von Vererbung lautet die Regel, daß eine Implementierung von einer Klasse jeder Methode bereitgestellt werden muß, daß das aber nicht Ihre Klasse sein muß. Das entspricht dem Fall, in dem die Subklasse einer Klasse eine Schnittstelle für Sie implementiert. Alles, was die abstract-Klasse nicht implementiert, muß die erste nachfolgende nicht abstrakte Klasse erledigen. Dann brauchen alle nachfolgenden Subklassen nichts mehr dazutun.

F Sie haben nichts über Callbacks gesagt. Sind sie im Zusammenhang mit Schnittstellen nicht wichtig?

A Ja, aber ich habe sie absichtlich noch nicht erwähnt, weil das die Beispiele in dieser Lektion unnötig aufgebläht hätte. Callbacks werden oft in Benutzeroberflächen (z.B. Fenstern) benutzt, um zu bestimmen, welche Methoden als Reaktion auf Benutzeraktionen (Mausklicks, Tastatureingaben) gesendet werden. Da die Benutzerschnittstellenklassen nichts über die Klassen, die sie benutzen, »wissen« sollten, ist es in diesem Fall wichtig, Methoden getrennt vom Klassenbaum zu definieren. Callbacks sind nicht so allgemein wie etwa die perform-Methode in Smalltalk, weil ein bestimmtes Objekt verlangen kann, daß es von einem Objekt der Benutzeroberfläche allein durch Verwendung eines Methodennamens »zurückgerufen« wird. Nimmt man an, daß das Objekt von zwei Objekten der Benutzeroberfläche der gleichen Klasse zurückgerufen werden will, müßten zwei verschiedene Namen verwendet werden. Das ist in Java aber nicht möglich. Das Objekt wäre gezwungen, einen speziellen Zustand zu benutzen und getrennt zu testen. Das bedeutet, daß Schnittstellen in diesem Fall zwar relativ nützlich, aber sicherlich nicht die ideale Callback-Einrichtung sind.


(c) 1997 SAMS
Ein Imprint des Markt&Technik Buch- und Software- Verlag GmbH
Elektronische Fassung des Titels: Java in 21 Tagen, ISBN: 3-8272-2009-2

Previous Page Page Top TOC Index Next Page See Page