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



3. Woche im Überblick:


15. Tag:

Modifier

von Charles L. Perkins

Nach einer gewissen Zeit der Programmierung in Java finden Sie die Tatsache, daß alle Klassen, Methoden und Variablen public sind, sicherlich lästig. Je umfangreicher ein Programm wird, um so mehr werden in der Regel Klassen für neue Projekte wiederverwendet, und um so mehr brauchen Sie eine gewisse Kontrolle über deren Sichtbarkeit. Eine der guten Lösungen- Pakete - lernen Sie morgen. In der heutigen Lektion erforschen Sie, was innerhalb einer Klasse in diesem Zusammenhang machbar ist.

Sie lernen heute folgendes:

Modifier sind Präfixe, die in verschiedenen Kombinationen auf die Methoden und Variablen einer Klasse und in gewissem Umfang auf die Klasse selbst angewandt werden können.

Die Liste der verfügbaren Modifier ist lang und vielfältigt. Ihre Reihenfolge hat keine Bedeutung. Sie können sie ganz nach Geschmack variieren. Suchen Sie sich einen Stil aus und wenden Sie ihn dann in allen Klassen konsistent an. Die empfohlene Reihenfolge ist wie folgt:

<access> static abstract synchronized <unusual> final native

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

In der Beta-Version wurde threadsafe durch volatile ersetzt. Beides hat mit Multithreading zu tun. Mehr verrate ich hier nicht (siehe 18. Tag). transient ist ein spezieller Modifier, der zum Deklarieren einer Variablen dient, die sich außerhalb des beständigen Teils eines Objekts befinden soll. Das vereinfacht die Implementierung von Speichersystemen für persistente Objekte in Java. Obwohl der Compiler diesen Modifier unterstützt, wird er im derzeitigen Java-System nicht benutzt. Mehrere reservierte Schlüsselwörter (z. B. byvalue, future und generic) können in späteren Java-Versionen eventuell <unusual>-Modifier sein. Im Beta-System erscheint im Quellcode der Java-Bibliotheksklassen keiner dieser ungewöhnlichen Modifier.

Alle Modifier sind notwendigerweise optional. Keiner muß in einer Deklaration erscheinen. Ein guter Stil impliziert das Hinzufügen so vieler Modifier wie nötig, um die beabsichtigte Nutzung und Einschränkungen dessen, was Sie deklarieren, optimal zu beschreiben. In manchen Sonderfällen (z. B. innerhalb einer Schnittstelle - wird morgen erklärt) sind bestimmte Modifier bereits gebrauchsfertig verfügbar, so daß Sie sie nicht tippen müssen.

Der synchronized-Modifier wird am 18. Tag erklärt. Er hat mit Multithreading-Methoden zu tun. Der native-Modifier ist Thema des 20. Tages. Er spezifiziert die Implementierung einer Methode in der nativen Rechnersprache (normalerweise C) anstelle von Java. Wie <access>-Modifier auf Klassen angewandt werden, wird morgen behandelt.

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.

public

Da jede Klasse eine Insel für sich ist, betrifft die erste dieser Beziehungen 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. Da Sie gerade erst lernen, wie das geht, beginnen wir mit dem breitestmöglichen Zugriff und schränken ihn mit zunehmender Erfahrung ein, bis Sie den Zugriff auf Ihre Methoden und Variablen fast schon im Schlaf definieren. 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, was uns zur nächsten Schutzebene führt.

package

In C gibt es eine Notion zum Verbergen eines Namens, so daß nur die Funktionen innerhalb einer bestimmten Quelldatei darauf zugreifen können. In Java werden Quelldateien durch die explizitere Notion von Paketen ersetzt, wodurch Klassen gruppiert werden können (Sie lernen morgen darüber mehr). Vorläufig genügt es zu wissen, daß die Beziehung einer Klasse zu anderen Klassen ein Teil des Systems, der Bibliothek oder des Programms (oder einer anderen Klassengruppierung) ist. Das definiert die nächsthöhere Schutzebene.

In der Java-Sprache hat diese nächste Zugriffsebene einen Namen. Er wird durch das Fehlen eines Zugriffsmodifiers in einer Deklaration bezeichnet. Historisch wurden dafür verschiedene Namen verwendet, darunter »friendly« und »package«. Der zweite Name ist passender und wird auch hier verwendet. In späteren Systemversionen ist es vielleicht möglich, explizit package zu definieren. Vorläufig gilt einfach der Standardschutz, wenn keiner definiert wird.

