Previous Page TOC Index Next Page See Page

Java für Fortgeschrittene

Je größer Ihr Programm wird, und je mehr Ihrer Klassen Sie für neue Projekte wiederverwenden, desto dringender brauchen Sie eine Möglichkeit, ihre Sichtbarkeit zu kontrollieren. Eine der Lösungen für dieses Problem, die Sie in einer Klasse realisieren können, sind Modifier.

Für größere Dimensionen könnten Sie Pakete bilden, die zusammen mit Schnittstellen ermöglichen, Klassen und Klassenverhalten zu implementieren und zu gruppieren.

Heute werden Sie lernen, die folgenden Dinge zu erzeugen und zu nutzen:

Modifier

Nachdem Sie eine Zeit lang mit Java programmiert haben, werden Sie feststellen, daß es recht müßig sein kann, alle Ihre Klassen, Methoden und Variablen als public zu deklarieren. Darüber hinaus ist einer der wichtigsten Aspekte der OOP die Kapselung, also das Verbergen von Informationen. Neben public gibt es jedoch noch mehrere ähnliche Schlüsselwörter, die Ihnen helfen, Ihre Klassen und deren Inhalt zu schützen, die sogenannten Modifier.


Modifier sind Schlüsselwörter, die in verschiedenen Kombinationen auf die Methoden und die Variablen einer Klasse und zum Teil auch auf die Klasse selbst angewendet werden können.
Es gibt sehr viele und unterschiedliche Modifier. Die Reihenfolge, in der sie angegeben werden, hat nichts mit ihrer Bedeutung zu tun - reine Geschmackssache. Gewöhnen Sie sich einfach einen bestimmten Stil an und behalten Sie ihn bei. Hier die empfohlene (kanonische) Reihenfolge:

<access> abstract static final <unusual> native synchronized interface

Hier kann <access> public, private oder protected sein, und <unusual> kann transient oder volatile sein.

Alle Modifier sind optional. Keiner davon muß in einer Deklaration angegeben werden. Sie können beliebig viele davon anwenden, je nachdem, was notwendig ist, um die gewünschte Verwendung und die Einschränkungen für Ihre Deklarationen zu realisieren. In einigen Sonderfällen (beispielsweise innerhalb einer Schnittstelle) werden bestimmte Modifier implizit für Sie definiert und Sie brauchen sie nicht anzugeben - sie sind standardmäßig vorhanden. Wenn Sie beispielsweise keinen <access>-Modifier angeben, setzt Java voraus, daß Sie den Zugriff für das gesamte Paket erlauben wollen, so daß kein Schlüsselwort erforderlich ist.

Einige dieser Modifier sind relativ kompliziert: transient, volatile, synchronized und native. Der Vollständigkeit halber folgt hier jedoch eine kurze Beschreibung. Der Modifier transient wird verwendet, um eine Variable außerhalb des persistenten Teils eines Objekts zu deklarieren. Dadurch ist es einfacher, Speichersysteme für persistente Objekte zu implementieren. Die Modifier volatile und synchronized haben mit dem Multithreading zu tun. Der Modifier native spezifiziert, daß eine Methode in der ursprünglichen Sprache Ihres Computers (normalerweise C) und nicht in Java implementiert ist. Sie müssen diese Modifier nicht kennen, wenn Sie die heutige Lektion verstehen wollen, lassen Sie sich also nicht verwirren, wenn Ihnen ihre Bedeutung noch nicht ganz klar ist.

Der Modifier interface wird, wie Sie schon erwartet haben, verwendet, um Klassen als Schnittstellen zu kennzeichnen. Mehr über Schnittstellen erfahren Sie später in dieser Lektion. In diesem Abschnitt werden Sie die <access>-Modifier (public, private, protected), abstract, static und final genauer kennenlernen.

Zugriffssteuerung für Methoden und Variablen

Die Zugriffssteuerung kontrolliert die Sichtbarkeit. Wenn eine Methode oder eine Variable für eine andere Klasse sichtbar ist, können deren Methoden dieses Klassenelement aufrufen oder verändern.


Ein Klassenelement ist eine Methode oder eine Variable der Klasse

Um eine Klasse oder ein Klassenelement vor solchen Zugriffen zu schützen, verwenden Sie die in den nächsten Teilabschnitten beschriebenen Sichtbarkeitsebenen. Diese Ebenen bieten sukzessive jeweils mehr Schutz als die darunterliegenden.

Sie sollten die Verwendung der Modifier verstehen, weil dadurch die grundlegenden Beziehungen beschrieben werden, die eine Methode oder eine Variable in einer Klasse zu anderen Klassen und Unterklassen im System haben kann.

public

Weil jede Klasse eine Insel ist, betrifft die erste dieser Beziehungen den Unterschied zwischen dem Inneren und dem Äußeren einer Klasse. Jede Methode oder Variable ist für die Klasse, in der sie definiert ist, sichtbar, aber was tun Sie, wenn sie auch für alle Klassen außerhalb dieser Klasse sichtbar sein sollen?

Die Antwort ist offensichtlich: Sie deklarieren das Klassenelement einfach mit öffentlichem Zugriff, public. Fast jedes Klassenelement, das in dieser Woche definiert wurde, war der Einfachheit halber public. Wenn Sie die gezeigten Beispiele in Ihrem eigenen Code einsetzen, wollen Sie vielleicht strengere Einschränkungen vorgeben. Beim Lernen ist es jedoch sinnvoll, den größtmöglichen Zugriff zu verwenden (public oder package), und diesen dann mit der Zeit immer weiter einzuschränken, nachdem Sie mehr Erfahrung gesammelt haben. Bald wird es ganz selbstverständlich für Sie sein, bei der Deklaration von Klassenelementen zu überlegen, welcher Zugriff darauf möglich sein soll. Hier einige Beispiele für public-Deklarationen:

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

Eine Klasse oder ein Klassenelement mit dem Zugriff public hat die größtmögliche Sichtbarkeit. Jeder kann es sehen, jeder kann es verwenden.

package (Standard)


Technischer Hinweis
In Java gibt es zwar das Schlüsselwort package, aber es wird nicht in diesem Kontext verwendet. Statt dessen setzt man es ein, um anzugeben, ob eine Klasse in einem Paket enthalten ist. (Siehe Abschnitt »Pakete« später in diesem Kapitel.) Der in diesem Abschnitt beschriebene Schutz ist auf Paketebene realisiert, aber das Schlüsselwort package ist implizit. Wenn kein anderer Zugriffs-Modifier angegeben ist, nimmt man als Standard package an.

In einigen Sprachen gibt es das Konzept, eine Klasse oder ein Klassenelement zu verbergen, so daß nur die Funktionen innerhalb einer bestimmten Quelldatei sie sehen können. In java wird dieses Konzept ersetzt durch das explizitere Schema der Pakete, in denen Klassen gruppiert werden können. Mehr darüber erfahren Sie später. Hier müssen Sie nur verstehen, wie die Beziehung einer Klasse zu ihren gleichgeordneten Klassen, aus denen sich ein System, eine Bibliothek oder ein Programm zusammensetzen, unterstützt werden kann. Das definiert die nächste Ebene verstärkten Schutzes und eingeschränkter Sichtbarkeit.

Aus irgendeinem Grund gibt es für diese Zugriffsebene kein Schlüsselwort. Es wird durch das Fehlen eines Zugriffs-Modifiers in einer Deklaration angezeigt und stellt deshalb den Standard dar. Historisch wurde es als »friend« oder »package« bezeichnet. Das letztere scheint am geeignetsten zu sein und wird deshalb hier verwendet. In zukünftigen Implementierungen wird Java Ihnen möglicherweise erlauben, diese Zugriffsebene explizit zu deklarieren, aber noch handelt es sich einfach um den Standard-Zugriffsschutz, wenn nichts anderes spezifiziert wurde.


Warum sollte man mehr Schreibarbeit als nötig wollen und eine Methode oder Variable explizit als package deklarieren? Hauptsächlich der Konsistenz und Klarheit halber. Wenn Sie ein Deklarationsmuster mit wechselnden Zugriffs-Modifiern haben, wollen Sie vielleicht immer, daß der Modifier explizit angegeben wird, sowohl für den Leser als auch, wenn Sie wollen, daß der Compiler in bestimmten Situationen Ihre Absicht erkennt und Sie über etwaige Konflikte benachrichtigt.


Sie können diese Zugriffsebene in der Zwischenzeit explizit beschriften, indem Sie einen Kommentar am Anfang der Codezeile einführen, zum Beispiel:

/* package */ float aPackageMethod() {...}


Ein Großteil der Deklarationen, die Sie bisher kennengelernt haben, verwenden diese Standardebene des Zugriffsschutzes. Hier ein Beispiel dafür, wie das funktioniert:

public class ALessPublicClass {
int aPackage Int = 2;
String aPackageString = "a 1 and a ";
float aPackageMethod() { // Kein Zugriffs-Modifier - > "package"
...
}
}
public class AClassInTheSamePackage {
public void testUse() {
ALessPublicClass aLPC = new ALessPublicClass();
System.out.println(aLPC.aPackageString + aLPC.aPackageInt);
aLPC.aPackageMethod(); // alle Zugriffe sind erlaubt
}
}

Wenn eine Klasse aus einem anderen Paket versuchen würde, auf aLPC so zuzugreifen, wie es AClassInTheSamePackage getan hat, würde ein Compiler-Fehler aufgeworfen.

Warum wurde diese Zugriffsebene als Standard eingeführt? Wenn Sie ein großes System entwickeln und Ihre Klassen auf Arbeitsgruppen verteilen, die jeweils kleinere Teile dieses Systems implementieren, brauchen die Klassen häufig gegenseitig Zugriff auf ihre Information, mehr, als auf die der Außenwelt. Das war genug, um einen Standard-Zugriffsschutz einzuführen.

Eine weitere interessante Eigenschaft dieser Zugriffsschutzebene ist, daß keine Unterklasse, die in anderen Paketen deklariert ist, Klassenelemente mit diesem Zugriffsschutz in der Oberklasse erben oder darauf zugreifen kann. Nur Unterklassen desselben Pakets wie ihre Oberklasse können auf diese Klassenelemente zugreifen oder sie erben.

