vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 3

Tag 15


Pakete, Schnittstellen und mehr

Die dritte Woche dieses Kurses erweitert, was Sie bereits wissen. Sie könnten an dieser Stelle aufhören und sinnvolle Programme entwickeln. Allerdings würden Ihnen einige der fortgeschritteneren Features, die die Stärke der Sprache ausmachen, fehlen.

Heute werden Sie Ihr Wissen über Klassen, und wie diese mit anderen Klassen in einem Programm interagieren, ausbauen. Die folgenden Themen werden behandelt:

Modifier

Die Techniken, die Sie heute für die Programmierung lernen, schließen unterschiedliche Strategien und Denkansätze zur Organisation von Klassen ein. Eine Sache haben allerdings alle diese Techniken gemein: sie verwenden alle spezielle Schlüsselworte von Java - die Modifier.

In der ersten Woche haben Sie gelernt, wie Sie in Java Klassen, Methoden und Variablen definieren. Modifier sind Schlüsselworte, die Sie den Definitionen hinzufügen, um deren Bedeutung zu verändern.

Java bietet eine große Auswahl an Modifiern an, darunter:

Um einen Modifier zu verwenden, integrieren Sie das entsprechende Schlüsselwort in der Definition der Klasse, Methode oder Variablen, auf die Sie diesen anwenden wollen. Der Modifier geht dem Rest der Anweisung voraus, wie das in den folgenden Beispielen gezeigt ist:

public class MyApplet extends java.applet.Applet { ... }

private boolean killJabberwock;

static final double weeks = 9.5;

protected static final int MEANINGOFLIFE = 42;

public static void main(String arguments[]) { ...}

Wenn Sie mehr als einen Modifier in einer Anweisung verwenden, können Sie diese in beliebiger Reihenfolge angeben, solange alle Modifier vor dem Element stehen, auf das sie angewendet werden. Stellen Sie sicher, daß Sie den Rückgabetyp einer Methode - z.B. void - nicht wie einen der Modifier behandeln.

Modifier sind optional - was Sie daran merken sollten, daß wir in den letzten zwei Wochen nur sehr wenige davon verwendet haben. Es gibt aber, wie Sie sehen werden, viele gute Gründe, sie zu verwenden.

Zugriffskontrolle für Methoden und Variablen

Mit »Zugriffskontrolle« ist hier die Kontrolle der Sichtbarkeit gemeint. Ist eine Methode oder Variable für eine andere Klasse sichtbar, können ihre Methoden auf diese Methode oder Variable verweisen (sie aufrufen oder modifizieren). Um eine Methode oder Variable vor solchen Referenzen zu »schützen«, können Sie die vier in den nächsten Abschnitten beschriebenen Sichtbarkeitsebenen anwenden. Jede Ebene ist einschränkender als die vorherige und bietet damit mehr Schutz.

Vier Schutzebenen

Die vier Schutzebenen (public, package, protected und private) bezeichnen die grundlegenden Beziehungen, die eine Methode oder Variable einer Klasse mit den anderen Klassen im System haben kann.

Der Standardzugriff

In den meisten Beispielen diesen Buches haben Sie keine bestimmte Zugriffskontrolle angegeben. Variablen und Methoden wurden mit Anweisungen wie den folgenden deklariert:

String singer = "Phil Harris";
boolean digThatCrazyBeat() {
    return true;
}

Eine Variable oder Methode, die ohne einen Modifier für die Zugriffskontrolle deklariert wird, ist für jede Klasse innerhalb desselben Pakets verfügbar. Sie haben bereits erfahren, daß die Klassen in der Klassenbibilothek von Java in Paketen organisiert sind. Das Paket java.awt ist eines davon - es bietet eine Reihe von Klassen mit Verhaltensweisen für das Abstract Windowing Toolkit von Java.

Jede Variable, die ohne Modifier deklariert wurde, kann von anderen Klassen in demselben Paket gelesen bzw. verändert werden. Jede Methode, die auf diese Art deklariert wurde, kann von jeder anderen Klasse in demselben Paket aufgerufen werden. Keine andere Klasse außerhalb des Pakets kann allerdings auf diese Elemente zugreifen.

Diese Ebene der Zugriffskontrolle kontrolliert allerdings nur wenig beim Zugriff. Wenn Sie beginnen darüber nachzudenken, wie Ihre Klasse von anderen Klassen verwendet wird, werden Sie einen der drei Modifier öfter verwenden, als die standardmäßige Zugriffskontrolle zu akzeptieren.


Die vorherige Diskussion wirft die Frage auf, in welchem Paket sich Ihre eigenen Klassen, die Sie bis zu diesem Zeitpunkt erstellt haben, befanden. Wie Sie später am heutigen Tag sehen werden, können Sie Ihre Klassen zu einem Mitglied eines Pakets machen, indem Sie die Anweisung package verwenden. Wenn Sie diesen Ansatz nicht verwenden, dann wird Ihre Klasse in ein Paket gepackt, in dem sich alle anderen Klassen ohne explizite Paketzugehörigkeit befinden.

