MarktundTechnik Home-Page Previous Page TOC Index Next Page See Page



16. Tag:

Pakete und Schnittstellen

von Charles L. Perkins

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

  1. Wie kann ich sie optimal anwenden, um die Methoden und Klassen meines Java-Programms besser zu organisieren?
  2. Wie kann ich sie beim Schreiben des Java-Codes in meinen Methoden nutzen?

    Den ersten Aspekt nennt man auch Programmierung im Großen, den zweiten Programmierung im Kleinen. Bill Joy, Mitbegründer von Sun Microsystems, sagt, daß sich Java beim Programmieren im Großen wie C und im Kleinen wie Smalltalk verhält. Er meint damit, daß Java beim Codieren vertraut und leistungsstark wie eine C-artige Sprache ist, beim Design aber die Erweiterbarkeit und Ausdrucksfähigkeit einer reinen 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 beschrieben: Wenn Sie ein Java-Programm entwickeln, legen Sie zuerst die Klassen aus und bestimmen die Beziehungen zwischen diesen Klassen, dann implementieren Sie den Java-Code nach Bedarf für jede ausgelegte Methode. 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 Designs 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 sowie der Verwendung von Paketen und Schnittstellen. Wir beginnen mit Paketen.

    Pakete

    Pakete sind Javas spezielle Art des Designs und der Organisation im Großen. Sie werden zum Kategorisieren und Gruppieren von Klassen verwendet. Wir untersuchen nun, warum Sie Pakete verwenden müssen oder sollen.

    Programmieren im Großen

    Wenn Sie mit der Entwicklung eines Java-Programms beginnen, das zahlreiche Klassen nutzt, entdecken Sie bald einige Einschränkungen des bisher präsentierten Java-Modells.

    Mit zunehmender Anzahl der Klassen in Ihrem Java-Programm steigt die Wahrscheinlichkeit, daß Sie einige davon wiederverwenden möchten. Verwenden Sie Klassen aus einem früheren Java-Programm, das Sie oder andere entwickelt haben (z. B. die Klassen in der Java-Bibliothek), denken Sie eventuell nicht daran oder wissen Sie nicht, daß die Klassennamen in Konflikt stehen. Hier kommt die Möglichkeit des Verbergens einer Klasse in einem Paket als handliche Lösung zuhilfe.

    Mit dem folgenden einfachen Code wird ein Paket in einer Java-Quelldatei erstellt:

    package myFirstPackage;
    
    public class MyPublicClass extends ItsSuperclass {
    
       ...
    
    }

    Eine package-Anweisung muß in einer Java-Quelldatei immer ganz oben stehen (selbstverständlich abgesehen von Kommentaren und leeren Stellen).

    Zuerst wird mit einer package-Anweisung der Name des Pakets deklariert. Dann definieren Sie auf die übliche Weise eine Klasse. Diese und alle anderen im gleichen Paket befindlichen Klassen werden gruppiert. (Die anderen Klassen befinden sich normalerweise in unterschiedlichen Quelldateien.)

    Pakete können innerhalb einer Hierarchie in etwa so organisiert werden wie die Vererbungshierarchie, wobei jede Ebene normalerweise eine kleinere spezifischere Klassengruppierung darstellt. Die Java-Klassenbibliothek ist selbst auf diese Weise organisiert (siehe die Diagramme in Anhang B). Die oberste Ebene heißt java. Die nächste Ebene beinhaltet Namen wie io, net, util und awt. Letztere hat eine noch niedrigere Ebene, auf der sich das Paket image befindet. Auf die ColorModel-Klasse, die im Paket image steht, kann eindeutig irgendwo im Java-Code unter java.awt.image.ColorModel Bezug genommen werden.

    Nach der geltenden Konvention spezifiziert die erste Ebene der Hierarchie den (global eindeutigen) Namen der Firma, die das bzw. die Java-Pakete entwickelt hat. Die Klassen von Sun Microsystem beispielsweise, die nicht Teil der Java-Standardumgebung sind, beginnen alle mit dem Präfix sun. Das Standardpaket java bildet eine Ausnahme zu dieser Regel, weil es eine Basis ist und irgendwann einmal vielleicht von verschiedenen Firmen implementiert wird.

    Im Beta-Release hat Sun erstmals eine formellere Prozedur für die Paketbenennung spezifiziert, die künftig eingehalten werden soll. Der Platzhalter für den Namen des Pakets auf der obersten Ebene ist jetzt für die Verwendung aller großgeschriebenen Abkürzungen, die für die obersten Internet-Domains benutzt werden (EDU, COM, GOV, FR, US usw.), reserviert. Diese reservierten Namen bilden den ersten Teil aller neuen Paketnamen, denen eine umgekehrte Version des Domainnamens als Präfix vorangestellt wird. Nach dieser Prozedur würden die sun-Pakete COM.sun heißen. Ist das Domain Ihrer Firma oder Universität weiter unten im Domainbaum, können Sie nach Herzenslust weiter umkehren, z. B. EDU.harvard.cs.projects.ai.learning.myPackage. Da Domainnamen bereits garantiert eindeutig und global sind, löst das ein kniffliges Problem auf angenehme Weise. Als zusätzlicher Bonus werden die Applets und Pakete der potentiell Millionen von Java-Programmierern in einer wachsenden Hierarchie unterhalb Ihres Klassenverzeichnisses gespeichert. Damit haben Sie eine Möglichkeit, sie alle übersichtlich zu finden und zu kategorisieren.

    Da sich jede Java-Klasse in einer eigenen Quelldatei befinden sollte, bietet die Gruppierung von Klassen in einer aus Paketen bestehenden Hierarchie eine ähnliche Organisationsweise wie Verzeichnisse in einem Dateisystem. Der Java-Compiler verstärkt diese Analogie noch dadurch, daß er von Ihnen verlangt, unter Ihrem Klassenverzeichnis eine Verzeichnishierarchie zu erstellen, die genau Ihrer Pakethierarchie entspricht, und Klassen in Verzeichnissen mit dem gleichen Namen (und auf der gleichen Ebene) abzulegen, in denen sich auch die Pakete befinden, in denen sie definiert sind.

    Die Verzeichnishierarchie der Java-Klassenbibliothek spiegelt ihre Pakethierarchie wider. Unter Unix wird die Klasse java.awt.image.ColorModel beispielsweise in einer Datei namens ColorModel.class im Verzeichnis .../classes/java/awt/image gespeichert (»...« bezeichnet den Pfad, auf dem Java installiert wurde). Wird ein Paket in myFirstPackage namens mySecondPackage durch Deklaration einer Klasse erstellt, z. B.:

    package myFirstPackage.mySecondPackage;
    
    public class AnotherPublicClass extends AnotherSuperclass {
    
       ...
    
    }

    muß sich die Java-Quelldatei (namens AnotherPublicClass.java) in einem Verzeichnis unter dem aktuellen Verzeichnis classes/myFirstPackage/mySecondPackage befinden, damit sie der Compiler (javac) finden kann. Erzeugt der Compiler die Datei AnotherPublicClass.class, stellt er sie in das gleiche Verzeichnis, damit sie der Java-Interpreter finden kann. Sowohl der Compiler als auch der Interpreter erwartet (und verlangt) diese Hierarchie.

    Das bedeutet auch, daß die Quelldatei (im heutigen Beispiel) mit APublicClass.java benannt werden und im Verzeichnis namens classes/myFirstPackage gespeichert werden muß. Was passiert, wenn Klassen ohne package-Anweisung definiert werden, wie in früheren Beispielen in diesem Buch? Der Compiler stellt solche Klassen in ein unbenanntes Standardpaket. Die .java- und .class-Dateien dieses Pakets befinden sich im aktuellen Verzeichnis oder im darunterliegenden Klassenverzeichnis. Genauer gesagt, bedeutet die in diesem Abschnitt benutzte Redewendung »im aktuellen Verzeichnis« eigentlich »jedes im Klassenpfad aufgelistete Verzeichnis«. Der Compiler und der Interpreter durchsuchen diese Liste nach den jeweils angeforderten Klassen. Sie können einen Klassenpfad auf der Befehlszeile angeben, wenn Sie javac oder java ausführen. Eine ständige Alternative dazu ist das Ändern einer speziellen Umgebungsvariablen namens CLASSPATH. (Weitere Einzelheiten hierzu finden Sie in der Dokumentation Ihrer Java-Version.)

    Programmieren im Kleinen

    Wenn Sie in Ihrem Java-Code auf eine Klasse mit Namen verweisen, verwenden Sie ein Paket. Die meiste Zeit ist man sich dessen nicht bewußt, weil viele der häufig im System benutzten Klassen im Paket java.lang sind, das der Java-Compiler automatisch importiert. Wenn Sie beispielsweise

    String aString;

    sehen, läuft mehr ab, als Sie glauben. Was nun, wenn Sie auf die Klasse verweisen wollen, die Sie am Anfang dieses Abschnitts erstellt haben, diejenige im Paket myFirstPackage? Wenn Sie es mit

    MyPublicClass someName;

    versuchen, meckert der Compiler. Die Klasse MyPublicClass ist im Paket java.lang nicht definiert. Um dieses Problem zu lösen, können Sie in Java jedem Klassennamen ein Präfix voranstellen, das den Namen des Pakets bezeichnet:

    myFirstPackage.MyPublicClass someName;

    Sie erinnern sich, daß Paketnamen laut Konvention mit einem Kleinbuchstaben beginnen müssen, um sie von Klassennamen zu unterscheiden.

    Nehmen wir beispielsweise an, Sie wollen viele Klassen aus einem Paket, ein Paket mit einem langen Namen oder beides verwenden. Um Referenzen auf Klassen wie that.really.long.package.name.ClassName zu vermeiden, ermöglicht Ihnen Java das »Importieren« der Namen solcher Klassen in Ihr Programm. Sie verhalten sich wie java.lang-Klassen und benötigen kein Präfix. Auf diese Weise können Sie z. B. den obigen langen Klassennamen so schreiben:

    import that.really.long.package.name.ClassName;
    
    ClassName anObject;
    
    // Und Sie können ClassName direkt so oft wie nötig verwenden

    Alle import-Anweisungen müssen nach package-Anweisungen, jedoch vor Klassendefinitionen erscheinen. Das bedeutet, daß sie alle am Anfang der Quelldatei stehen.

    Wenn Sie mehrere Klassen aus dem gleichen Paket benötigen, könnten Sie so vorgehen:

    that.really.long.package.name.ClassOne first;
    
    that.really.long.package.name.ClassTwo second;
    
    that.really.long.package.name.ClassThree andSoOn;

    Das ist aber nur etwas für unermüdliche Programmierer. Die pfiffigen unter ihnen erreichen den gleichen Zweck durch Importieren eines ganzen Pakets mit public-Klassen:

    import that.really.long.package.name.*;
    
    ClassOne   first;
    
    ClassTwo   second;
    
    ClassThree   andSoOn;

    Das Sternchen (*) in diesem Beispiel ist nicht genau das, was Sie in einem Befehlsprompt verwenden würden, um den Inhalt eines Verzeichnisses anzugeben. Fordern Sie z. B. die Liste des Verzeichnisses classes/java/awt/* an, erscheinen in dieser Liste alle .class-Dateien und Unterverzeichnisse, wie image und peer. Mit import java.awt.* werden keine untergeordneten Pakete wie image und peer importiert. Um alle Klassen in einer komplexen Pakethierarchie zu importieren, müssen Sie explizit auf jeder Hierarchieebene import schreiben.

    Falls Sie planen, eine Klasse oder ein Paket nur ein paarmal in Ihrer Quelldatei zu verwenden, lohnt sich der Import nicht. Zu bedenken bleibt auch, daß der Leser anhand des Paketnamens erkennen kann, wo er weitere Informationen über die Klasse findet.

    Betrachten Sie einmal folgende Einträge der A-Klasse einer Quelldatei:

    package packageA;
    
    public class ClassName {
    
       ...
    
    }
    
    public class ClassA {
    
       ...
    
    }

    und der B-Klasse der gleichen Quelldatei:

    package packageB;
    
    public class ClassName {
    
       ...
    
    }
    
    public class ClassB {
    
       ...
    
    }

    Dann steht an irgendeiner anderen Stelle folgendes:

    import packageA;
    
    import packageB;
    
    ClassName anObject;   // Welcher ClassName ist gemeint?

    Hier sind hinsichtlich der Klasse zwei Interpretationen möglich: packageA oder packageB. Da dies zweideutig ist, weiß der Compiler nichts damit anzufangen. Er erzeugt selbstverständlich einen Fehler. Das heißt, man muß seine Absichten klar formulieren. Hier ein Beispiel:

    import packageA.*;
    
    import packageB.*;
    
    packageA.ClassName anObject; //Jetzt richtig
    
    packageB.ClassName another Object; // Auch richtig
    
    ClassA anAObject; // War nie ein Problem
    
    Class B aBObject; // Ebenfalls kein Problem

    Sie wundern sich vielleicht über die zahlreichen Deklarationen, die in der heutigen Lektion als Beispiele verwendet werden. Deklarationen eignen sich gut als Beispiele, weil sie die einfachste Möglichkeit sind, auf einen Klassennamen zu verweisen. Jede Verwendung eines Klassennamens (z. B. in der extends-Klausel oder in new ClassName()) unterliegt diesen Regeln.

    Verbergen von Klassen

    Scharfsinnige Leser haben eventuell bemerkt, daß über das Importieren mit einem Sternchen (*) behauptet wurde, man könne damit ein ganzes Paket von public-Klassen importieren. Warum sollte man Klassen einer anderen Art brauchen? Sehen Sie sich einmal folgendes Beispiel an:

    package collections;
    
    public class LinkedList {
    
       private Node root;
    
       public void add(Object o) {
    
          root = new Node(o, root);
    
       }
    
       ...
    
    }
    
    class Node {   // Nicht public
    
       private Object contents;
    
       private Node next;
    
       Node(Object o, Node n) {
    
          contents = o;
    
          next = n;
    
       }
    
       ...
    
    }

    Wäre dieser gesamte Code in einer Datei, würde eine Konvention des Compilers verletzt werden: in einer Java-Quelldatei darf sich nur jeweils eine Klasse befinden. Für den Compiler ist aber nur entscheidend, daß sich jede public-Klasse in einer separaten Datei befindet. Andererseits ist es eine gute Programmierpraxis, Klassen in separaten Dateien zu verwalten.

    Mit der LinkedList-Klasse soll eine Reihe nützlicher public-Methoden (z. B. add()) für andere Klassen bereitgestellt werden. LinkedList deklariert keinen Schutzmodifier, was dem Ausdruck package entspricht. Die Klasse kann nur von anderen Klassen des gleichen Pakets benutzt werden. In diesem Fall ist das das collections-Paket. Sie können LinkedList wie folgt verwenden:

    import collections.*;   // Importiert nur public-Klassen
    
    LinkedList aLinkedList;
    
    /* Node n; */   // Das erzeugt einen Kompilierfehler
    
    aLinkedList.add(new Integer(1138));
    
    aLinkedList.add("THX-");
    
    ...

    Sie können in diesem Beispiel LinkedList auch als collections.LinkedList importieren oder deklarieren. Da LinkedList auf Node verweist, wird diese Klasse automatisch geladen und benutzt. Der Compiler prüft, ob LinkedList (als Teil des Paketes collections) berechtigt ist, die Node-Klasse zu benutzen. Dieses Recht besteht in diesem Beispiel - und allgemein - nicht.

    Einer der großen Vorteile des Verbergens von Klassen ist, daß die gesamte Komplexität beim Importieren der Klasse verborgen wird. Somit besteht das Erstellen eines guten Pakets aus der Definition einer kleinen übersichtlichen Menge von public-Klassen und -Methoden zur Verwendung durch andere Klassen und das Implementieren derselben durch Verwendung einer Reihe von verborgenen (package) Klassen. Später in der heutigen Lektion arbeiten wir nochmal mit verborgenen Klassen.

    Schnittstellen

    Wie die abstrakten Klassen und Methoden, die Sie gestern gelernt haben, bieten Schnittstellen Masken mit Eigenschaften, die andere Klassen implementieren sollen, sind jedoch viel leistungsstärker.

    Programmieren im Großen

    Wenn man erstmals mit dem Design objektorientierter Programme beginnt, erscheint einem die Klassenhierarchie fast wundersam. Innerhalb dieses einzelnen Baumes können eine Hierarchie numerischer Typen, zahlreiche einfache bis komplexe Beziehungen zwischen Objekten und Prozessen und viele Punkte entlang der Achse von abstrakt/ allgemein bis konkret/spezifisch ausgedrückt werden. Nach längeren Überlegungen und mehr praktischen Erfahrungen wirkt dieser wundersame Baum bald einschränkend, zuweilen wie eine Zwangsjacke.

    Manche Sprachen greifen dieses Problem dadurch auf, daß sie mehr Laufzeitleistung einführen, z. B. den Codeblock und perform in Smalltalk. Andere stellen komplexere Vererbungshierarchien bereit, z. B. die Mehrfachvererbung. Beide Lösungen sind nicht ideal. Bei der ersten ist die Implementierung von Sicherheit schwieriger und die Sprache ist nicht so leicht erklärbar und erlernbar. Bei der zweiten Lösung führt die Komplexität zu Verwirrung und fehleranfälligen Zweideutigkeiten. In Java wurde keine dieser Lösungen umgesetzt. Vielmehr wurde ganz im Geist der C-Protokolle eine separate Hierarchie realisiert, um die Zwangsjacke zu lockern.

    Diese neue Hierarchie ist eine Hierarchie von Schnittstellen. Schnittstellen sind nicht auf eine Superklasse begrenzt, deshalb ermöglichen Sie eine Art der Mehrfachvererbung. Sie geben aber nur Methodenbeschreibungen an ihre Kinder weiter, keine Implementierungen und keine Instanzvariablen von Methoden, so daß viele Komplexitäten einer vollen Mehrfachvererbung vermieden werden.

    Wie Klassen werden auch Schnittstellen in Quelldateien deklariert, und zwar eine Schnittstelle pro Datei. Wie Klassen werden sie ebenfalls in .class-Dateien kompiliert. Fast überall, wo in diesem Buch ein Klassenname auftaucht, können Sie diesen durch einen Schnittstellennamen ersetzen. Java-Programmierer sprechen meist von »Klasse«, wenn sie »Klasse« oder »Schnittstelle« meinen. Schnittstellen runden die Leistung von Klassen ab, und beide können fast gleich behandelt werden. Einer der wenigen Unterschiede zwischen Klassen und Schnittstellen ist, daß von einer Schnittstelle keine Instanz erstellt werden kann: new kann nur eine Instanz einer Klasse erstellen. Hier die Deklaration einer Schnittstelle:

    package myFirstPackage;
    
    public interface MyFirstInterface extends Interface1, Interface2, ... {
    
       ...
    
       // Alle hier befindlichen Methoden sind public und abstract
    
       // Alle Variablen sind public, static und final
    
    }

    Dieses Beispiel ist eine umgeschriebene Version des ersten Beispiels der heutigen Lektion. Der Code enthält jetzt eine neue public-Schnittstelle zum Paket myFirstPackage anstelle einer neuen public-Klasse. Beachten Sie, daß mehrere Eltern in der extends-Klausel einer Schnittstelle aufgelistet werden können.

    Ist keine extends-Klausel vorhanden, erben Schnittstellen nicht standardmäßig von Object, weil Object eine Klasse ist. Schnittstellen haben keine »oberste« Schnittstelle, von der alle automatisch abstammen.

    Eventuell in einer public-Schnittstelle definierte Variablen oder Methoden erhalten implizit ein Präfix durch die in den Kommentaren erwähnten Modifier. Genau diese Modifier können (wahlweise) erscheinen, nicht aber andere:

    public interface MySecondInterface {
    
       public static final int theAnswer = 42; // Beide Zeilen sind OK
    
       public abstract int lifeTheUniverseAndEverything();
    
       long bingBangCounter = 0; // OK, wird public, static und final
    
       long ageOfTheUniverse();   // OK, wird public und abstract
    
       protected int aConstant; // Nicht in Ordnung
    
       private int getAnInt();   // Nicht in Ordnung
    
    }

    Eine nicht public deklarierte Schnittstelle (d. h. package) erhält keinen public-Modifier als Präfix. Wenn Sie innerhalb einer solchen Schnittstelle public deklarieren, legen Sie wirklich public fest, geben also keine redundante Anweisung. Allerdings ist es selten, daß eine Schnittstelle nur von den Klassen eines Pakets benutzt wird und nicht auch von den Klassen, die dieses Paket ebenfalls nutzen.

    Trennung von Design und Implementierung

    Einer der stärksten Aspekte von Schnittstellen ist, daß sie Java um die Fähigkeit erweitern, die Vererbung von Design und Implementierung zu trennen. In einem Vererbungsbaum mit einer einzigen Klasse sind diese zwei Bereiche unentwirrbar miteinander verflochten. Manchmal muß man aber auch in der Lage sein, eine Schnittstelle zu einer Klasse mit Objekten abstrakt zu beschreiben, ohne sie unmittelbar in eine Implementierung einzubinden. Sie könnten beispielsweise eine abstract-Klasse wie gestern erstellen. Damit eine neue Klasse diese Art von »Schnittstelle« verwenden kann, muß sie eine Subklasse der abstract-Klasse werden und deren Position im Baum akzeptieren. Was nun, wenn die neue Klasse aus Implementierungsgründen auch eine Subklasse einer anderen Klasse im Baum werden muß? Soll sie zwei solche »Schnittstellen« gleichzeitig nutzen? Sehen Sie sich einmal diesen Code an:

    class FirstImplementor extends SomeClass implements MySecondInterface {
    
       ...
    
    }
    
    class SecondImplementor implements MyFirstInterface, MySecondInterface {
    
       ...
    
    }

    Die erste Klasse oben »steckt« in dem einzelnen Vererbungsbaum direkt unter der Klasse SomeClass, ist aber frei, auch eine Schnittstelle zu implementieren. Die zweite Klasse steckt unter Object, implementiert aber zwei Schnittstellen (sie könnte mehrere implementieren). Implementierung einer Schnittstelle bedeutet, daß mit Sicherheit alle darin spezifizierten Methoden implementiert werden.

    Obwohl es einer abstract-Klasse gestattet ist, diese strikte Anforderung zu ignorieren und die Methoden nur teilweise (oder überhaupt nicht) zu implementieren, müssen alle ihre nicht abstrakten Subklassen gehorchen.

    Da Schnittstellen in einer separaten Hierarchie stehen, können Sie in einem Vererbungsbaum mit Klassen gemischt werden, so daß der Designer eine Schnittstelle an jeder beliebigen Stelle im Baum, wo sie gebraucht wird, einfügen kann. Der Klassenbaum mit Einzelvererbung kann somit als reine Implementierungshierarchie betrachtet werden. Die Designhierarchie (mit vorwiegend abstract-Methoden) ist im Schnittstellenbaum mit Mehrfachvererbung enthalten. Das ist ein leistungsstarkes Konzept für die Organisation von Programmen, das zwar gewöhnungsbedürftig, aber sehr nützlich ist.

    Wir sehen uns nun ein einfaches Beispiel dieser Trennung anhand einer neuen Klasse namens Orange an. Wir gehen davon aus, daß Sie bereits eine Implementierung der Klasse Fruit und der Schnittstelle Fruitlike, die das darstellt, was man mit Früchten erwartungsgemäß anstellen kann, vorliegen haben. Sie möchten eine Orange als Frucht definieren, wollen Sie aber auch als rundes Objekt betrachten, das gerollt, geworfen und anderweitig manipuliert werden kann. Das alles kann wie folgt ausgedrückt werden:

    interface Fruitlike extends Foodlike {
    
       void decay();
    
       void squish();
    
       ...
    
    }
    
    class Fruit extends Food implements Fruitlike {
    
       private Color myColor;
    
       private int daysTillRot;
    
       ...
    
    }
    
    interface Spherelike {
    
       void toss();
    
       void rotate();
    
       ...
    
    }
    
    class Orange extends Fruit implements Spherelike {
    
       ... // Werfen (toss()) kann mich zermatschen (squish()), aber nur mich
    
    }

    Wir verwenden dieses Beispiel noch einmal in der heutigen Lektion. Vorläufig wäre zu beachten, daß die Klasse Orange nicht extra implements Fruitlike sagen muß, weil das durch extends Fruit schon geschehen ist.

    Der umgekehrte Fall trifft aber nicht zu. Die Implementierung einer Schnittstelle sagt nichts über die Implementierungshierarchie einer Klasse aus. Übrigens: Falls Sie Klassen auf traditionelle Weise auslegen (was nicht unbedingt besser sein muß), wäre die Klasse Fruit die Schnittstellenbeschreibung und die Implementierung.

    Der Vorteil dieser Struktur ist, daß Sie jederzeit Ihre Meinung darüber ändern können, welche Klasse mit extends gemeint ist (wenn beispielsweise eine Sphere-Klasse implementiert wird). Trotzdem versteht die Klasse Orange die gleichen zwei Schnittstellen:

    class Sphere implements Spherelike { // Erweitert Object
    
       private float radius;
    
       ...
    
    }
    
    class Orange extends Sphere implements Fruitlike {
    
       ... // Benutzer von Orange müssen über die Änderung nichts erfahren!
    
    }

    Durch die Möglichkeit, Schnittstellen einzumischen, können mehrere Klassen über den Einfachvererbungsbaum verteilt sein und dennoch die gleichen Methoden (oder gar nur eine) implementieren. Diese Klassen haben zwar eine gemeinsame Superklasse (im schlimmsten Fall Object), jedoch können sich unterhalb dieser gemeinsamen Eltern viele Subklassen befinden, die an diesen Methoden überhaupt nicht interessiert sind. Das Hinzufügen von Methoden zur Elternklasse oder gar das Erstellen einer neuen abstract-Klasse und das Einfügen derselben in die Hierarchie oberhalb der Eltern ist keine gute Lösung.

    Statt dessen verwenden Sie eine Schnittstelle, um die Methode(n) zu spezifizieren. Sie kann durch jede Klasse mit gemeinsamen Bedürfnissen implementiert werden, ohne die anderen Klassen zu zwingen, diese im Einfachvererbungsbaum zu »verstehen«. Das bedeutet, daß Design nur im Bedarfsfall angewandt wird. Die Benutzer der Schnittstelle können dann Variablen und Argumente eines neuen Schnittstellentyps definieren, der auf eine der Klassen, die die Schnittstelle implementieren, verweist (wie Sie unten noch sehen werden). Zu diesen Einmischbeispielen zählen die Methoden read() und write() für Objekte, die etwas produzieren oder verbrauchen, und die Bereitstellung allgemein nützlicher Konstanten. Letzteres könnte so aussehen:

    public interface PresumablyUsefulConstants {
    
       public static final int oneOfThem = 1234;
    
       public static final float another = 1.234F;
    
       public static final String yetAnother = "!1234";
    
       ...
    
    }
    
    public class AnyClass implements PresumablyUsefulConstants {
    
       public static void main(String argV[]) {
    
          double calculation = oneOfThem * another;
    
          System.out.println("hello " + yetAnother + calculation);
    
          ...
    
       }
    
    }

    Dieser Code gibt die absolut nichtssagende Zeichenkette hello 12341522.756 aus, zeigt aber auf, daß die Klasse AnyClass direkt auf alle in der Schnittstelle PresumablyUsefulConstants definierten Variablen Bezug nehmen kann. Normalerweise wird auf solche Variablen und Konstanten über die Klasse verwiesen, wie bei der Konstanten Integer.min_value, die von der Integer-Klasse bereitgestellt wird. Wird eine größere Gruppe von Konstanten häufiger benutzt, lohnt es sich, sie in eine Schnittstelle zu stellen und global zu implementieren.

    Programmieren im Kleinen

    Wie verwenden wir diese Schnittstellen? Fast überall, wo Klassen benutzt werden können, sind statt dessen Schnittstellen verwendbar. Wir versuchen nun, die zuvor definierte Schnittstelle MySecondInterface zu benutzen:

    MySecondInterface anObject = getTheRightObjectSomehow();
    
    long age = anObject.ageOfTheUniverse();

    Nachdem wir anObject als Typ MySecondInterface deklariert haben, können wir anObject als Empfänger jeder Meldung, die die Schnittstelle definiert (oder erbt), benutzen. Was bedeutet die vorherige Deklaration eigentlich?

    Wenn eine Variable als Schnittstellentyp deklariert wird, bedeutet das einfach, daß jedes Objekt, auf das sich die Variable bezieht, diese Schnittstelle implementiert, d. h. sie soll erwartungsgemäß alle Methoden verstehen, die diese Schnittstelle spezifiziert. Das ist zwar relativ abstrakt, ermöglicht aber beispielsweise, daß der Code geschrieben werden kann, bevor Klassen erstellt und implementiert werden. In der traditionellen objektorientierten Programmierung ist man gezwungen, eine Klasse mit »Stub«-Implementierungen zu erstellen, um die gleiche Wirkung zu erzielen.

    Hier ein komplexeres Beispiel:

    Orange anOrange = getAnOrange();
    
    Fruit aFruit = (Fruit) getAnOrange();
    
    Fruitlike aFruitlike = (Fruitlike) getAnOrange();
    
    Spherelike aSpherelike = (Spherelike) getAnOrange();
    
    aFruit.decay();      // Früchte verfaulen
    
    aFruitlike.squish();   // und zermatschen
    
    aFruitlike.toss();   // Nicht OK
    
    aSpherelike.toss();   // OK
    
    anOrange.decay();   // Orangen können alles machen
    
    anOrange.squish();
    
    anOrange.toss();
    
    anOrange.rotate();

    In diesem Beispiel wird eine Orange durch Deklarationen auf die Fähigkeiten einer Frucht oder Kugel eingeschränkt, um die Flexibilität der zuvor entwickelten Struktur aufzuzeigen. Hätten wir statt dessen die zweite Struktur (die mit der neuen Sphere-Klasse) benutzt, würde der Großteil dieses Codes immer noch funktionieren. (In der Zeile mit Fruit müssen alle Instanzen von Fruit durch Sphere ersetzt werden. aFruit.deday() könnte beispielsweise durch aSphere.rotate() ersetzt werden. Alles andere kann bleiben.)

    Die direkte Verwendung von Klassennamen dient lediglich zu Demonstrationszwecken. Normalerweise würde man nur Schnittstellennamen in diesen Deklarationen verwenden, so daß kein Codeteil in diesem Beispiel geändert werden müßte, um die neue Struktur zu unterstützen.

    Schnittstellen werden in der gesamten Java-Klassenbibliothek implementiert und benutzt, wenn ein Verhalten durch mehrere verteilte Klassen implementiert werden soll. In Anhang B stehen unter anderem die Schnittstellen java.lang.Runnable, java.util.Enumeration, java.util.Observable, java.awt.image.ImageConsumer und java.awt.image.ImageProducer. Wir wollen nun eine dieser Schnittstellen - Enumeration - benutzen, das LinkedList-Beispiel wieder aufgreifen und im Zusammenhang mit der heutigen Lektion die Verwendung von Paketen und Schnittstellen aufzeigen:

    package collections;
    
    public class LinkedList {
    
       private Node root;
    
       ...
    
       public Enumeration enumerate() {
    
          return new LinkedListEnumerator(root);
    
       }
    
    }
    
    class Node {
    
       private Object contents;
    
       private Node next;
    
       ...
    
       public Object contents() {
    
          return contents;
    
       }
    
       public Node next() {
    
          return next;
    
       }
    
    }
    
    class LinkedListEnumerator implements Enumeration {
    
       private Node currentNode;
    
       LinkedListEnumerator(Node root) {
    
          currentNode = root;
    
       }
    
       public boolean hasMoreElements() {
    
          return currentNode != null;
    
       }
    
       public Object nextElement() {
    
          Object anObject = currentNode.contents();
    
          currentNode = currentNode.next();
    
          return anObject;
    
       }
    
    }

    Nachfolgend eine typische Verwendung von Enumeration:

    collections.LinkedList aLinkedList = createLinkedList();
    
    java.util.Enumeration e = aLinkedList.enumerate();
    
    while (e.hasMoreElements()) {
    
       Object anObject = e.nextElement();
    
       // Etwas Nützliches mit anObject machen
    
    }

    Beachten Sie, daß wir Enumeration e so benutzen, als wüßten wir, was das ist, wissen es aber nicht. Es handelt sich um eine Instanz einer verborgenen Klasse (LinkedListEnumerator), die man nicht direkt sehen oder benutzen kann. Durch eine Kombination von Paketen und Schnittstellen gelingt es der LinkedList-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 anwendbar ist.

    Zusammenfassung

    Heute haben Sie gelernt, wie Pakete - Ihre eigenen und die aus der Java-Klassenbibliothek- benutzt werden können, um Klassen in aussagefähige Gruppen zusammenzufassen und zu kategorisieren. 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 gelernt, wie man Schnittstellen deklariert und benutzt. Das ist ein starker Mechanismus zur Erweiterung der traditionellen Einfachvererbung von Java-Klassen und zum Trennen des Designs von der 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. Enumeration, die einfach erscheinen, dennoch aber den Großteil ihrer Implementierung vor den Benutzern verbergen. Das ist eine leistungsstarke Technik.


    Fragen und Antworten

    F: Was passiert mit den Paket-/Verzeichnishierarchien, wenn Java um die eine oder andere Art von Archivierung erweitert wird?

    A: Die Fähigkeit, über das Internet ein ganzes Archiv mit Paketen, Klassen und Ressourcen herunterzuladen, dürfte im Java-System bald realisiert werden. Wenn das passiert, bricht die einfache Abbildung zwischen der Verzeichnis- und Pakethierarchie zusammen. Wir werden also nicht mehr in der Lage sein, mühelos festzustellen, wo die einzelnen Klassen gespeichert sind (d. h. in welchem Archiv). Dieses neue fortschrittlichere Java-System soll aber Werkzeuge bieten, die diese Aufgabe (sowie das Kompilieren und Verknüpfen von Programmen im allgemeinen) vereinfachen.

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

    A: Nein, das import-Sternchen (*) verhält sich nicht wie ein Sternchen in der Befehlszeile.

    F: Was bedeutet dann eigentlich import mit einem Sternchen?

    A: Damit werden alle public-Klassen importiert, die sich direkt in dem benannten Paket befinden, nicht in einem seiner 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: Besteht eine Möglichkeit, verborgene package) Klassen irgendwie aus ihrer Verborgenheit herauszuholen?

    A: Ein seltsamer Fall, bei dem eine verborgene Klasse in die Sichtbarkeit gezwungen wird, tritt ein, wenn die Klasse eine public-Superklasse hat und jemand eine Instanz auf sie oder die Superklasse abbildet. Dadurch können public-Methoden dieser Superklasse über Ihre verborgene Klasseninstanz aufgerufen werden, auch wenn Sie diese Methoden nicht als public beabsichtigt hatten. Normalerweise sind das die public-Methoden, die Ihre Instanzen getrost ausführen können, ansonsten hätten Sie sie nicht unter einer public-Superklasse deklariert. Das ist nicht immer der Fall. Viele der im System integrierten Klassen sind public, so daß man manchmal keine Wahl hat. Zum Glück ist dieser Fall selten.

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

    A: Der Grund ist nicht unbedingt die Komplexität, sondern der, daß die Sprache dadurch übermäßig kompliziert wird. Wie Sie am letzten Tag noch lernen werden, kann das verursachen, daß größere Systeme an Sicherheit einbüßen. Nehmen wir beispielsweise 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, »Super«-Methoden 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 Eltern 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 erwähnt. Sind sie im Zusammenhang mit Schnittstellen wichtig?

    A: Ja, aber ich habe sie absichtlich 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 (z. B. 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.


    Copyright ©1996 Markt&Technik
    Buch- und Software- Verlag GmbH
    Alle Rechte vorbehalten. All rights reserved.

    Schreiben Sie uns!

    Previous Page TOC Index Next Page See Page