protected

Diese Zugriffsebene kann nur auf Klassenelemente angewendet werden, nicht auf Klassen. Andere Klassen müssen sich mit dem öffentlichen Gesicht zufriedengeben, das ihnen eine als protected deklarierte Klasse präsentiert. Um die für Unterklassen reservierte Zugriffsebene zu unterstützen, haben moderne Programmiersprachen eine Zwischenstufe eingeführt, die strenger ist als die beiden vorhergehenden Ebenen, aber etwas lockerer als reine Privatheit. Diese Ebene bietet mehr Schutz und weniger Sichtbarkeit gegenüber dem Rest der Welt.

Wenn ein Klassenelement als protected deklariert ist, steht es den Klassen aus demselben Paket zur Verfügung, Unterklassen aus dem selben Paket haben Zugriff darauf und können davon erben, und es kann von Unterklassen geerbt werden, die in anderen Paketen deklariert sind. Ein Zugriff auf die Elemente der protected-Klasse aus einem anderen Paket ist jedoch nicht möglich, auch wenn es sich um eine eigene Unterklasse handelt.

Was passiert, wenn Ihre Implementierung Details aufweist, die nicht einmal die gleichgeordneten Klassen aus dem selben Paket erkannt werden sollen, daß es aber möglich sein sollte, daß die Unterklassen sie erben? Damit kommen wir schnell zur nächsten Zugriffschutzebene, private protected.

private protected

Die Beziehung zwischen einer Oberklasse und ihren aktuellen und zukünftigen Unterklassen wird durch diese nächste Ebene des Zugriffsschutzes noch weiter eingeschränkt. Klassen können nicht als private protected deklariert werden, sondern nur Klassenelemente. Keine andere Klasse kann auf Klassenelemente zugreifen, die als private protected deklariert sind, auch keine Unterklassen im selben Paket. Die einzige Sichtbarkeit, die auf dieser Ebene gewährt wird, ist, daß Unterklassen diese Klassenelemente erben können, unabhängig davon, ob sie in dem Paket oder außerhalb deklariert sind.

public class AProtectedClass {
private protected int aProtectedInt = 4;
private protected String aProtectedString = "and a 3 and a ";
privte protected float aProtectedMethod() {
...
}
}
public class AProtectedClassSubclass extends AProtectedClass {
public void testUse() {
AProtectedClassSubclass aPCS = new AProtectedClassSubclass();
System.out.println(aPCS.aProtectedString + aPCS.aProtectedInt);
aPCS.aProtectedMethod(); // alle Verweise sind ok
}
}
public class AnyClassInTheSamePackage {
public void testUse() {
AProtectedClassSubclass aPCS = new AProtectedClassSubclass();
System.out.println(aPCS.aProtectedString + aPCS.aProtectedInt);
aPCS.aProtectedMethod(); // diese Zugriffe sind nicht erlaubt
}
}

Auch wenn sich AnyClassInTheSamePackage im selben Paket wie AProtectedClass befindet, ist es keine Unterklasse davon (standardmäßig ist es eine Unterklasse von Object). Beachten Sie, daß nur Unterklassen die als private protected deklarierten Klassenelemente erben dürfen.

private

Die strengste aller Beziehungen wird durch den Modifier private repräsentiert. Er realisiert den kleinsten Sichtbarkeitsbereich und den größtmöglichen Zugriffsschutz - das genaue Gegenteil von public. Klassenelemente, die als private deklariert sind, stehen nicht für andere Klassen zur Verfügung und können auch nicht von einer Unterklasse geerbt werden. Sie können nur von der Klasse genutzt werden, in der sie definiert sind.

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

Das scheint sehr restriktiv zu sein, wird aber häufig als Zugriffsschutz genutzt. Alle privaten Daten, interne Statuszustände oder für Ihre Implementierung einzigartigen Konzepte - alles, was Unterklassen nicht gezeigt werden soll - sollten als private deklariert werden. Sie wissen, daß die wichtigste Aufgabe eines Objekts ist, seine Daten zu kapseln, sie vor der Einsichtnahme durch die Außenwelt zu schützen und nur bestimmte Änderungen zuzulassen. Am besten wird das realisiert, indem so viele Daten wie möglich als privat dargestellt werden. Ihre Methoden können immer etwas weniger restriktiv sein, wie Sie später noch sehen werden, aber es ist wichtig, daß Sie die Zügel immer in der Hand halten.

Zugriffskonventionen für Instanzvariablen

Eine gute Faustregel ist, daß, wenn eine Instanzvariable nicht konstant ist, sie wenigstens private sein sollte. Wenn Sie das nicht so handhaben, haben Sie folgendes Problem:

public class AFoolishClass {
public String aUsefulString;
aUsefulString = "etwas wirklich Praktisches";
}

Diese Klasse wurde vielleicht dafür angelegt, einen aUsefulString für die anderen Klassen bereitzustellen, mit der Erwartung, daß diese ihn nur lesen würden. Weil diese Klasse jedoch nicht als private deklariert ist, können die anderen Klassen einfach folgendes machen:

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

Weil es keine Möglichkeit gibt, den Zugriffsschutz zum Lesen von dem zum Schreiben von Instanzvariablen separat zu spezifizieren, sollten sie eigentlich immer private sein.


Der aufmerksame Leser wird bemerkt haben, daß diese Regeln in vielen Beispielen in diesem Buch mißachtet wurde. Ein Großteil dieser Verletzungen erfolgte aus pädagogischen Gründen, um die Beispiele deutlicher zu machen und sie kurz zu halten. (Sie werden gleich sehen, daß es sehr viel aufwendiger ist, es richtig zu machen.)

Eine Verwendung kann nicht vermieden werden: die Aufrufe System. out.print() und System.out.println(), die Sie überall im Buch finden, müssen die Variable public out direkt verwenden. Sie können diese Systemklasse nicht ändern (die Sie vielleicht anders realisiert hätten). Sie können sich vorstellen, welch verheerende Folgen es hätte, wenn jemand versehentlich den Inhalt dieser globalen public-Variablen ändern würde!

Wenn Instanzvariablen privat sind, wie können Sie der Außenwelt dann Zugriff darauf erteilen? Durch Zugriffsmethoden. Die Verwendung von Methoden zur Steuerung des Zugriffs auf eine Instanzvariable ist eines der gebräuchlichsten Konzepte der objektorientierten Programmierung. Die konsequente Anwendung in allen Ihren Klassen zahlt sich durch robustere und wiederverwendbare Programme aus. Hier ein einfaches Beispiel, das zeigt, wie das bewerkstelligt werden kann:

public class ACorrectClass {
private String aUsefulString;

public String getAUsefulString() {
return aUsefulString;
}

private protected void setAUsefulString(String aStr) {
aUsefulString = aStr;
}
}

Beachten Sie, wie die Aufteilung zwischen dem Lesen und dem Schreiben der Instanzvariablen (unter Verwendung von getAUsefulString() und setAUsefulString()) ermöglicht, eine public-Methode zu spezifizieren, die ihren Wert zurückgibt (so daß sie für die Außenwelt nur gelesen werden kann), und eine private protected-Methode, mit der sie gesetzt wird (so daß sie innerhalb der Klasse gelesen und gesetzt werden kann). Das ist in der Regel ein sinnvolles Zugriffsschema, weil jeder vielleicht einmal den Wert abfragen muß, aber nur Sie (oder Ihre Unterklassen) dazu in der Lage sein sollten, ihn zu ändern. Wenn es sich um besonders geheime Daten handelt, könnten Sie die Methode zum Setzen dafür private machen, und die Methoden zum Ermitteln protected, oder eine andere Kombination verwenden, die die Bedeutung der Daten entsprechend berücksichtigt.

Wenn Sie etwas an Ihre eigene Instanzvariable anfügen wollen, versuchen Sie es mit dem folgenden:

setAUsefulString(getAUsefulString() + " anzufügender Text");

In diesem Beispiel verwenden Sie Zugriffsmethoden, um aUsefulString so zu ändern, als würde es von außerhalb der Klasse geändert. Warum?

Sie haben die Variable zunächst geschützt, so daß Änderungen Ihrer Darstellung die Verwendung Ihrer Klasse durch andere nicht beeinflussen würden. Sie sollten denselben Schutz nutzen. Wenn Sie jetzt die Darstellung von aUsefulString ändern wollen, müssen Sie nicht jede Verwendung dieser Variablen in Ihrer Klasse ändern (wie das der Fall wäre, wenn Sie nicht die Zugriffsmethoden verwenden). Statt dessen betrifft die Änderung nur die Implementierungen der Zugriffsmethoden für die Variable.

Einer der besten Nebeneffekte bei dieser Art des Zugriffs auf Ihre eigenen Instanzvariablen ist, daß wenn Sie später Code brauchen, der immer dann ausgeführt wird, wenn ein Zugriff auf aUsefulString erfolgt, dieser an einer Stelle abgelegt werden kann, und alle anderen Methoden in Ihrer Klasse (und in allen anderen Klassen) rufen diesen Code korrekt auf. Hier ein Beispiel:

private protected void setAUsefulString(String aStr) {
aUsefulString = aStr;
performSomeImportantBookeeping();
}

Es scheint relativ aufwendig zu sein, Zugriffsmethoden aufzurufen, statt die Instanzvariablen direkt in Ihrem Code zu verwenden, aber diese kleine Mühe wird später belohnt durch Wiederverwendbarkeit und einfachere Wartung.

Schutz von Variablen und Methoden

Was ist, wenn Sie eine gemeinsam genutzte Variable erzeugen wollen, die alle Ihre Instanzen sehen und verwenden können? Wenn Sie eine Instanzvariable verwenden, besitzt jede Instanz eine eigene Kopie dieser Variablen, das erfüllt die Aufgabe also nicht. Wenn Sie sie jedoch in der Klasse selbst ablegen, gibt es nur eine Kopie und alle Instanzen der Klasse nutzen sie gemeinsam. Sie wird dann als Klassenvariable bezeichnet.


Eine Klassenvariable gehört allen Instanzen einer Klasse gleichzeitig und ihr Wert ist in der Klasse abgelegt.

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