private

Die höchste Schutzebene ist das Gegenteil zu public. private-Methoden und -Variablen sind nur innerhalb der eigenen Klasse sichtbar:

public class APrivateClass {
private int aPrivateInt;
private String aPrivateString;
private float aPrivateMethod() {
...
}
}

Das mag zwar extrem einschränkend erscheinen, ist aber die vorwiegend angewandte Schutzebene. Private Daten, interne Zustände oder eindeutige Darstellungen in Ihrer Implementierung - kurz: alles, was die Subklassen nicht direkt mitnutzen sollen - sind private. Bedenken Sie, daß die primäre Aufgabe eines Objekts die Kapselung seiner Daten ist, sie also vor der Welt zu verbergen, damit sie nicht manipuliert werden können. Sie können trotzdem weniger einschränkende Methoden verwenden, jedoch ist ein straffer Zügel über Ihre internen Darstellungen wichtig, wie Sie noch sehen werden. Sie trennen dadurch das Design von der Implementierung, minimieren die Informationsmenge, die eine Klasse von einer anderen braucht, um ihre Aufgabe zu erfüllen, und reduzieren den Umfang der im Code erforderlichen Änderungen, falls Sie die Darstellung ändern.

public

Da jede Klasse eine Insel für sich ist, betrifft diese Beziehung die Unterscheidung zwischen dem internen und externen Bereich einer Klasse. Eine Methode oder Variable ist für die Klasse, in der sie definiert wurde, sichtbar. Was aber muß geschehen, wenn Sie sie für alle Klassen außerhalb dieser Klasse sichtbar machen wollen?

Die Antwort ist klar: Sie deklarieren die Methode oder Variable einfach als public. Fast jede in diesem Buch definierte Methode und Variable wurde der Einfachheit halber public deklariert. Wenn Sie mit Ihrem eigenen Code arbeiten, können Sie den Zugriff weiter einschränken. Nachfolgend einige Beispiele mit public-Deklarationen:

public class APublicClass {
public int aPublicInt;
public String aPublicString;
public float aPublicMethod() {
...
}
}

Eine Variable oder Methode mit public-Zugriff ist am stärksten sichtbar, d.h. sie kann von allen gesehen werden und alle können auf sie zugreifen. Selbstverständlich ist das nicht immer wünschenswert. In diesen Fällen greift dann eine der anderen Schutzebenen.

protected

Die dritte Beziehung betrifft eine Klasse und ihre gegenwärtigen und zukünftigen Subklassen. Auf Methoden und Variablen von protected-Klassen können nur Subklassen der Klassen zugreifen.

Subklassen stehen einer Superklasse aus folgenden Gründen viel näher als »fremde« Klassen (Klassen anderer Pakete):

Diese Ebene bietet mehr Schutz und grenzt den Zugriff noch weiter ein, erlaubt den Subklassen aber immer noch vollen Zugriff. Im folgenden ein paar Beispiele für Deklarationen mit protected:

public class AProtectedClass {
protected int aProtectedInt = 4;
protected String aProtectedString = "and a 3 and a ";
protected float aProtectedMethod() {
...
}
}
public class AProtectedClassSubclass extends AProtectedClass {
public void testUse() {
AProtectedClass aPC = new AProtectedClass();
System.out.println(aPC.aProtectedString + aPC.aProtectedInt);
aPC.aProtectedMethod(); // Alle hier sind A.O.K.(absolut OK)
}
}
public class AnyClassInTheSamePackage {
public void testUse() {
AProtectedClass aPC = new AProtectedClass();
System.out.println(aPC.aProtectedString + aPC.aProtectedInt);
aPC.aProtectedMethod(); // Keine hiervon ist legal
}
}

Obwohl sich AnyClassInTheSamePackage im gleichen Paket befindet wie AProtectedClass , ist sie keine Subklasse davon (sondern von Object). Nur Subklassen ist es gestattet, protected-Variablen und -Methoden zu sehen und zu verwenden.

Eines der deutlichsten Beispiele der Notwendigkeit für diese spezielle Zugriffsebene zeigt sich in der Unterstützung einer public-Abstraktion in Ihrer Klasse. Was die Außenwelt betrifft, haben Sie eine einfache public-Schnittstelle (über Methoden) für jede Abstraktion, die Sie für Ihre Benutzer definiert haben. Eine komplexere Darstellung und die Implementierung, die davon abhängt, ist im Inneren verborgen. Wenn Subklassen diese Darstellung erweitern und ändern, müssen sie die zugrundeliegende konkrete Darstellung erhalten:

public class SortedList {
protected BinaryTree theBinaryTree;
...
public Object[] theList() {
return theBinaryTree.asArray();
}
public void add(Object o) {
theBinaryTree.addObject(o);
}
}
public class InsertSortedList extends SortedList {
public void insert(Object o, int position) {
theBinaryTree.insertObject(o, position);
}
}