Warum sollte jemand mehr Tipparbeit auf sich nehmen und explizit package definieren? Das ist eine Sache der Konsistenz und Klarheit. In Deklarationen mit verschiedenen Zugriffsmodifiern ist es wünschenswert, den Modifier immer explizit anzugeben. Das ist einerseits für den Leser einfacher und deutet andererseits dem Compiler Ihre Absichten an, um vor eventuellen Konflikten zu warnen, weil in manchen Zusammenhängen von unterschiedlichen »Standard«-Schutzebenen ausgegangen wird.

Die meisten Deklarationen, mit denen Sie in den letzten zwei Wochen gearbeitet haben, enthalten diesen Standardschutz. Hier zur Erinnerung nochmal ein Beispiel:

public class ALessPublicClass {

   int aPackageInt = 2;

   String aPackageString = "a 1 und a ";

   float aPackageMethod() { //Kein Zugriffsmodifier bedeutet "package"

      ...

   }

}

public class AClassInTheSamePackage {

   public void testUse() {

      ALessPublicClass aLPC = new ALessPublicClass();

      System.out.println(aLPC.aPackageString + aLPC.aPackageInt);

      aLPC.aPackageMethod(); //Alle hier sind A.O.K.

   }

}

Versucht eine Klasse aus einem anderen Paket, auf aLPC so zuzugreifen wie AClassInTheSamePackage in diesem Beispiel, würden Sie Kompilierfehler erhalten. (Sie lernen morgen, wie solche Klassen erstellt werden.)

Warum wurde package als Standard definiert? Wenn Sie ein großes System entwickeln und Ihre Klassen in Arbeitsgruppen unterteilen, um kleinere Systemteile zu implementieren, müssen Klassen oft mehr voneinander als von der Außenwelt gemeinsam nutzen. Die Notwendigkeit dieser gemeinsamen Nutzung ist so üblich, daß es sich lohnt, hier einen Standardschutz einzurichten.

Was soll nun geschehen, wenn einige Einzelheiten Ihrer Implementierung nicht von diesen »Freunden« genutzt werden sollen? Die Antwort auf diese Frage führt uns zur nächsten Schutzebene.

protected

Die dritte Beziehung betrifft eine Klasse und ihre gegenwärtigen und zukünftigen Subklassen. Diese Subklassen stehen einer »Elternklasse« aus folgenden Gründen viel näher als »fremden« Klassen:

Niemandem sonst ist das Privileg dieser Zugriffsebene gestattet. Andere müssen sich mit dem public-Aspekt der Klasse zufriedengeben.

Um die für Subklassen reservierte Intimitätsebene zu unterstützen, hat man in modernen Programmiersprachen eine zwischengelagerte Zugriffsebene zwischen den zwei vorherigen Ebenen und vollem Schutz erfunden. Diese Ebene bietet mehr Schutz und grenzt den Zugriff noch weiter ein, erlaubt den Subklassen aber immer noch vollen Zugriff. In Java nennt man diese Schutzebene 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 Elternklasse 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.

private

Die letzte Schutzebene und höchste Schutzebene ist das Gegenteil von 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 - ist 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.

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!

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. Durch Trennen des Lesens und Schreibens von Instanzvariablen können Sie einen Wert mit einer public-Methode ausgeben und mit einer protected-Methode setzen. Das ist meist ein angemessener Schutz, weil wahrscheinlich jeder in der Lage sein muß, den Wert anzufordern, jedoch nur Sie (und Ihre Subklassen) in der Lage sein sollten, ihn zu ändern. Handelt es sich um besonders schutzwürdige Daten, können Sie die »setzende« Methode private und die »holende« protected deklarieren. Im Prinzip eignet sich jede Kombination, solange sie gegenüber der Außenwelt den geforderten Datenschutz sicherstellt.

Gemäß Spezifikation der Beta-Sprache ist es nicht zulässig, daß Instanzvariablen und Methoden den gleichen Namen haben. Der Beta-Compiler läßt das aber zu! Da unklar ist, wie dieser Konflikt gelöst wird, wenden Sie das einfache Namensschema an, das Sie bisher in Ihren Programmen benutzt haben. Falls sich der Compiler in einem späteren Release darüber beschwert, können Sie die Methodennamen ändern.

Eine Alternative zur Namenskonvention für Accessor-Methoden ist das Voranstellen des Präfixes get bzw. set vor den Variablennamen. Abgesehen davon, daß Sie für weniger Klarheit mehr tippen müssen, zwingt Sie dieser Stil (durch die Groß- und Kleinschreibung, die in Java berücksichtigt werden muß), Methodennamen wie setAnnoyingFirstCapitalLetter() zu schreiben. Das ist selbstverständlich eine Sache des Geschmacks. Wichtig ist, daß Sie immer die gleiche Konvention verwenden.