Aufgrund seiner historischen Verbindungen zu C/C++ verwendet Java das Wort static, um Klassenvariablen und -methoden zu deklarieren. Wenn Sie das Schlüsselwort static sehen, könnten Sie es in Gedanken durch das Wort »Klasse« ersetzen.

Instanzen können auf ihre eigenen Klassenvariablen zugreifen, als handelte es sich um Instanzvariablen, wie im letzten Beispiel gezeigt. Weil pi als public deklariert ist, können auch Methoden aus anderen Klassen darauf zugreifen:

float circumference = 2 * Circle.pi * r;


Instanzen einer Klasse können auch die Form instanzName.klassenVarName verwenden, um auf eine Klassenvariable zuzugreifen. In den meisten Fällen wird jedoch die Form klassenName.klassenVarName bevorzugt, weil sie deutlich kennzeichnet, daß es sich um eine Klassenvariable handelt. Außerdem erkennt der Leser dann sofort, daß die Variable global für alle Instanzen ist.

Klassenmethoden werden analog definiert. Der Zugriff erfolgt ebenso durch Instanzen ihrer Klasse, aber andere Klassen können nur über ihren vollständigen Klassennamen darauf zugreifen (was der bevorzugte Stil ist, wie im obigen Hinweis erklärt). Hier folgt eine Klasse, die Klassenmethoden definiert, welche helfen, ihre eigenen Instanzen zu zählen:

public class InstanceCounter {
private static int instanceCount = 0; // eine Klassenvariable
private protected static int getInstanceCount() {
return instanceCount; // eine Klassenmethode
}
private static void incrementCount() { // eine Klassenmethode
++instanceCount;
}
InstanceCounter() { // der Klassenkonstruktor
InstanceCounter.incrementCount();
}
}

In diesem Beispiel ruft eine explizite Verendung des Klassennamens die Methode incrementCount() auf. Das scheint überflüssig zu sein, aber in einem größeren Programm erkennt der Leser daran sofort, welches Objekt (die Klasse und nicht die Instanz) die Methode verarbeiten soll. Das ist insbesondere praktisch, wenn der Leser herausfinden will, wo diese Methode in einer größeren Klasse deklariert ist, die alle ihre Klassenmethoden oben plaziert (übrigens die empfohlene Vorgehensweise).

Beachten Sie, daß instanceCount mit 0 initialisiert wurde. Wie eine Instanzvariable beim Erzeugen der Instanz zur Laufzeit initialisiert wird, wird eine Klassenvariable initialisiert, wenn ihre Klasse bei der Kompilierung erzeugt wird. Diese Klasseninitialisierung passiert, bevor irgend etwas anderes für die Klasse oder ihre Instanzen passieren kann, die Klasse in dem Beispiel funktioniert also wie erwartet.

Die Konventionen, die Sie für den Zugriff auf eine Instanzvariable kennengelernt haben, werden in diesem Beispiel angewendet, um auf eine Klassenvariable zuzugreifen. Die Zugriffsmethoden sind deshalb Klassenmethoden. (Es gibt hier keine Methode zum Setzen, sondern nur zum Ermitteln und zum Inkrementieren, weil niemand instanceCount direkt setzen darf.) Beachten Sie außerdem, daß nur Unterklassen instanceCount abfragen kann, weil es sich hier um ein relativ intimes Detail handelt. Hier ein Test von InstanceCounter:

public class InstanceCounterTester extends InstanceCounter {
public static void main(String args[]) {
for (int i = 0; i < 10; ++1)
new InstanceCounter();
System.out.println("Zähler" + InstanceCounter.getInstanceCount());
}
}

Es ist nicht überraschend, daß das Beispiel die folgende Ausgabe erzeut:

Zähler 10

final

Der Modifier final ist sehr vielseitig und hat unterschiedliche Auswirkungen:

final-Klassen

Die Deklaration einer final-Klasse sieht wie folgt aus:

public final class AFinalClass {...}

Eine Klasse wird aus zwei Gründen als final deklariert. Der erste Grund ist die Sicherheit. Sie wollen, da die Instanzen der Klasse unverändert verwendet werden, und niemand soll Unterklassen oder neue und unterschiedliche Instanzen der Klasse anlegen können. Der zweite Grund ist die Effizienz. Sie wollen, daß nur Instanzen dieser einen Klasse (und keiner Unterklassen) im System vorhanden sind, so daß Sie die Klasse optimieren können.


Die Java-Klassenbibliothek verwendet zahlreiche final-Klassen. Einige Beispiele für den ersten Grund (Sicherheit) für die Verwendung von final sind die Klassen java.lang.System, java.net.InetAddress und java.net.Socket. Ein gutes Beispiel für den zweiten Grund (Effizienz) ist java.lang.String. Strings sind so gebräuchlich in Java und so zentral dafür, daß die Laufzeitumgebung sie gesondert verarbeitet.

Nur selten müssen Sie final-Klassen selbst erzeugen, auch wenn Sie häufig darüber stöhnen, daß Systemklassen final sind (wodurch ihre Erweiterung ziemlich schwierig wird). Aber das ist der Preis für Sicherheit und Effizienz.

final-Variablen

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

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

Beachten Sie, daß die erste Konstante eine öffentliche Klassenkonstante ist (gekennzeichnet durch den Modifier static), bei der zweiten handelt es sich einfach um eine öffentliche Konstante.

Die final-Klassen und -Instanzvariablen können in Ausdrücken wie ganz normale Klassen und Instanzvariablen verwendet werden, aber sie können nicht verändert werden. final-Variablen müssen also ihren (konstanten) Wert bei der Deklaration erhalten, wie im obigen Beispiel gezeigt. Klassen können anderen Klassen sinnvolle Konstanten bereitstellen, indem sie Klassenvariablen verwenden, wie etwa aConstantInt im vorigen Beispiel. Andere Klassen greifen wie gewohnt darauf zu: AnotherFinalClass.aConstantInt.

Lokale Variablen (innerhalb von Codeblöcken, die in geschweifte Klammern eingeschlossen sind - z.B. in while- oder for-Schleifen) können nicht als final deklariert werden. Lokale Variablen werden ganz ohne Modifier spezifiziert:

{
int aLocalVariable; // Ich bin so allein ohne meine Modifier...
...
}

final-Methoden

Hier ein Beispiel für die Verwendung der final-Methoden:

public class MyPenultimateFinalClass {
public static final void aClassMethodThatCannotBeOverridden() {
...
}

public final void aRegularMethodThatCannotBeOverridden() {
...
}
}

Diese final-Methoden können von Unterklassen nicht überschrieben werden. Es ist ganz selten, daß eine Methode wirklich das letzte Wort in ihrer eigenen Implementierung sein soll, warum also wendet man diesen Modifier auf Methoden an?

Die Antwort lautet: Effizient. Wenn Sie eine Methode als final deklarieren, kann der Compiler in die Methoden, die sie aufrufen einkopieren, weil er weiß, daß niemand jemals eine Unterklasse anlegen oder sie überschreiben und ihr Verhalten ändern kann. Bei Ihrer anfänglichen Arbeit mit Klassen werden Sie das Schlüsselwort final vielleicht noch nicht gebrauchen, aber wenn Sie Ihr System später optimieren, werden Sie feststellen, daß Ihre Klassen sehr viel schneller werden, wenn Sie final einsetzen. Fast alle Ihre Methoden sind jedoch gut so wie sie sind.

Die Java-Klassenbibliothek deklariert zahlreiche häufig genutzte Methoden also final, so daß Sie den Geschwindigkeitsvorteil nutzen können, was für diesen teilweise kompilierten, teilweise interpretierten Teil der Sprache wesentlich ist. Bei Klassen, die bereits final sind, ist das durchaus sinnvoll. Die wenigen final-Methoden, die in nicht-final-Klassen deklariert sind, werden Sie zweifellos stören - Ihre Unterklassen können sie nicht überschreiben. Wenn die Effizienz in der Java-Umgebung weniger wichtig ist, können viele dieser final-Methoden zu nicht-final-Methoden gemacht werden, so daß das System flexibler wird.


Methoden, die als private deklariert sind, sind eigentlich final, weil sie in einer Unterklasse nicht überschrieben werden können. Ebenso ist es mit allen Methoden, die in einer final-Klasse deklariert sind, weil davon keine Unterklasse angelegt werden können. Es ist erlaubt, diese Methoden als final zu deklarieren (wie es in der Java-Bibliothek auch manchmal der Fall ist), aber es wäre redundant. Der Compiler behandelt sie schon als final.

Es ist möglich, aus denselben Sicherheitsgründen wie bei final-Klassen auch final-Methoden einzusetzen, aber das passiert sehr viel seltener.

Wenn Sie viele Zugriffsmethoden einsetzen (wie empfohlen), und wenn Sie sich Gedanken um die Effizienz machen, betrachten Sie die folgende, viel schnellere Version von ACorrektClass:

public class ACorrectFinalClass {
private String aUsefulString;
public final String getAUsefulString() {
return aUsefulString; // jetzt schneller
}
private protected final void setAUsefulString(String aStr) {
aUsefulString = aStr; // jetzt auch schneller
}
}

Es kann sein, daß zukünftige Implementierungen von Java intelligent genug sind, einfache Methoden automatisch einzukopieren, aber noch kann das mit dem Schlüsselwort final veranlaßt werden.

abstract-Methoden und Klassen

Immer wenn Sie Klassen in einer Vererbungshierarchie anordnen, geht man dabei davon aus, daß Klassen auf höherer Ebene abstrakter und allgemeiner gehalten sind, während Unterklassen auf einer unteren Ebene konkreter und spezifischer sind. Häufig werden gemeinsame Aspekte von Klassen extrahiert und als gemeinsame Oberklasse angelegt. Wenn der Hauptgrund für die Existenz einer Oberklasse ist, als gemeinsam genutztes Behältnis zu dienen, und wenn nur ihre Unterklassen direkt verwendet werden sollen, wird diese Oberklasse auch als abstrakte Klasse bezeichnet.