Ohne in der Lage zu sein, auf theBinaryTree direkt zuzugreifen, muß die insert()- Methode die Liste als Object-Array über die public-Methode theList() erhalten, ein neues größeres Array zuweisen und das neue Objekt manuell einfügen. Da sie »sieht«, daß ihre Superklasse BinaryTree verwendet, um die sortierte Liste zu implementieren, kann sie die in BinaryTree befindliche Methode insertObject() benutzen, um diese Aufgabe zu erfüllen.

Einige Sprachen, z.B. CLU, experimentieren mit expliziteren Formen des Anhebens und Senkens der Abstraktionsebene, um das gleiche Problem auf allgemeinere Art zu lösen. In Java löst protected das Problem nur teilweise, indem das Konkrete vom Abstrakten getrennt werden kann. Der Rest wird dem Programmierer überlassen.

Konventionen für den Zugriff auf Instanzvariablen

Als allgemeine Faustregel gilt, daß eine Instanzvariable private sein sollte, wenn sie nicht konstant ist (wie das definiert wird, lernen Sie in Kürze). Falls Sie diese Faustregel nicht einhalten, stoßen Sie auf folgendes Problem:

public class AFoolishClass {
public String aUsefulString;
... // Den nützlichen Wert für die Zeichenkette einrichten
}

Diese Klasse kann aUsefulString zur Verwendung durch andere Klassen einrichten, die diese (nur) lesen können. Da sie nicht private ist, können sich die anderen Klassen jedoch so verhalten:

AFoolishClass aFC = new AFoolishClass();
aFC.aUsefulString = "oops!";

Da es keine Möglichkeit gibt, die Schutzebene getrennt zum Lesen und Schreiben von Instanzvariablen zu bestimmen, sollten sie immer private sein.

Dem aufmerksamen Leser ist wahrscheinlich nicht entgangen, daß diese Regel in vielen Beispielen dieses Buches nicht eingehalten wird. Der Grund hierfür ist lediglich, die Beispiele übersichtlich und kurz zu halten. (Sie werden bald feststellen, daß viel Platz nötig ist, wenn man das richtigstellt.) Eine Verwendung kann nicht umgangen werden: Die System.out.print()-Aufrufe überall im Buch müssen die public-Variable out direkt benutzen. Sie können diese final-Systemklasse (die Sie eventuell anders geschrieben haben) nicht ändern. Sie können sich die verheerenden Folgen vorstellen, wenn jemand versehentlich den Inhalt dieser (globalen) public-Variablen ändert!

Vergleich der Zugriffskontrollebenen

Die Unterschiede zwischen den verschiedenen Arten des Zugriffsschutzes können einen sehr schnell verwirren. Speziell im Fall von protected-Methoden und -Variablen. Tabelle 15.1 hilft, die Unterschiede zwischen der am wenigsten einschränkenden (public ) bis zur restriktivsten (private) Form des Zugriffsschutzes zu verdeutlichen.

Tabelle 15.1: Die unterschiedlichen Ebenen des Zugriffsschutzes

Sichtbarkeit

public

protected

Default

private

Innerhalb derselben Klasse

Ja

Ja

Ja

Ja

Von einer bel. Klasse im selben Paket

Ja

Ja

Ja

Nein

Von einer bel. Klasse außerhalb des Pakets

Ja

Nein

Nein

Nein

Von einer Subklasse im selben Paket

Ja

Ja

Ja

Nein

Von einer Subklasse außerhalb des Pakets

Ja

Ja

Nein

Nein

Zugriffskontrolle und Vererbung

Ein letztes Thema bei der Zugriffskontrolle für Methoden steht im Zusammenhang mit Subklassen. Wenn Sie eine Subklasse erstellen und eine Methode überschreiben, dann müssen Sie die Zugriffskontrolle der Original-Methode beachten.

Sie werden sich vielleicht erinnern, daß die Applet-Methoden wie z.B. init() und paint() in Ihren eigenen Applets public sein mußten.

Als allgemeine Regel kann man folgendes sagen: Sie können eine Methode in Java nicht überschreiben und der neuen Methode eine stärkere Zugriffskontrolle zuweisen als die Original-Methode hatte. Allerdings haben Sie die Möglichkeit, die Zugriffskontrolle zu lockern. Folgende Regeln gelten für geerbte Methoden:

Accessor-Methoden

Wie kann die Außenwelt auf private-Instanzvariablen zugreifen? Indem »Accessor«- Methoden geschrieben werden:

public class ACorrectClass {
private String aUsefulString;
public String aUsefulString() { //Wert holen
return aUsefulString;
}
protected void aUsefulString(String s) { //Wert setzen
aUsefulString = s;
}
}

Die Verwendung von Methoden für den Zugriff auf eine Instanzvariable ist die häufigste Vorgehensweise in objektorientierten Programmen. Diese Vorgehensweise in allen Klassen zahlt sich aus, da die Programme robuster werden und gut wiederverwendet werden können.