Wenn Sie an Ihre eigenen Instanzvariablen etwas anhängen möchten, versuchen Sie es damit:

aUsefulString(aUsefulString() + " some appended text");

Sie verwenden dabei Accessor-Methoden wie ein klassenexternes Element, um aUsefulString zu ändern. Wozu soll das gut sein?

Erstens schützen Sie die Variable, so daß sich Änderungen nicht auf die Verwendung Ihrer Klasse durch andere auswirken, jedoch können Sie das nach wie vor durch Ihre Klasse bewirken. Wie in der obigen Gegenüberstellung von Abstrakt und Konkret diskutiert wurde, sollten Sie nicht zu viel von Ihrer eigenen Darstellung kennen, außer die Stellen, über die Sie alles wissen müssen. Müssen Sie etwas in aUsefulString ändern, wirkt sich das nicht auf diese Variable in Ihrer Klasse aus (was ohne Accessor-Methoden der Fall wäre), sondern nur auf die Implementierung des Accessors.

Dieser Schutz von Instanzvariablen vor Ihrem eigenen Zugriff hat die nützliche Nebenwirkung, daß Sie einen Spezialcode, der irgendwann später jedesmal ausgeführt werden muß, wenn ein Zugriff auf aUsefulString erfolgt, an einer Stelle einfügen können, so daß dieser Spezialcode in allen anderen Methoden Ihrer Klasse (und in denen anderer) richtig aufgerufen wird. Hier ein Beispiel:

protected void aUsefulString(String s) { //Die »set«-Methode

   aUsefulString = s;

   performSomeImportantBookkeepingOn(s);

}

Folgende Schreibweise:

x(12 + 5 * x());

sollte man sich anstelle von

x = 12 + 5 * x;

angewöhnen. Die geringfügig größere Tipparbeit lohnt sich angesichts einer rosigen Zukunft der Wiederverwendbarkeit und einfachen Pflege der ersten Version.

Klassenvariablen und -methoden

Was muß geschehen, wenn Sie eine Variable erstellen möchten, die alle Instanzen sehen und verwenden soll? Jede Instanz einer Instanzvariablen hat eine eigene Kopie der Variablen, so daß ihr Sinn zunichte gemacht wird. 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 definiert eine Klasse Klassenmethoden, um ihre eigenen Instanzen zu zählen:

public class InstanceCounter {

   private static int instanceCount = 0; // Eine Klassenvariable

   protected static int instanceCount() { // Eine Klassenmethode

      return instanceCount;

   }

   private static void incrementCount() {

      ++instanceCount;

   }

   InstanceCounter() {

      InstanceCounter.incrementCount();

   

}

In diesem Beispiel ruft eine explizite Verwendung des Klassennamens die Methode incrementCount() auf. Das scheint in diesem kleinen Beispiel zwar umständlich, teilt in einem umfangreichen Programm dem Leser aber sofort mit, welches Objekt (der Klasse, nicht der Instanz) die Methode erwartungsgemäß abarbeiten soll. Das ist besonders nützlich, wenn der Leser herausfinden will, wo diese Methode in einer größeren Klasse, in der alle Klassenmethoden oben stehen (was die bevorzugte Vorgehensweise ist), deklariert wurde.

Beachten Sie die Initialisierung von instanceCount auf 0. Ebenso wie eine Instanzvariable beim Erstellen ihrer Instanz initialisiert wird, erfolgt die Initialisierung einer Klassenvariablen beim Erstellen ihrer Klasse. Diese Klasseninitialisierung ist notwendig, bevor etwas mit dieser Klasse oder ihren Instanzen durchgeführt wird, so daß die Klasse in diesem Beispiel wie geplant funktioniert.

Die Konventionen für den Zugriff auf eine Instanzvariable, die Sie heute gelernt haben, werden in diesem Beispiel angewandt, um auf eine Klassenvariable zuzugreifen. Die Accessor-Methoden sind deshalb Klassenmethoden. (Es gibt hier kein set, sondern nur eine increment-Methode, weil niemandem gestattet wird, instanceCount direkt zu setzen.) Nur den Subklassen ist es gestattet, den Wert von instanceCount anzufordern, weil das eine (relativ) intime Einzelheit ist. Nachfolgend ein Test von InstanceCounter in Aktion:

public class InstanceCounterTester extends InstanceCounter {