Klassen, die abstrakt sind, können keine Instanzen erzeugen. Sie enthalten alles, was eine normale Klasse enthalten kann, und können darüber hinaus jeder ihrer Methoden den Modifier abstract als Präfix voranstellen. Nicht-abstrakte Klassen können diesen Modifier für ihre Klassenelemente nicht verwenden. Wenn er nur einmal für Ihre Methoden genutzt wird, müßten Sie die gesamte Klasse als abstrakt deklarieren. Hier ein Beispiel:

public abstract class MyFirstAbstractClass {
int anInstanceVariable;
public abstract int aMethodNonAbstractSubclassesMustImplement();
public void doSomething() {
... // eine normale Methode
}
}
public class AConcreteSubclass extends MyFirstAbstractClass {
public int aMethodNonAbstractSubclassesMustImplement(); {
...
/* Diese Unterklasse *muß* diese Methode implementieren,
damit wir sie in dieser Unterklasse nutzen können */
...
}
}

Hier zwei Versuche, diese Klassen zu verwenden:

Object a = new MyFirstAbstractClass(); // illegal, abstrakte Klasse

Object c = new AConcreteSubclass(); // legal, konkrete Klasse

Beachten Sie, daß abstract-Methoden keine Implementierung brauchen; nicht-abstrakte Unterklassen stellen eine Implementierung bereit. Die Klasse abstract stellt einfach nur die Schablone für die Methoden bereit (durch Definition der Methodensignatur), die durch andere Unterklassen später implementiert werden. in der Java-Klassenbibliothek haben mehrere abstrakte Klassen keine dokumentierten Unterklassen im System, sondern stellen einfach nur eine Basis bereit, von der aus Sie Unterklassen in Ihren eigenen Programmen anlegen können. Schnittstellenklassen sind ein gutes Beispiel dafür.

Die Verwendung einer abstrakte Klasse, um nur einen Entwurf zu präsentieren - d.h. nur abstrakte Methoden - wird in Java besser durch die Verwendung einer Schnittstelle bewerkstelligt. Immer wenn ein Entwurf jedoch eine Abstraktion erforderlich macht, in der Instanzvariablen oder eine teilweise Implementierung angegeben werden müssen, ist die Verwendung einer abstrakten Klasse die einzige sinnvolle Lösung. In früheren objektorientierten Sprachen waren abstrakte Klassen eine Konvention. Sie erwiesen sich als so wertvoll, daß Java sie nicht nur in der hier beschriebenen Form unterstützt, sondern auch in der reineren Form der Schnittstellen, die Sie heute noch kennenlernen werden.

Pakete

Pakete bieten die Möglichkeit, bestimmte Klassen und Schnittstellen zu kombinieren, so daß man sie als Gruppe ansprechen oder importieren kann.


Ein Paket ist eine Sammlung miteinander verwandter Klassen und Schnittstellen.

Die Gruppierung von Klassen und Schnittstellen in Paketen eliminiert außerdem mögliche Konflikte in Hinblick auf Klassennamen, und Sie können Paketnamen nutzen, um einen Klassennamen vollständig zu qualifizieren, so daß genau ersichtlich wird, auf welche Klasse Sie zugreifen. Pakete können mehrere Quellcodedateien enthalten, die jedoch den Paketnamen beinhalten müssen. Pakete helfen außerdem, den Zugriff und die Sicherheit zu steuern, wie Sie heute bereits erfahren haben.

Pakete können in anderen Paketen verschachtelt werden, so daß sie weiter strukturiert werden. Der voll qualifizierte Klassenname java.awt.Color beispielsweise zeigt an, daß die Klasse Color im awt-Paket (Abstract Windowing Toolkit) enthalten ist, das wiederum im java-Paket liegt.

Entwurf von Paketen

Wenn Sie Java-Programme entwickeln, die viele Klassen verwenden, werden Sie schnell die Einschränkungen des Modells erfahren, das bisher für den Entwurf und die Realisierung eingesetzt wurde.

Und wenn die Anzahl der Klassen, die Sie erzeugen, steigt, ist es sehr wahrscheinlich, daß Sie die kurzen, einfachen Namen bestimmter Klassen mehrere Male verwenden wollen. Wenn Sie Ihre Klassen vor längerer Zeit entwickelt haben, oder jemand anderer hat sie für Sie entwickelt (wie etwa die Klassen in der Java-Bibliothek), dann haben wissen Sie vielleicht nicht mehr - oder Sie haben es noch nie gewußt -, daß diese Klassennamen einen Konflikt erzeugen. Dann wird die Möglichkeit, eine Klasse in einem Paket zu »verbergen«, praktisch.

Hier folgt ein einfaches Beispiel, wie in einer Java-Quelldatei ein Paket erzeugt wird:

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


Wenn in einer Java-Quelldatei die Anweisung package erscheint, muß sie ganz vorne in dieser Datei stehen (außer Kommentaren und Whitespaces).

Zuerst deklarieren Sie in einer package-Anweisung den Namen des Pakets, und anschließend definieren Sie eine Klasse, wie gewohnt. Diese Klasse und alle anderen Klassen mit demselben Paketnamen werden gruppiert. (Diese anderen Klassen befinden sich in der Regel in anderen, separaten Quelldateien und haben identische package-Anweisungen.)

Pakete können noch weiter strukturiert werden, in eine Hierarchie, die der Vererbungshierarchie ähnlich ist, und wo jede Ebene jeweils eine kleinere, spezifischer Gruppierung darstellt. Die Java-Klassenbibliothek ist auf diese Weise aufgebaut. Die oberste Ebene heißt java; die nächste Ebene beinhaltet Namen wie io, net, util und awt. awt hat noch eine tiefere Ebene mit dem Paket image. Die Klasse ColorModel, die sich im Paket image befindet, kann überall in Ihrem Java-Code als java.awt.image.ColorModel angesprochen werden.


Der aktuellen Konvention gemäß spezifiziert die erste Ebene der Hierarchie den global eindeutigen Namen der Firma, die das Java-Paket bzw. die Pakete entwickelt hat. Beispielsweise beginnen die Klassen von Sun Microsystems, die nicht Teil der Standard-Java-Umgebung sind, alle mit dem Präfix sun, die Klassen von Borland beginnen mit dem Präfix borland. Das Standardpaket, java, stellt eine Ausnahme dar, weil es so grundlegend ist, und weil es eines Tages von mehreren Firmen implementiert werden könnte.

Sun hat eine formalere Prozedur für die Namensgebung von Paketen vorgeschlagen, die in Zukunft befolgt werden soll. Der Name der obersten Domeins des Internet sollte dabei in Großbuchstaben angegeben werden (EDU, COM, GOV, ORG, FR, US, RU usw.), gefolgt vom Internet-Domainnamen. Durch diese Prozedur erhielten die Sun-Pakete das Präfix COM.sun, die Borland-Pakete das Präfix COM.borland. (Beachten Sie jedoch, daß weder Sun noch Borland diese Vorgehensweise momentan befolgen.)

Der Gedanke dabei ist, den Paketnamen immer weiter zu ergänzen, je weiter man in der internen Hierarchie der Firma nach unten geht, beispielsweise EDU.harvard.cs.projects.ai.myPackage. Weil Domainnamen bereits garantiert global eindeutig sind, wäre das eine elegante Lösung für ein drohendes Problem, und darüber hinaus würden die Applets und Pakete von möglicherweise Millionen von Java-Programmierern automatisch in einer wachsenden Hierarchie unterhalb Ihres Klassenverzeichnisses gespeichert, so daß Sie eine Möglichkeit hätten, sie gezielt zu suchen und einzuordnen.

Weil Java-Klassen normalerweise in separaten Quelldateien abgelegt sind, ist die Gruppierung der Klassen durch eine Pakethierarchie analog zur Gruppierung der Dateien in einer Verzeichnishierarchie in Ihrem Dateisystem. Der Java-Compiler erzwingt diese Analogie, indem er Sie auffordert, unter Ihrem myclasses-Verzeichnis eine Verzeichnishierarchie anzulegen, die genau mit der von Ihnen erzeugten Pakethierarchie übereinstimmt, und eine Klasse jeweils in dem Verzeichnis mit dem Namen und derselben Ebene wie das Paket, in dem sie definiert ist abzulegen.

In der JBuilder-Installation ist die Verzeichnishierarchie für die Java-Klassenbibliothek etwas anders angeordnet. Beispielsweise wird unter Windows 95 die Klasse java.awt.image.ColorModel in der Datei ColorModel.class gespeichert, die in der komprimierten Datei classes.zip im Unterverzeichnis JBuilder\java\lib abgelegt ist. Wenn Sie jedoch classes.zip betrachten, sehen Sie, daß der Pfad für ColorModel.class als \java\awt\image angegeben ist, was den Paketnamen widerspiegelt.

Wenn Sie in myFirstPackage ein Paket mit dem Namen mySecondPackage erzeugt haben, indem Sie eine Klasse deklarierten:

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

dann muß sich die Java-Quelldatei (AnotherPublicClass.java) im JBuilder-Unterverzeichnis myprojects\myFirstPackage\mySecondPackage befinden. Wenn Sie die Datei kompilieren, wird AnotherPublicClass.class im JBuilder-Unterverzeichnis myclasses\myFirstPackage\mySecondPackage plaziert, so daß der Java-Interpreter sie findet.

Das erste Beispiel dieser Lektion, APublicClass, würde in myprojects\myFistPackage abgelegt, und die Datei APublicClass.class im Unterverzeichnis myclasses\myFirstPackage. Java-basierte Compiler und Interpreter erwarten und erzwingen diese Hierarchie. Aber was passiert, wenn Klassen, wie in früheren Beispielen in diesem Buch, ohne die Anweisung package definiert werden?

Wenn keine explizite package-Anweisung in der Quelldatei der Klasse angegeben ist, plaziert der Compiler sie in einem Standardpaket ohne Namen, und die .java- und .class-Dateien werden auf der obersten Ebene der JBuilder-Unterverzeichnisse myprojects bzw. myclasses abgelegt.


Jedes JBuilder-Projekt kann den in der IDE gesetzten CLASSPATH überschreiben, indem es einen anderen Pfad angibt. Dazu wählen Sie Datei | Projekteigenschaften. Wenn der Eigenschaftendialog für ProjectName.jpr erscheint, tragen Sie in das Feld für den Klassenpfad den gewünschten Pfad ein und klicken auf OK. Wenn Ihr Projekt die Projektdateien nicht findet, ist dies die erste Stelle, an der Sie nach einem fehlerhaften Klassenpfad suchen könnten, weil sie Priorität gegenüber der CLASSPATH-Einstellung der IDE hat.