Eine Namenskonvention für Accessor-Methoden ist das Voranstellen des Präfixes get bzw. set vor den Variablennamen. Diese Variante der Namensgebung erhöht die Lesbarkeit des Codes und Sie laufen nicht Gefahr, irgendwann einmal den Code ändern zu müssen, falls die andere Variante nicht mehr zulässig ist. Davon abgesehen ist es eine Frage des persönlichen Stils, welche Konvention Sie verwenden. Wenn Sie sich für eine entschieden haben, sollten Sie diese allerdings konsequent verwenden.

Class Circle
private int x, y, radius;

public int getRadius(){
return Radius
}
public int setRadius(int value){
radius = value
draw();
doOtherStuff();
return Radius
}

Aufruf:

oldRadius = theCircle.getRadius();   // Den Wert ermitteln
newRadius = theCircle.setRadius(4);  // Den Wert setzen usw.


Diese Konvention wird mit jeder Version von Java immer mehr zum Standard. Sie werden sich vielleicht daran erinnern, daß die Methode size() der Klasse Dimension mit Java 1.2 in getSize() umbenannt wurde. Sie werden diese Namenskonventionen vielleicht auch für Ihre eigenen Accessor-Methoden verwenden wollen, um Klassen verständlicher zu machen.

Klassenvariablen und -methoden

Was muß geschehen, wenn Sie eine Variable erstellen möchten, die alle Instanzen einer Klasse sehen und verwenden soll? Jede Instanz einer Instanzvariablen hat eine eigene Kopie der Variablen, so daß ihr Sinn zunichte gemacht werden würde. Wenn Sie sie in die Klasse setzen, gibt es nur eine Kopie und alle Instanzen der Klasse nutzen sie gemeinsam. Das nennt man Klassenvariable:

public class Circle {
public static float pi = 3.14159265F;
public float area(float r) {
return pi * r * r;
}
}

Aufgrund historischer Verflechtungen nutzt Java das Wort static, um Klassenvariablen und -methoden zu deklarieren. Wann immer Sie das Wort static sehen, denken Sie daran, sich geistig »Klasse« vorzustellen.

Instanzen können auf ihre eigenen Klassenvariablen so verweisen, als wären es Instanzvariablen, wie Sie im letzten Beispiel gesehen haben. Da pi public ist, können auch Methoden anderer Klassen darauf verweisen:

float circumference = 2 * Circle.pi * r;

Auch Instanzen von Circle können diese Zugriffsform benutzen. In den meisten Fällen ist das der Klarheit halber die bevorzugte Form, auch für Instanzen. Sie zeigt dem Leser sofort auf, daß und wo eine Klassenvariable benutzt wird und daß sie global in allen Instanzen vorkommt. Das mag pedantisch erscheinen, macht aber alles viel übersichtlicher.


Nebenbei bemerkt, falls Sie irgendwann über den Zugriff auf eine Klassenvariable Ihre Meinung ändern, sollten Sie für die Instanz (oder sogar die Klasse) Accessor-Methoden erstellen, um sie vor solchen Änderungen zu schützen.

Klassenmethoden werden analog definiert. Auf sie können Instanzen ihrer Klasse genauso zugreifen, während Instanzen anderer Klassen nur mit dem vollen Klassennamen auf sie zugreifen können. Im folgenden Beispiel (Listing 15.1) definiert eine Klasse Klassenmethoden, um ihre eigenen Instanzen zu zählen:

Listing 15.1: Der gesamte Quelltext von CountInstances.java

 1: public class CountInstances {
 2:     private static int numInstances = 0;
 3:
 4:     protected static int getNumInstances() {
 5:         return numInstances;
 6:     }
 7:
 8:     private static void addInstance() {
 9:         numInstances++;
10:     }
11:
12:     CountInstances() {
13:         CountInstances.addInstance();
14:     }
15:
16:     public static void main(String arguments[]) {
17:         System.out.println("Starting with " +
18:             CountInstances.getNumInstances() + " instances");
19:         for (int  i = 0; i < 10; ++i)
20:             new CountInstances();
21:         System.out.println("Created " +
22:             CountInstances.getNumInstances() + " instances");
23:     }
24: }

Das Programm erzeugt die folgende Ausgabe:

Started with 0 instances
Creates 10 instances

Dieses Beispiel hat eine ganze Reihe von Features. Sie sollten sich die Zeit nehmen, es Zeile für Zeile durchzuarbeiten. In Zeile 2 deklarieren Sie eine private-Klassenvariable (numInstances), die die Anzahl der Instanzen speichert. Es wird eine Klassenvariable (die Variable ist als static deklariert) verwendet, da die Anzahl der Instanzen für die Klasse als Gesamtes relevant ist und nicht für die einzelnen Instanzen. Sie ist außerdem private, so daß sie denselben Regeln bezüglich der Accessor-Methoden genügt wie Instanzvariablen.