   public static void main(String args[]) {

      for (int i = 0; i < 10; ++i)

         new InstanceCounter();

      System.out.println("made " + InstanceCounter.instanceCount());

   }

}

Seltsamerweise wird in diesem Beispiel folgendes ausgegeben:

made 10

Der final-Modifier

Der final-Modifier ist sehr vielseitig:

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. Sie finden diese Klassen in den Hierarchiediagrammen in Anhang B (final-Klassen sind dunkler abgesetzt als public-Klassen). 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.

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. Diese Variablen funktionieren wie eine bessere Version der #define-Konstanten von C. Außerdem können Klassen ü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 nicht final deklariert werden. Vor lokalen Variablen können überhaupt keine Modifier stehen:

{

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

   ...

}

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 Bedeutung 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 wieder hergestellt wird.

private-Methoden sind effektiv final, da alle Methoden in einer final-Klasse deklariert sind. 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 über 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.

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 aus einer Superklasse. Ist dieser gemeinsame Speicherplatz der primäre Grund, daß eine Superklasse existiert und soll er nur von ihren Subklassen verwendet werden, nennt man eine solche Superklasse eine abstract-Klasse.

abstract-Klassen können keine Instanzen erstellen, 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. Nicht abstrakte 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 nicht abstrakten 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. Wenn Sie sich die Diagramme in Anhang B ansehen, erkennen Sie, daß abstract-Klassen dunkler schattiert sind als final-Klassen und recht häufig in der Bibliothek vorkommen.

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 abstract-Klasse allerdings nicht die einzige Wahl. In älteren objektorientierten Sprachen sind abstract-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 reicheren Form von Schnittstellen unterstützt werden.

Zusammenfassung

Heute haben Sie gelernt, wie Variablen und Methoden ihre Sichtbarkeit und den Zugriff durch andere Klassen anhand von vier Schutzebenen steuern können: public, package, protected und private. Sie haben auch gelernt, daß Instanzvariablen zwar meist private deklariert werden, daß Sie aber mit Accessor-Methoden das Lesen und Schreiben dieser Variablen getrennt kontrollieren können. Mit Schutzebenen können Sie beispielsweise Ihre public-Abstraktionen von der konkreten Darstellung sauber trennen.

Ferner haben Sie gelernt, wie Klassenvariablen und -methoden erstellt und final-Variablen, -Methoden und -Klassen deklariert werden, um Konstanten, schnelle oder sichere Methoden bzw. Klassen darzustellen.

Schließlich haben Sie gelernt, wie abstract-Klassen, von denen keine Instanzen erstellt werden können, und abstract-Methoden, die keine Implementierung brauchen und in Subklassen überschrieben werden müssen, deklariert und verwendet werden. Insgesamt bieten sie eine Maske für Subklassen, die ausgeführt und als Variante der leistungsstarken Schnittstellen von Java, die Sie morgen lernen, fungieren können.


Fragen und Antworten

F: Warum gibt es in Java so viele verschiedene Schutzebenen?

A: Jede Schutz- bzw. Zugriffsebene bietet eine andere Sicht Ihrer Klasse zur Außenwelt. Eine Sicht ist für alle zugeschnitten, eine ist für Klassen in Ihrem eigenen Paket ausgelegt, eine dient nur für Ihre Klassen und deren Subklassen, und die letzte dient für alles in Ihrer Klasse. Jede Ebene ist eine logisch gut definierte und sinnvolle Trennung, die Java direkt in der Sprache unterstützt (im Gegensatz etwa zu Accessor-Methoden, deren Konvention Sie einhalten müssen).

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

A: 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.

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

A: Ja und Nein. Der Beta-Compiler ermöglicht deren Vererbung, jedoch sind static-Methoden jetzt nach einer eher seltsamen Änderung in der Beta-Version 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. Da dies der Java-Philosophie (die da lautet, daß alles so einfach wie möglich sein soll) widerspricht, dürfte das in künftigen Versionen wieder geändert werden. Vorläufig folgen Sie dem Compiler und gehen Sie davon aus, daß Klassenmethoden normal vererbt werden.

F: 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?

A: 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.

F: Wie verhält es sich dann mit static transient oder final transient?

A: Sie führen ebenfalls zu Kompilierfehlern. Da sich der transient-Teil eines Objekts naturgemäß bei jeder Instanz ändert, kann er nicht static oder final sein. Diese Einschränkung spielt aber erst in künftigen Versionen eine Rolle, wenn transient in Java genutzt wird.


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

Schreiben Sie uns!

Previous Page TOC Index Next Page See Page