Pakete implementieren

Wenn Sie in Ihrem Java-Code auf eine Klasse durch Angabe ihres Namens verweisen, verwenden Sie ein Paket. Größtenteils sind Sie sich dessen nicht bewußt, weil viele der gebräuchlichen Klassen im System sich in einem Paket befinden, das der Java-Compiler für Sie automatisch importiert: im Paket java.lang. Immer, wenn Sie beispielsweise folgendes gesehen haben:

String aString;

ist etwas viel Interessanteres passiert, als Sie sich vielleicht gedacht haben. Was passiert, wenn Sie die Klasse, die Sie am Anfang dieses Abschnitts erzeugt haben, die im Paket myFirstPackage abgelegt ist, zugreifen wollen? Versuchen Sie folgendes:

MyPublicClass someName;

Der Compiler wird sich beschweren. Die Klasse MyPublicClass ist im Paket java.lang nicht definiert. Um das Problem zu lösen, erlaubt Java, daß Klassennamen die Paketnamen vorangestellt sind, in denen sie definiert sind, um so einen vollständigen Verweis auf die Klasse anzugeben:

myFirstPackage.MyPublicClass someName;


nventionsgemäß beginnen Paketnamen mit einem Kleinbuchstaben, so daß sie in vollständigen Klassenverweisen einfach von Klassennamen unterschieden werden können. In dem voll qualifizierten Namen der String-Klasse java.lang.String beispielsweise, ist es durch diese Konvention einfacher, den Paketnamen vom Klassennamen visuell zu unterscheiden. Weil Java die Groß-/Kleinschreibung berücksichtigt, können dadurch potentielle Namenskonflikte zwischen Paketnamen und Klassennamen vermieden werden.

Angenommen, Sie wollen sehr viele Klassen aus einem Paket verwenden, ein Paket mit einem sehr langen Namen, oder beides. In so einem Fall wollen Sie nicht immer mit that.really.long.package.name.ClassName auf Ihre Klasse zugreifen, wenn Sie sie brauchen. Java ermöglicht Ihnen, die Namen dieser Klassen in Ihr Programm zu importieren. Diese Klassen verhalten sich dann genau wie java.lang-Klassen, und Sie können ohne die voll qualifizierten auf sie zugreifen. Um beispielsweise diesen wirklich langen Klassennamen einfacher verwenden zu können, würden Sie das folgende tun:

import that.really.long.package.name.ClassName;

ClassName anObject; // das ist doch schon viel besser!

Jetzt können Sie direkt auf ClassName zugreifen, und zwar so oft Sie möchten. Alle import-Anweisung müssen unmittelbar hinter einer package-Anweisung stehen, aber vor den Klassendefinitionen, also relativ weit oben in Ihren Quellcode-Dateien.

Was passiert, wenn Sie mehrere Klassen aus demselben Paket verwenden wollen? Hier ein Versuch von einem Programmierer, der sicher bald müde sein wird:

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

Und hier der Versuch eines ausgebufften Programmierers, der weiß, wie man ein ganzes Paket mit public-Klassen importiert:

import that.really.long.package.name.*;

ClassOne first;
ClassTwo second;
ClassThree andSoOn;


Der Stern (*) in diesem Beispiel ist nicht derselbe, wie der, den Sie an einem Befehlsprompt verwenden, um eine Dateigruppe zu spezifizieren. Der Stern in einer import-Anweisung teilt Java mit, daß Sie alle public-Klassen aus einem bestimmten Paket importieren wollen.

Der Stern importiert jedoch nicht alle Unterpakete des angegebenen Pakets. Beispielsweise importiert import java.awt.* alle public-Klassen aus dem Paket java.awt, wie etwa java.awt.Font und java.awt.Graphics. Es importiert jedoch nicht die Unterpakete java.awt.image oder java.awt.peer (oder ihre public-Klassen).

Um alle public-Klassen eines Pakets und seine Unterpakete zu importieren, müssen Sie für jedes Paket und Unterpaket auf der jeweiligen Pakethierarchie eine separate import-Anweisung angeben.

Wenn Sie eine Klasse oder ein Paket nur wenige Male in Ihrer Quelldatei einsetzen wollen, dann lohnt es sich möglicherweise nicht, sie bzw. es zu importieren. Sie müssen sich fragen, überwiegt die Forderung nach Klarheit die Bequemlichkeit, weniger Zeichen eingeben zu müssen? Wenn das der Fall ist, dann verwenden Sie import nicht. Verwenden Sie statt dessen den voll qualifizierten Klassennamen. Sie wissen, daß der voll qualifizierte Klassenname den Paketnamen enthält, so daß der Programmierer sofort erkennt, wo er weitere Informationen über diese Klasse findet, und nicht die import-Anweisung auswerten muß. Und wenn es mehrere import-Anweisungen gibt, ist unklar, zu welchem importierten Paket der abgekürzte Klassenname gehört.

Was passiert, wenn Sie das folgende in der Quelldatei von ClassA stehen haben:

package Motorcycle;
public class startEngine {...}
public class ClassA {...}

Und folgendes in der Quelldatei von ClassB:

package Car;
public class startEngine {...}
public class ClassB {...}

An irgendeiner anderen Stelle könnten Sie beispielsweise das schreiben:

import Motorcycle.*;
import Car.*;

startEngine YamahaObject; // Compiler-Fehler
startEngine CrownVictoriaObject; // ich auch!

Sie fragen sich jetzt vielleicht, welche startEngine war gemeint? Das sollten Sie auch. Es gibt zwei mögliche Interpretationen, welche Klasse Sie meinen könnten; eine in Motorcycle und eine in Car. Weil das zweideutig ist, was soll der arme Compiler dann tun? Er erzeugt natürlich einen Compiler-Fehler, und Sie müssen genauer angeben, welche Klasse Sie wünschen. Hier ein Beispiel, wie das Problem gelöst werden könnte:

import Motorcycle.*;
import Car.*;

Motorcycle.startEngine YamahaObject; // korrigiert
Car.startEngine CrownVictoriaObject; // korrigiert


Sie wundern sich vielleicht über die zahlreichen Deklarationen, die in den Beispielen der heutigen Lektion auftauchen. Deklarationen sind gute Beispiele, weil sei die einfachste Möglichkeit darstellen, auf einen Klassennamen zu verweisen. Jede Verwendung eines Klassennamens - beispielsweise in Ihrer extends-Klausel oder in new startEngine() - folgt denselben Regeln.

Klassen verbergen

Der scharfsinnige Beobachter hat vielleicht bemerkt, daß in der Beschreibung des Importierens mit einem Stern (*) erwähnt wurde, daß ein ganzes Paket mit public-Klassen importiert wird. Warum sollten Sie Klassen einer anderen Art importieren wollen? Betrachten Sie das folgende:

package collections;