Beachten Sie bitte, daß numInstances in derselben Zeile mit 0 initialisiert wird. Genauso wie eine Instanzvariable initialisiert wird, wenn deren Instanz erzeugt wird, wird eine Klassenvariable initialisiert, wenn deren Klasse erzeugt wird. Die Initialisierung einer Klasse findet statt, bevor irgend etwas anderes mit der Klasse oder deren Instanzen geschehen kann. Aus diesem Grund wird das Beispiel wie geplant funktionieren.

In den Zeilen 4-6 erstellen Sie eine get-Methode (getNumInstances()) für die private -Klassenvariable, um deren Wert auszulesen. Diese Methode ist ebenfalls als Klassenmethode deklariert, da diese zu der Klassenvariablen gehört. Die Methode getNumInstances() ist als protected und nicht als public deklariert, da nur diese Klasse und vielleicht noch Subklassen davon an dem Wert interessiert sind. Andere Klassen bleiben aus diesem Grund außen vor.

Beachten Sie bitte, daß es keine Accessor-Methode zum Setzen des Werts gibt. Der Grund dafür ist, daß der Wert der Variablen nur dann inkrementiert werden soll, wenn eine neue Instanz erzeugt wird. Sie sollte nicht einfach so auf einen Wert gesetzt werden. Deshalb erstellen Sie anstelle einer Accessor-Methode eine spezielle private- Methode mit dem Namen addInstance() in den Zeilen 8-10, die den Wert von numInstances um 1 inkrementiert.

In den Zeilen 12-14 befindet sich der Konstruktor dieser Klasse. Erinnern Sie sich bitte daran, daß Konstruktoren aufgerufen werden, sobald ein neues Objekt erzeugt wird. Dies stellt den sinnvollsten Ort dar, um die Methode addInstance() aufzurufen und die Variable zu inkrementieren.

Schließlich deutet die main()-Methode darauf hin, daß Sie dieses Programm als Java- Applikation ausführen können. Alle anderen Methoden können Sie mit dieser testen. In der main()-Methode erzeugen Sie 10 Instanzen der Klasse CountInstances. Anschließend wird der Wert der Klassenvariablen numInstances ausgegeben (der 10 sein sollte).

Der final-Modifier

Der final-Modifier ist sehr vielseitig:

final-Variablen

Um Konstanten in Java zu deklarieren, verwenden Sie final-Variablen:

public class AnotherFinalClass {
public static final int aConstantInt = 123;
public final String aConstantString = "Hello World!";
}

final-Klassen und -Instanzvariablen können in Ausdrücken wie normale Klassen und Instanzvariablen verwendet, aber nicht geändert werden. Deshalb muß final-Variablen ihr (konstanter) Wert zum Zeitpunkt der Deklaration zugewiesen werden. Klassen können über final-Klassenvariablen anderen Klassen nützliche Konstanten liefern. Andere Klassen greifen auf sie wie oben zu: AnotherFinalClass.aConstantInt.

Lokale Variablen (diejenigen, die in Codeblöcken zwischen Klammern stehen, z.B. in while- oder for-Schleifen) können unter Java 1.02 nicht final deklariert werden. Vor lokalen Variablen dürfen überhaupt keine Modifier stehen:

{
int aLocalVariable; // Ich komme ganz gut ohne Modifier zurecht...
...
}

In Java 1.2 wurde dies im Zuge der Einführung der Inner Classes möglich.

final-Methoden

Nachfolgend ein Beispiel mit final-Methoden:

public class MyPenultimateFinalClass {
public static final void aUniqueAndReallyUsefulMethod() {
...
}
public final void noOneGetsToDoThisButMe() {
...
}
}

final-Methoden können nicht in Subklassen überschrieben werden. Eine Methode soll nicht das letzte Wort in einer Implementierung haben, weshalb sollte auf Methoden also dieser Modifier angewandt werden?

Aus Gründen der Effizienz. Wenn Sie eine Methode final deklarieren, kann der Compiler davon ausgehen, daß nie eine Subklasse davon auftaucht und daß die Definition der Methode nicht geändert werden kann.

Die Java-Klassenbibliothek deklariert viele übliche Methoden final, so daß Sie Vorteile in bezug auf Geschwindigkeit haben. Im Fall von Klassen, die bereits final sind, ist das absolut sinnvoll. Die wenigen final-Methoden, die in Nicht-final-Klassen deklariert sind, sind eher ein Ärgernis. Sie können sie nicht in Subklassen überschreiben. Ist Effizienz in künftigen Java-Versionen keine vorrangige Frage mehr, werden eventuell viele dieser final-Methoden wieder »aufgetaut«, so daß entgangene Flexibilität des Systems wiederhergestellt wird.


private-Methoden sind effektiv final, wie das auch bei allen Methoden in einer final -Klasse der Fall ist. Die Kennzeichnung dieser Methoden mit final (was die Java- Bibliothek manchmal macht) ist zulässig, aber redundant. Der derzeitige Compiler behandelt sie ohnehin als final. final-Methoden können aus den gleichen Sicherheitsgründen wie final-Klassen benutzt werden, jedoch ist das eher selten.

Falls Sie (wie empfohlen) reichlich Gebrauch von Accessor-Methoden machen und sich um die Effizienz sorgen, sehen Sie sich diese neue Fassung von ACorrectClass an, die viel schneller ist:

public class ACorrectFinalClass {
private String aUsefulString;
public final String aUsefulString() { // Läuft jetzt schneller
return aUsefulString;
}
protected final void aUsefulString(String s) { // Auch schneller
aUsefulString = s;
}
}

Künftige Java-Compiler werden sicherlich klug genug sein, um einfache Methoden automatisch zu verarbeiten, deshalb müssen Sie final in solchen Fällen eventuell nicht mehr verwenden.

final-Klassen

Nachfolgend die Deklaration einer final-Klasse:

public final class AFinalClass {
...
}

Eine Klasse wird aus zwei Gründen final deklariert: erstens wegen der Sicherheit. Niemand außer Ihnen soll in der Lage sein, Subklassen und neue oder andere Instanzen davon zu erstellen. Zweitens wegen der Effizienz. Sie möchten sich darauf verlassen können, daß sich Instanzen in nur einer Klasse (nicht in Subklassen) befinden, so daß Sie sie optimieren können.

In der Java-Klassenbibliothek werden final-Klassen reichlich verwendet. Beispiele des ersten Grundes für final sind folgende Klassen: java.lang.System sowie InetAddress und Socket aus dem Paket java.net. Ein gutes Beispiel für den zweiten Grund von final ist java.lang.String.

Sie werden zwar selten Gelegenheit haben, eine final-Klasse selbst zu erstellen, jedoch erhalten Sie reichlich Gelegenheit, sich darüber zu ärgern, daß bestimmte Systemklassen final sind (und damit ihre Erweiterung erschweren). Nehmen wir das für mehr Sicherheit und Effizienz eben in Kauf. Wir wollen hoffen, daß Effizienz bald kein Thema mehr ist und einige dieser Klassen wieder public sein werden.

abstract-Methoden und -Klassen

Bei der Anordnung von Klassen in einer Vererbungshierarchie geht man von der Annahme aus, daß die höheren Klassen abstrakter und allgemeiner sind, während die unteren Subklassen konkreter und spezifischer sind. Meist verwendet man bei der Auslegung von Klassen gemeinsame Design- und Implementierungsmerkmale in einer Superklasse. Ist dieser gemeinsame Speicherort der primäre Grund, daß eine Superklasse existiert und sollen nur Subklassen verwendet werden, nennt man eine solche Superklasse eine abstrakte Klasse. Abstrakte Klassen werden mit dem Modifier abstract deklariert.

Von abstract-Klassen können keine Instanzen erstellt werden, jedoch können sie alles enthalten, was in einer normalen Klasse stehen kann. Darüber hinaus sind Präfixe für Methoden mit dem Modifier abstract zulässig. Nichtabstrakte Klassen dürfen diesen Modifier nicht verwenden. Hier ein Beispiel:

public abstract class MyFirstAbstractClass {
int anInstanceVariable;
public abstract int aMethodMyNonAbstractSubclassesMustImplement();
public void doSomething() {
... // Eine normale Methode
}
}
public class AConcreteSubClass extends MyFirstAbstractClass {
public int aMethodMyNonAbstractSubclassesMustImplement() {
... // Wir müssen diese Methode implementieren
}
}

Und hier ein paar Versuche, diese Klassen zu benutzen:

Object a = new MyFirstAbstractClass(); // Unzulässig, ist abstrakt
Object c = new AConcreteSubClass();    // OK, das ist eine konkrete Subklasse

abstract-Methoden brauchen keine Implementierung, während das bei nichtabstrakten Subklassen notwendig ist. Die abstract-Klasse stellt nur eine Maske für die Methoden bereit, die später von anderen implementiert werden. In der Java-Klassenbibliothek gibt es viele abstract-Klassen, für die es im System keine dokumentierten Subklassen gibt. Sie dienen lediglich als Grundlage zum Erstellen von Subklassen in eigenen Programmen.

Die Verwendung einer abstract-Klasse zur Umsetzung eines reinen Designs, d.h. mit nichts als abstract-Methoden, wird in Java mit einer Schnittstelle (wird morgen behandelt) besser erreicht. Ruft ein Design eine Abstraktion auf, die einen Instanzzustand und/oder eine teilweise Implementierung beinhaltet, ist eine abstrakte Klasse allerdings nicht die einzige Wahl. In älteren objektorientierten Sprachen sind abstrakte Klassen lediglich eine Konvention. Sie haben sich als derart nützlich erwiesen, daß sie in Java nicht nur in der hier beschriebenen Form, sondern auch in der reineren Form von Schnittstellen unterstützt werden.

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 wiederzuverwenden 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 Vererbungshierarchie 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 entwikkelt 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 anderen 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 Dateien und Unterverzeichnisse, wie image und peer, in diesem Verzeichnis. 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. Der Umfang einer Klasse wird dadurch nicht erweitert.

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. Ein Mensch würde fragen, auf welche Version der Name-Klasse sich Ihr Programm bezieht, wenn Sie folgendes eingeben:

Name myName = new Name("Susan");

Doch dies ist bei Java 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.

Eigene Pakete erstellen

Das Erstellen eigener Pakete ist in Java nicht komplexer als das Erstellen einer Klasse. 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 Paket 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-Namens 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 eindeutig 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. Sie haben 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.

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 15.2 zeigt zwei Klassen, die diesen Punkt darstellen. Die erste ist eine öffentliche Klasse, die eine verkettete Liste implementiert, die zweite ist ein Knoten dieser Liste.

Listing 15.2: Der gesamte Quelltext von LinkedList.java

 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 LinkedList-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 LinkedList verwendet. Node, das eine dieser Hilfsklassen ist, wird deshalb ohne einen public-Modifier deklariert und erscheint nicht als Teil der öffentlichen Schnittstelle des collections-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 Einfachvererbung

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 Probleme verdeutlichen. Am 2. Tag, als Sie die Klassenhierarchie erstmals kennengelernt haben, wurde die Vehicle- Hierarchie erläutert, siehe Abb. 15.1.


Abbildung 15.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 electricalSystemFailure()()enthält), ist diesen beiden Klassen gemeinsam, aber da 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 BritishThing-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 zugunsten 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 Hauptklassenhierarchie 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 Schnittstellenhierarchie. 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 Methodendeklarationen 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.

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. Im folgenden soll der Blick auf die einzelnen Kodierungen geworfen werden. 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 bei den Threads durchgeführt und die Schnittstelle Runnable in Ihre Applet-Definition eingefügt:

// 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 eine 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 Klasse 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, wovon die Klasse Orange abgeleitet werden soll (wenn z.B. plötzlich eine großartige Sphere-Klasse 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 Einfachvererbung in der 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 implementieren,
                         // 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 anderer Klassen (Methodensignaturen) 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 durch die Verwendung von Schnittstellen zur Trennung zwischen Design und Implementierung ein Code verallgemeinern und einfacher gestalten läßt.

Schnittstellen definieren und ableiten

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 {
...
}