public class LinkedList {
private Node root;

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


Wenn sich das alles in einer Klasse befindet, verletzen Sie eine der Compiler-Konventionen: Jede Java-Quellcodedatei sollte nur eine Klasse enthalten. (Deshalb fragt der Compiler sich, wie er die .class-Datei benennen soll.) Der Compiler kümmert sich nur darum, daß jede public-Klasse in einer separaten Datei steht. Dennoch sollten Sie sich angewöhnen, wirklich für jede Klasse eine eigene Datei anzulegen.

Das Ziel der Klasse LinkedList ist, anderen Klassen praktische public-Methodeen bereitzustellen (wie etwa add()). Es ist unwichtig für die anderen Klassen, ob LinkedList noch andere Klassen benötigt, um seine Arbeit zu erledigen. Darüber hinaus denkt LinkedList vielleicht, daß die Klasse Node lokale für seine Implementierung ist und nicht von anderen Klassen eingesehen werden sollte.

Für Methoden und Variablen könnte dies durch die heute beschriebenen Zugriffs-Modifier realisiert werden: private, protected private, protected, package (Standard) und public. Sie haben bereits viele public-Klassen kennengelernt, und weil private und protected wirklich nur dann Sinn machen, wenn sie innerhalb einer Klassendefinition verwendet werden, können Sie sie nicht außerhalb als Teil der Definition einer neuen Klasse verwenden. LinkedList muß vielleicht nur für seine Quelldatei sichtbar sein, aber weil sich jede Klasse konventionsgemäß in einer separaten Quelldatei befindet, wäre das ein allzu enger Ansatz.

Statt dessen deklariert LinkedList keine Zugriffs-Modifier, so daß es dieselben Rechte hat, als wäre es als package deklariert. Jetzt kann die Klasse nur von anderen Klassen aus dem Paket, in dem sie definiert wurde, gesehen und verwendet werden. In diesem Fall handelt es sich um das collections-Paket. Sie könnten LinkedList wie folgt einsetzen:

import collections.*; // importiert nur public-Klassen

LinkedList aLinkedList;
/* Node n; */ // würde einen Compiler-Fehler erzeugen
aLinkedList.add("THX-");
aLinkedList.add(new Integer(1138));
...

Sie können auch unter Verwendung von collections.LinkedList eine Instanz von aLinkedList erzeugen. Weil die public-Klasse LinkedList sich auf die package-Klasse Node bezieht, wird diese Klasse automatisch geladen und verwendet, und der Compiler stellt sicher, daß LinkedList (als Teil des collections-Pakets) das Recht hat, die Node-Klasse zu erzeugen und zu verwenden. Sie hätten dieses Recht jedoch immer noch nicht, wie im vorigen Beispiel gezeigt.

Eine der Stärken verborgener Klassen ist, daß Sie eine wesentliche Komplexität in die Implementierung einer public-Klasse einbringen können, die aber vollständig verborgen wird, wenn diese Klasse importiert oder verwendet wird. Um ein gutes Paket zu erzeugen, müssen Sie also eine kleine, saubere Menge öffentlicher Klassen und Methoden für andere Klassen definieren, die Sie dann unter Verwendung einer beliebiger Anzahl verborgener (package) Hilfs-Klassen implementieren.

Sie werden heute noch einen anderen Verwendungszweck für verborgene Klassen kennenlernen. Jetzt wollen wir sie aber vergessen und uns den Schnittstellen zuwenden.

Schnittstellen

Sie wissen, daß Java-Klassen nur eine einzige Oberklasse haben können, und daß sie die Variablen und Methoden dieser Oberklasse sowie aller darüber liegenden Oberklassen erben. Die Einfachvererbung macht zwar die Beziehung zwischen Klassen und die Funktionalität, die diese Klassen implementieren, leichter nachvollziehbar und zu entwerfen, aber sie kann auch einschränkend wirken. Das trifft insbesondere dann zu, wenn Sie ähnliche Verhalten haben, die über verschiedene Zweige der Klassenhierarchie dupliziert werden müssen. Java löst dieses Problem gemeinsam genutzten Verhaltens durch das Konzept der Schnittstellen.


Eine Schnittstelle besteht aus mehreren Methodendeklarationen ohne die eigentlichen Implementierungen.

Obwohl jede Java-Klasse aufgrund der Einfachvererbung nur eine einzige Oberklasse haben kann, kann sie beliebig viele Schnittstellen implementieren. Dadurch stellt die Klasse Methodenimplementierungen (Definitionen) für die in der Schnittstelle deklarierten Methodensignaturen bereit. Wenn zwei verschiedene Klassen dieselbe Schnittstelle implementieren, können sie auf dieselben Methodenaufrufe reagieren, aber das, was sie dafür ausführen, kann sehr unterschiedlich sein.

Schnittstellen bieten, wie die abstrakten Klassen oder Methoden, die Sie heute bereits kennengelernt haben, Verhaltensschablonen, die andere Klassen implementieren sollen, aber Schnittstellen sind sehr viel leistungsfähiger als abstrakte Klassen. Nun wollen wir untersuchen, wozu Sie diese Möglichkeit einsetzen könnten.

Der Entwurf von Schnittstellen

Wenn Sie beginnen, objektorientierte Programme zu entwickeln, wird Ihnen die Klassenhierarchie undurchschaubar vorkommen. Innerhalb dieses einzigen Baums können Sie eine Hierarchie numerischer Typen, viele einfache bis hin zu relativ komplexen Beziehungen zwischen Objekten und Prozessen und beliebig viele Punkte entlang der Linie von abstrakt/allgemein hin zu konkret/spezifisch ausdrücken. Nachdem Sie länger darüber nachgedacht und Erfahrung gesammelt haben beginnt dieser wunderbare Baum, plötzlich sehr restriktiv zu werden - manchmal wie eine Zwangsjacke. Die eigentliche Leistungsfähigkeit und Disziplin, die sie realisiert haben, indem Sie sorgfältig eine Kopie jeder Idee irgendwo im Baum plaziert haben, wird Sie plötzlich verfolgen, wenn Sie verschiedene Teile dieses Baums miteinander kreuzen wollen.

Einige Sprachen lösen dieses Problem, indem sie eine flexiblere Laufzeitumgebung bereitstellen, wie etwa den Codeblock und die perform:-Methode von Smalltalk. Andere stellen kompliziertere Vererbungshierarchien zur Verfügung, etwa die Mehrfachvererbung. Weil die Mehrfachvererbung aber zu vielen verwirrenden und fehleranfälligen Mehrdeutigkeiten und Mißverständnissen führt, und weil die Problemlösung aus Smalltalk es schwierig macht, Sicherheit zu implementieren und die Sprache verkompliziert, hat Java keine dieser Möglichkeiten übernommen, sondern verwendet statt dessen eine völlig separate Hierarchie, um die Ausdruckskraft zu realisieren, die benötigt wird, um die Zwangsjacke zu lockern.

Diese neue Hierarchie ist eine Schnittstellenhierarchie. Schnittstellen sind nicht auf eine einzelne Ober-Schnittstelle eingeschränkt, deshalb erlauben sie eine Art Mehrfachvererbung. Aber sie geben nur Methodenbeschreibungen nur an ihre Kinder weiter, keine Methodenimplementierungen oder Instanzvariablen, was auch hilft, viele der Probleme einer vollen Mehrfachvererbung zu eliminieren.

Schnittstellen werden wie Klassen in Quelldateien deklariert, jeweils eine Schnittstelle in einer Datei. Wie Klassen werden sie auch in .class-Dateien kompiliert. Fast überall in diesem Buch können Sie dort, wo ein Klassenname in einem Beispiel steht, auch einen Schnittstellennamen einsetzen. Java-Programmierer sagen oft »Klasse«, wenn sie eigentlich »Klasse oder Schnittstelle« meinen. Schnittstellen ergänzen und erweitern die Leistungsfähigkeit von Klassen, und beide können in etwa gleich behandelt werden. Einer der wenigen Unterschiede zwischen ihnen ist, daß eine Schnittstelle nicht instantiiert werden kann: new kann nur eine Instanz einer nicht-abstrakten Klasse erzeugen. Hier folgt die Deklaration einer Schnittstelle:

package myFirstPackage;

public interface MyFirstInterface extends Interface1, Interface2, ... {
...
// alle Methoden hier sind public und abstract, und alle
// Variablen sind public, static und final
...
}

Dieses Beispiel ist eine neue Version des ersten Beispiels der heutigen Lektion. Es fügt dem Paket myFirstPackage eine öffentliche Schnittstelle hinzu, statt einer öffentlichen Klasse. Beachten Sie, daß in der extends-Klausel einer Schnittstelle mehrere Eltern aufgelistet werden können.


Wenn keine extends-Klausel angegeben ist, erben Schnittstellen nicht standardmäßig von Object, weil es sich bei Object um eine Klasse handelt, und Schnittstellen nur andere Schnittstellen erweitern können. Schnittstellen haben keine oberste Schnittstelle, von der sie garantiert alle abgeleitet sind. Wenn es also keine extends-Klausel gibt, wird die Schnittstelle zu einer oberen Schnittstelle (möglicherweise eine von vielen).

Alle Variablen oder Methoden, die in einer public interface definiert sind, verwenden implizit die Modifier, die im Kommentar des obigen Beispiels aufgelistet sind (d.h. public abstract für Methoden und public static final für Variablen). Genau diese Modifier dürfen (optional) erscheinen, aber keine anderen:

public interface MySecondInterface {
public static final int theAnswer = 42; // OK
public abstract int lifeTheUniverseAndEverything(); // OK
long theWordCounter = 0; // OK, wird public static final
long ageOfTheUniverse(); // OK, wird public abstract
private protected int aConstant; // nicht OK
private int getAnInt(); // nicht OK
}

Wenn eine Schnittstelle nicht als public deklariert ist (mit anderen Worten, wenn es sich um ein Paket handelt), werden die public-Modifier nicht implizit angenommen. Wenn Sie innerhalb einer solchen Schnittstelle public sagen, bewirken Sie eine echte Öffentlichkeit, nicht nur eine redundante Anweisung. Es kommt jedoch nur selten vor, daß eine Schnittstelle nur von den Klassen innerhalb eines einzigen Pakets verwendet wird, und nicht von den Klassen, die dieses Paket verwenden.

Entwurf und Implementierung

Einer der leistungsfähigsten Aspekte, die die Schnittstellen in Java einführen, ist die Möglichkeit, die Entwurfs-Vererbung von der Implementierungs-Vererbung zu trennen. Im Klassenbaum mit Einfachvererbung sind die beiden nicht zu trennen. Manchmal wollen Sie jedoch eine Schnittstelle für eine Klasse mit Objekten abstrakt beschreiben, ohne eine Implementierung dafür bereitstellen zu müssen. Sie könnten eine abstrakte Klasse einführen. Damit eine neue Klasse diese Art »Schnittstelle« verwenden kann, muß sie jedoch eine Oberklasse der abstrakten Klasse werden und ihre Position im Baum akzeptieren. Was passiert, wenn diese neue Klasse der Implementierung halber auch eine Unterklasse einer anderen Klasse in dem Baum sein muß? Wie kann das ohne Mehrfachvererbung realisiert werden: Schauen Sie:

class FirstImplementor extends SomeClass implements MySecondInterface {...}

class SecondImplementor implements MyFirstInterface, MySecondInterface {...}

Die erste Klasse »steckt« im Einfachvererbungsbaum direkt unterhalb der Klasse SomeClass, kann jedoch auch eine Schnittstelle implementieren. Die zweite Klasse liegt unterhalb von Object, hat aber zwei Schnittstellen implementiert. (Sie könnte beliebig viele implementieren.)


Eine abstrakte Klasse kann diese strikte Vorgehensweise ignorieren, und sie kann eine beliebige Untermenge der Methoden der Schnittstellen implementieren (oder gar keine). Aber alle ihre nicht-abstrakten Unterklassen müssen dieser Vorgabe dennoch gehorchen.

Weil Schnittstellen in einer separaten Hierarchie angeordnet sind, können sie mit den Klassen im Einfachvererbungsbaum kombiniert werden, so daß der Entwickler überall dort im Baum eine Schnittstelle einfügen kann, wo diese benötigt wird. Der Einfachvererbungsbaum kann damit so betrachtet werden, als enthielte er nur die Implementierungshierarchie. Die Entwurfshierarchie (vollständige oder abstrakte Methoden) sind im Schnittstellenbaum enthalten.

Jetzt wollen wir ein einfaches Beispiel für diese Aufteilung betrachten. Dazu erzeugen wir eine neue Klasse, Orange. Angenommen, Sie besitzen bereits eine korrekte Implementierung der Klasse Fruit sowie eine Schnittstelle, Fruitlike, die angibt, wie Fruit-Objekte aussehen sollten. Sie wollen, daß eine Orange ein Obst ist, aber Sie wollen auch, daß es sich dabei um ein kugelförmiges Objekt handelt (Spherelike), das hochgeworfen, gedreht usw. werden kann. Und so drücken Sie das ganze aus:

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 {
. . . // das Hochwerfen (toss()) kann mich entsaften (squish())
// (einzigartig für mich)
}

Dieses Beispiel werden Sie im nächsten Abschnitt wieder aufgreifen. Hier sollten Sie nur beachten, daß die Klasse Orange nicht sagen muß implements Fruitlike, weil das durch die Erweiterung von Fruit bereits passiert ist!

Ganz praktisch bei dieser Struktur ist, daß Sie es sich anders überlegen können, was die Klasse Orange erweitert (wenn beispielsweise plötzlich eine wirklich großartige Klasse Sphere implementiert wird), und doch versteht es immer die beiden folgenden Schnittstellen:

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

Der Sinn dieser Möglichkeit, Schnittstellen zu kombinieren, ist es, mehreren Klassen, die über den ganzen Einfachvererbungsbaum verteilt sein können, zu erlauben, dieselbe Menge an Methoden zu implementieren. Diese Klassen haben zwar eine gemeinsame Oberklasse (mindestens Object), aber es ist wahrscheinlich, daß es unterhalb dieser gemeinsamen Elternklasse viele Unterklassen gibt, die nicht an diesen Methoden interessiert sind. Das Hinzufügen der Methoden in die Elternklasse oder die Einführung einer neuen abstrakten Klasse, in die sie aufgenommen werden könnten, und die dann irgendwo über der Elternklasse in die Hierarchie eingefügt werden könnte, ist also nicht die ideale Lösung.

Statt dessen verwenden Sie eine Schnittstelle, um die Methoden zu spezifizieren. Sie kann von jeder Klasse implementiert werden, die sie braucht, und sie muß von keiner verwendet werden, die ansonsten gezwungen wäre, sie in dem Einfachvererbungsbaum zu erben. Der Entwurf wird nur dort angewendet, wo er benötigt wird. Anwender der Schnittstelle können jetzt Variablen und Argumente für einen neuen Schnittstellentyp angeben, die auf beliebige Klassen verweisen, die die Schnittstelle implementieren (wie Sie gleich sehen werden) - eine leistungsfähige Abstraktion. Einige Beispiele dafür sind die Objektpersistenz (über die Methoden read() und write()), das Erzeugen und Verbrauchen (die Java-Bibliothek macht das für Bilder) sowie die Bereitstellung allgemein sinnvoller Klassen. Das letztere könnte wie folgt 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 args[]) {
double calculation = oneOfThem * another;
System.out.println("hello " + yetAnother + calculation);
. . .
}
}

Hier entsteht die relativ unsinnige Ausgabe hello 12341522.756, aber der Prozeß demonstriert, daß die Klasse AnyClass direkt auf alle in der Schnittstelle PresumablyUsefulConstants definierten Variablen zugreifen kann. Normalerweise greifen Sie auf solche Variablen und Konstanten über die Klasse zu, wie etwa bei der Konstanten Integer.MIN_VALUE, die von der Klasse Integer bereitgestellt wird. Wenn mehrere Konstanten häufig eingesetzt werden, oder wenn ihr Klassenname sehr lang ist, ist die Abkürzung, direkt auf sie zugreifen zu können (als oneOfThem statt PresumableUsefulConstants.oneOfThem), Rechtfertigung genug, sie in einer Schnittstelle zu plazieren und allgemein zu implementieren.

Schnittstellen implementieren

Wie können Sie diese Schnittstellen nutzen? Sie wissen, daß Sie fast überall, wo Sie eine Klasse verwenden können, auch eine Schnittstelle einsetzen können. Probieren wir, die Schnittstelle MySecondInterface zu verwenden, die wir oben definiert haben:

MySecondInterface anObject = getTheRightObjectSomehow();

long age = anObject.ageOfTheUniverse();

Nachdem Sie anObject vom Typ MySecondInterface deklariert haben, können Sie anObject als Empfänger aller Nachrichten verwenden, die die Schnittstelle definiert (oder erbt). Was bedeutet also die obige Deklaration?

Wenn eine Variable mit einem Schnittstellentyp deklariert wird, bedeutet das einfach, daß ein Objekt, auf das die Variable verweist, diese Schnittstelle implementiert haben muß - d.h. man erwartet, daß sie alle Methoden versteht, die die Schnittstelle spezifiziert. Man geht davon aus, daß eine Vereinbarung zwischen dem Entwickler der Schnittstelle und denjenigen, die sie implementieren, besteht, und daß diese eingehalten wird. Das ist zwar ein eher abstraktes Konzept, aber es erlaubt beispielsweise, daß der obige Code geschrieben wird, noch lange bevor es Klassen gibt, die dann letztlich implementiert werden. In der traditionellen objektorientierten Programmierung werden Sie gezwungen, eine Klasse mit Stub-Implementierungen zu erzeugen, um denselben Effekt zu erzielen wie Schnittstellen.


Ein Stub ist eine Routine (Methode, Klasse usw.), die als Platzhalter verwendet wird, und die in der Regel Kommentare enthält, die beschreiben, was die Routine macht, wenn sie implementiert ist. Diese Technik ermöglicht den Programmierern, zurückzukommen und die Löcher später zu füllen, wenn es Zeit dafür ist, während sie in anderen Teilen des Codes bereits auf die Routine verweisen können, ohne dadurch einen Compiler-Fehler zu verursachen.
Hier ein komplexeres Beispiel für Schnittstellen:

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

aFruit.decay(); // Obst verfault
aFruitlike.squish(); // Obst versaftet

aFruitlike.toss(); // nicht OK
aSpherelike.toss(); // OK

anOrange.decay(); // Orangen können das alles
anOrange.squish();
anOrange.toss();
anOrange.rotate();

Die Deklarationen und die Schnittstellen werden in diesem Beispiel genutzt, um eine Orange darauf zu beschränken, sich mehr wie ein Obst oder eine Kugel zu verhalten, nur um die Flexibilität der zuvor aufgebauten Struktur zu demonstrieren. Wenn statt dessen die zweite Struktur (die mit der Klasse Sphere) verwendet worden wäre, würde ein Großteil dieses Codes auch funktionieren. In den Zeilen, in denen Fruit enthalten ist, müßten einfach alle Instanzen von Fruit durch Sphere ersetzt werden. Fast alles andere würde gleich bleiben.


Die direkte Verwendung von Klassennamen in der Implementierung dient nur der Demonstration. Normalerweise werden Sie nur Schnittstellennamen in Deklarationen und Umwandlungen verwenden, so daß der Code nicht geändert werden muß, wenn eine neue Struktur unterstützt werden soll.

Schnittstellen werden implementiert und in der gesamten Java-Klassenbibliothek verwendet, wenn ein bestimmtes Verhalten von mehreren verschiedenen Klassen erwartet wird. Sie finden unter anderem beispielsweise die Schnittstellen java.lang.Runnable, java.util.Enumeration und java.util.Observable. Wir wollen eine davon verwenden, Enumeration, um das LinkedList-Beispiel noch einmal zu betrachten und die sinnvolle Verwendung von Paketen und Schnittstellen zu demonstrieren.

In JBuilder wählen Sie Datei | Neues Projekt, geben in das Feld Datei C:\JBUILDER\myprojects\JAdvanced.jpr ein und klicken auf Weiter. Um die ersten drei Dateien einzufügen, müssen Sie das Unterverzeichnis collections für das Paket erzeugen. Klicken Sie im AppBrowser auf das Icon Dem Projekt hinzufügen und klicken Sie im Dialog Datei öffnen/erzeugen auf das Icon zum Erzeugen eines neuen Ordners. Geben Sie collections ein und drücken Sie die Eingabetaste. Doppelklicken Sie auf den neuen Ordner, um ihn zum aktuellen Verzeichnis zu machen. Schließen Sie den Dialog und verwenden Sie dann das Icon Dem Projekt hinzufügen im AppBrowser, um die folgenden drei Dateien in das Projekt einzufügen (Listings 4.1 bis 4.3).

Listing 4.1: LinkedList.java

1: package collections;
2:
3: import java.util.Enumeration;
4:
5: public class LinkedList {
6: private Node root;
7:
8: public void add(Object o) {
9: root = new Node(o, root);
10: }
11:
12: public Enumeration enumerate() {
13: return new LinkedListEnumerator(root);
14: }
15:
16: }

Listing 4.2: Node.java

1: package collections;
2:
3: class Node {
4: private Object contents;
5: private Node next;
6:
7: Node(Object o, Node n) {
8: contents = o;
9: next = n;
10: }
11:
12: public Object contents() {
13: return contents;
14: }
15:
16: public Node next() {
17: return next;
18: }
19:
20: }

Listing 4.3: LinkedListEnumerator.java

1: package collections;
2:
3: import java.util.Enumeration;
4:
5: class LinkedListEnumerator implements Enumeration {
6: private Node currentNode;
7:
8: LinkedListEnumerator(Node root) {
9: currentNode = root;
10: }
11:
12: public boolean hasMoreElements() {
13: return currentNode != null;
14: }
15:
16: public Object nextElement() {
17: Object anObject = currentNode.contents();
18:
19: currentNode = currentNode.next();
20: return anObject;
21: }
22:
23: }

Um das nächste Listing einzufügen, klicken Sie im AppBrowser auf das Icon Add to Project und klicken im Dialog Datei öffnen/erzeugen auf das Icon Übergeordneter Ordner. Geben Sie LinkedListTester.java ein und drücken Sie die Eingabetaste, um Listing 4.4 einzugeben, das die typische Verwendung einer Auflistung zeigt.

Listing 4.4: LinkedListTester.java

1: public class LinkedListTester {
2:
3: public static void main(String argv[]) {
4:
5: collections.LinkedList aLinkedList = new collections.LinkedList();
6:
7: aLinkedList.add(new Integer(1138));
8: aLinkedList.add("THX-");
9:
10: java.util.Enumeration e = aLinkedList.enumerate();
11:
12: while (e.hasMoreElements()) {
13: Object anObject = e.nextElement();
14: // etwas sinnvolles mit anObject machen
15: System.out.print(anObject);
16: }
17: System.out.print("\n");
18: }
19:
20: }

Wenn Sie LinkedListTester kompilieren und ausführen, erhalten Sie die folgende Ausgabe:



THX-1138