Dies ist im Grunde dasselbe wie eine Klassendefinition, wobei das Wort interface das Wort class ersetzt. Innerhalb der Schnittstellendefinition 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 als public oder ohne Modifier deklariert sein. 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, welche 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 kein Fruit-Objekt 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 Subschnittstelle 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 Schnittstellen ableiten (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 Schnittstellendefinition 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, bei denen sich lediglich der Rückgabetyp unterscheidet, erzeugen einen Compiler-Fehler.

Ein Beispiel: Verkettete Listen

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 15.3 zeigt den Code.

Listing 15.3: Der gesamte Quelltext von LinkedList.java

 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 = createLinkedList();
java.util.Enumeration e = aLinkedList.enumerate();

while (e.hasMoreElements()) {
    Object  anObject = e.nextElement();
    // etwas sinnvolles mit anObject anstellen
}

Beachten Sie, daß wir Enumeration e zwar so benutzen, als wüßten wir, was es 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 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 anzuwenden ist.

Interne Klassen

Die meisten Java-Klassen wurden auf der Paketebene definiert, das bedeutet, daß jede Klasse ein Mitglied eines 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.

Listing 15.4 beinhaltet ein Applet, das den Namen Inner, das eine interne Klasse namens BlueButton verwendet. Diese Klasse repräsentiert Schaltflächen, deren Hintergrundfarbe standardmäßig blau ist.

Listing 15.4: Der gesamte Quelltext von Inner.java

 1: import java.awt.Button;
 2: import java.awt.Color;
 3:
 4: public class Inner extends java.applet.Applet {
 5:     Button b1 = new Button("One");
 6:     BlueButton b2 = new BlueButton("Two");
 7:
 8:     public void init() {
 9:         add(b1);
10:         add(b2);
11:     }
12:     class BlueButton extends Button {
13:         BlueButton(String label) {
14:             super(label);
15:             this.setBackground(Color.blue);
16:         }
17:     }
18: }

Die Abbildung 15.2 wurde mit dem Appletviewer erzeugt. Das Applet wurde über den folgenden HTML-Code in die Webseite integriert:

<applet code="Inner.class" width=100 height=100>
</applet>


Abbildung 15.2:
Das Inner-Applet

In diesem Beispiel unterscheidet sich die Klasse BlueButton nicht von einer Hilfsklasse, die sich in derselben Quelldatei befindet, in der sich auch die Hauptklasse des Programms befindet. Der einzige Unterschied ist, daß die Hilfsklasse in der Hauptklasse selbst definiert ist, was einige Vorteile hat:

In vielen Fällen ist eine interne Klasse eine kleine Klasse, die nur für eine sehr eingeschränkte Aufgabe zuständig ist. In dem Inner-Applet ist die Klasse BlueButton gut für die Implementierung als interne Klasse geeignet, da sie kaum komplexe Verhaltensweisen und Attribute enthält.

Der Name der internen Klasse ist mit dem Namen der Klasse verbunden, die die interne Klasse beinhaltet. Dieser wird bei der Kompilierung des Programms automatisch zugewiesen. Im BlueButton-Beispiel wird der Name Inner$BlueButton.class vom JDK vergeben.


Wenn Sie interne Klassen verwenden, müssen Sie noch stärker darauf achten, daß Sie alle .class-Dateien mitliefern, wenn Sie ein Programm verfügbar machen. Jede interne Klasse hat ihre eigene .class-Datei und diese müssen zusammen mit der jeweiligen top-level-Klasse zur Verfügung gestellt werden. Wenn Sie z.B. das Inner-Applet veröffentlichen würden, dann müßten Sie sowohl die Datei Inner.class als auch die Datei Inner$BlueButton.class veröffentlichen.

Interne Klassen scheinen nur eine kleine Erweiterung der Sprache Java zu sein. Tatsächlich stellen Sie eine wesentliche Änderung der Sprache dar.

Regeln, die den Gültigkeitsbereich einer internen Klasse betreffen, decken sich fast mit denen der Variablen. Der Name einer internen Klasse ist außerhalb ihres Gültigkeitsbereichs nicht sichtbar - außer in einer vollständigen Namensangabe. Dies hilft bei der Strukturierung von Klassen in einem Paket. Der Code einer internen Klasse kann einfach die Namen der umgebenden Gültigkeitsbereiche verwenden. Darunter fallen sowohl Klassen und Variablen von umgebenden Klassen als auch lokale Variablen umgebender Blocks.

Zusätzlich können Sie eine Top-Level-Klasse als ein static-Mitglied einer anderen Top-Level-Klasse definieren. Anders als eine interne Klasse kann eine Top-Level-Klasse nicht direkt die Instanz-Variablen einer anderen Klasse verwenden. Die Möglichkeit, Klassen auf diese Art ineinander zu verschachteln, erlaubt es einer Top-Level- Klasse, eine Art Paketorganisation für logisch zueinander in Bezug stehende Top-Level-Klassen der zweiten Reihe zu bieten.

Zusammenfassung

Heute haben Sie gelernt, wie Sie ein Objekt mit Hilfe von Modifiern für die Zugriffskontrolle auf dessen Methoden und Variablen kapseln. Sie haben auch gelernt, wie Sie die Modifier static, final und abstract bei der Entwicklung von Java-Klassen und Klassenhierarchien verwenden.

Um den Aufwand der Entwicklung und Verwendung einer Reihe von Klassen zu unterstützen, haben Sie gelernt, wie Klassen in Paketen gruppiert werden können. Diese Gruppen helfen Ihnen dabei, Ihre Programme besser zu organisieren, und erlauben es Ihnen, Ihre Klassen mit den vielen Java-Programmierern zu teilen, die ihren Code öffentlich verfügbar machen.

Schließlich haben Sie noch gelernt, wie Sie Schnittstellen und interne Klassen implementieren. Dabei handelt es sich um zwei Strukturen, die beim Entwurf einer Klassenhierarchie hilfreich sind.

Fragen und Antworten

Frage:
Ich fürchte, daß die intensive Verwendung von Accessor-Methoden meinen Java- Code verlangsamt. Stimmt das?

Antwort:
Nicht unbedingt. Demnächst sind Java-Compiler klug genug, um alles automatisch zu beschleunigen. Wenn Sie sich aber über die Geschwindigkeit Sorgen machen, können Sie Accessor-Methoden final deklarieren, dann laufen sie so schnell wie direkte Instanzvariablen.

Frage:
Unterliegen static-Methoden der gleichen Vererbung wie Instanzmethoden?

Antwort:
Nein. Klassenmethoden sind standardmäßig final. Das bedeutet, daß Sie keine Klassenmethode als nicht final deklarieren können! Die Vererbung von Klassenmethoden ist nicht zulässig, was die Symmetrie zu Instanzmethoden bricht.

Frage:
Sofern ich die letzte Lektion richtig verstanden habe, scheinen final-abstract- oder private-abstract-Methoden oder -Klassen unsinnig zu sein. Sind sie überhaupt zulässig?

Antwort:
Nein, sind sie nicht. Das haben Sie richtig erkannt; sie führen zu Kompilierfehlern. Um überhaupt brauchbar zu sein, müssen abstract-Methoden überschrieben und von abstract-Klassen müssen Subklassen angelegt werden. Beide Operationen sind aber unzulässig, falls sie gleichzeitig auch public oder final sind.



vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Ein Imprint des Markt&Technik Buch- und Software-Verlag GmbH.
Elektronische Fassung des Titels: Java 2 in 21 Tagen, ISBN: 3-8272-5578-3