Beachten Sie, daß Sie zwar Enumeration e so verwenden, als ob Sie es bereits kennen würden, was jedoch nicht der Fall ist. Es handelt sich um die Instanz einer verborgenen Klasse LinkedListEnumerator, die Sie nicht sehen oder direkt verwenden können. Durch eine Kombination von Paketen und Schnittstellen hat die Klasse LinkedList es geschafft, eine transparente öffentliche Schnittstelle für sein wichtigstes Verhalten bereitzustellen (über die bereits definierte Schnittstelle java.util.Enumeration), während ihre beiden Klassen verborgen werden, in denen die eigentliche Implementierung stattfindet.


Zusammenfassung

Heute haben Sie gelernt, wie Variablen und Methoden ihre Sichtbarkeit und den Zugriff durch andere Klassen steuern können, indem sie Modifier-Schlüsselworte verwenden. Tabelle 4.1 zeigt einen Überblick über diese Schlüsselwörter und die entsprechenden Zugriffsebenen.

Tabelle 4.1: Zugriffsschutz und Sichtbarkeit in Java

public

package (Standard)

protected

private protected

private


Klasse im Paket (ändernder Zugriff)

Ja

Ja

Ja

Nein

Nein

Unterklasse im Paket (ändernder Zugriff)

Ja

Ja

Ja

Nein

Nein

Unterklasse im Paket (Vererbung)

Ja

Ja

Ja

JA

Nein

Äußere Klasse (ändernder Zugriff)

Ja

Nein

Nein

Nein

Nein

Äußere Unterklasse (ändernder Zugriff)

Ja

Nein

Nein

Nein

Nein

Äußere Unterklasse (Vererbung

Ja

Nein

Ja

Ja

Nein

Sie haben gelernt, daß Instanzvariablen zwar größtenteils als private deklariert werden, daß aber die Deklaration von Zugriffsmethoden Ihnen ermöglicht, das Lesen und Schreiben dieser Variablen separat zu steuern. Zugriffsschutzebenen ermöglichen Ihnen beispielsweise, Ihre public-Abstraktionen sauber von ihren konkreten Darstellungen zu trennen.

Sie haben gesehen, wie Klassenvariablen und -Methoden geschützt werden können, die der Klasse selbst zugeordnet sind, und wie final-Variablen, -Methoden und -Klassen deklariert werden, um Konstanten anzulegen, schnelle und sichere Methoden sowie Klassen, die nicht überschrieben und von denen keine Unterklassen angelegt werden können.

Sie haben entdeckt, wie abstrakte Klassen deklariert und verwendet werden, die nicht instantiiert werden können, und abstrakte Methode, die keine Implementierung besitzen und deshalb in Unterklassen überschrieben werden müssen. Insgesamt bilden sie eine Schablone, die von Unterklassen ausgefüllt wird und als Variante für Schnittstellen dient.

Außerdem wurde gezeigt, wie Pakete genutzt werden können, um Klassen in sinnvollen Gruppen und Hierarchien zusammenzufassen und zu kategorisieren. Dadurch erhalten nicht nur Ihre Programme eine bessere Struktur, sondern dadurch können Sie und alle anderen Programmierer im Internet Projekte eindeutig benennen und gemeinsam nutzen. Sie haben gelernt, wie Pakete genutzt werden - sowohl Ihre eigenen als auch die vielen, die es in der Java-Klassenbibliothek bereits gibt.

Dann haben Sie erfahren, wie Schnittstellen deklariert und verwendet werden. Schnittstellen stellen einen leistungsfähigen Mechanismus für die Erweiterung der traditionellen Einfachvererbung der Klassen von Java und für die Abtrennung von Entwurfs-Vererbung und Implementierungsvererbung in Ihren Programmen dar. Schnittstellen werden auch häufig verwendet, um allgemeine (gemeinsam genutzte) Methoden aufzurufen, wenn nicht genau bekannt ist, welche Klasse gemeint ist.

Pakete und Schnittstellen können kombiniert werden, um sinnvolle Abstraktionen zu bieten, die einfach erscheinen und dabei eigentlich nur ihre komplexen, implementierungsbasierten Komponenten vor ihren Anwendern verstecken - eine sehr leistungsfähige Technik.

F&A

F Wird mein Java-Code langsam, wenn ich überall Zugriffsmethoden verwende?

A Nicht immer. Bald werden die Java-Compiler intelligent genug sein, sie automatisch schnell zu machen, aber wenn Sie sich Sorgen über die Geschwindigkeit machen, können Sie die Zugriffsmethoden immer als final deklarieren, dann sind sie so schnell wie der direkte Zugriff auf Instanzvariablen.

F Werden Klassenmethoden (static) vererbt wie Instanzmethoden?

A Nein, Klassenmethoden (static) sind momentan standardmäßig final. Aber wie könnte man dann jemals eine Klassenmethode deklarieren, die nicht final ist? Die Antwort ist: überhaupt nicht. Die Vererbung von Klassenmethoden wird nicht unterstützt, wodurch sie sich von Instanzmethoden unterscheiden. Weil das nicht mit der Java-Philosophie in Einklang zu bringen ist, alles so einfach wie möglich zu machen, wird es vielleicht in einem späteren Release anders realisiert.

F Nach dem, was ich heute gelernt habe, scheinen prviate abstract-Methoden und final abstract-Methoden oder -Klassen nicht sinnvoll zu sein. Sind sie erlaubt?

A Nein. Sie erzeugen Compiler-Fehler, wie Sie schon vermutet haben. Um sinnvoll zu sein, müssen abstract-Methoden überschrieben werden können, und von abstract-Klassen müssen Unterklassen gebildet werden können, aber keine dieser beiden Operationen wäre erlaubt, wenn sie gleichzeitig private oder final wären.

F Was passiert mit Paket/Verzeichnis-Hierarchien, wenn Java eine Archivierung hinzugefügt wird?

A Im JDK 1.1 gibt es eine Archivierung, aber die Paket/Verzeichnis-Hierarchien sind immer noch relevant. Um Ihre Pakete für die Archivierung vorzubereiten, müssen Sie sie immer noch in dieser Hierarchie anlegen. Die Archive heißen JAR-Dateien (Java ARchives). Diese komprimierten Dateien sorgen dafür, daß es sehr viel schneller geht, die Klassendateien für ein Applet vom Internet herunterzuladen. Der JDK stellt ein Befehlszeilen-Werkzeug namens jar.exe bereit (es befindet sich im Verzeichnis JBuilder\java\bin), mit dem Sie den Inhalt von JAR-Dateien einsehen, eine JAR-Datei dekomprimieren oder Ihre eigenen Dateien komprimieren können.

F Gibt es eine Möglichkeit, wie eine verborgene Klasse nicht-verborgen dargestellt werden kann?

A Der eigentümliche Fall, wo eine verborgene Klasse gezwungen werden kann, sichtbar zu werden, tritt auf, wenn sie eine public-Oberklasse hat und jemand eine Instanz davon in die Oberklasse umwandelt. Alle public-Variablen oder -Methoden dieser Oberklasse stehen jetzt zum Zugriff bereit oder können über die Instanz Ihrer verborgenen Klasse aufgerufen werden, auch wenn diese Variablen oder Methoden dort nicht als public vorgesehen waren. In der Regel sind diese public-Methoden/Variablen diejenigen, auf die Ihre Instanzen Zugriff haben, sonst hätten Sie sie nicht mit dieser public-Oberklasse deklariert. Das ist jedoch nicht immer der Fall. Viele der eingebauten System-Klassen sind public - Sie haben gar keine andere Wahl. Aber hoffentlich ist das eine sehr selten auftretende Situation.

F Die abstrakten Klassen müssen nicht alle Methoden in einer Schnittstelle selbst implementieren. Müssen alle ihre Unterklassen das?

A Nein. Die Regel lautet, daß irgendeine Klasse eine Implementierung für eine Methode bereitstellen muß, aber das muß nicht unbedingt die Ihre sein. Das bedeutet, alles, was die abstract-Klasse nicht implementiert, muß die erste nicht-abstrakte Klasse darunter implementieren. Alle anderen Unterklassen müssen nichts mehr tun.

Workshop

Der Workshop bietet zwei Möglichkeiten, zu überprüfen, was Sie in diesem Kapitel gelernt haben. Der Quiz-Teil stellt Ihnen Fragen, die Ihnen helfen sollen, Ihr Verständnis für den vorgestellten Stoff zu vertiefen. Die Antworten auf die Fragen finden Sie in Anhang A. Der Übungen-Teil ermöglicht Ihnen, Erfahrungen in der Anwendung der Dinge zu sammeln, die Sie hier kennengelernt haben. Versuchen Sie, diese Dinge durchzuarbeiten, bevor Sie mit der nächsten Lektion weitermachen.

Quiz

Richtig oder falsch?

Übungen

public class InstanceCounter {
private static int instanceCount = 0; // eine Klassenvariable

private protected static int getInstanceCount() {
return instanceCount; // eine Klassenmethode
}
private static void incrementCount() { // eine Klassenmethode
++instanceCount;
}
InstanceCounter() { // der Klassenkonstruktor
InstanceCounter.incrementCount();
}
}

Ändern Sie den Code so ab, daß mit der Methode getInstanceCount() Zugriff auf Klassen und Unterklassen im selben Paket möglich ist.

public interface FordTaurusConstants {

public static final int mpgFTC = 25;

public static final float tankSizeFTC = 13.5;

public static final String modelNameFTC = "Ford Taurus GL";

}


public class RentalCars implements FordTaurusConstants {

public static void main(String args[]) {

double totalMilesFTC = mpgFTC * tankSizeFTC;


System.out.println("Die Gesamtreichweite für den "

+ modelNamePLC + " beträgt "

+ totalMilesFTC + " Kilometer.");

...

}

}

Der veränderte Code sollte bei Ausführung in einer Java-Anwendung die folgende Ausgabe erzeugen:



Die Gesamtreichweite für den Ford Taurus GL beträgt 337.5 Kilometer.
Die Gesamtreichweite für den Plymouth Laser Turbo RS beträgt 722 Kilometer.


© 1997 SAMS
Ein Imprint des Markt&Technik Buch- und Software- Verlag GmbH
Elektronische Fassung des Titels: JBuilder in 14 Tagen, ISBN: 3-87791-895-6

Previous Page Page Top TOC Index Next Page See Page