|
|||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||
|
Non-Polymorphic Classes - final in Conjuction with Classes and Methods
|
||||||||||||||||||
Mit dem Schüsselwort final können Variablen, Klassen und
Methoden qualifiziert werden. Die Bedeutung von final-Variablen haben wir
uns im letzen Artikel angesehen (siehe /
KRE1
/). Diesmal
wollen wir uns mit final-Klassen und -Methoden befassen. Die wohl bekannteste
final-Klasse ist die Klasse java.lang.String. Jeder Java-Programmierer
weiß, daß die String-Klasse besondere Eigenschaften hat: sie
ist unveränderlich. Bisweilen wird deshalb angenommen, die Unveränderlichkeit
ergäbe sich aus der Qualifizierung mit final. Zwar hat final mit Unveränderlichkeit
zu tun, aber eine final-Klasse ist nicht automatisch unveränderlich.
In diesem Artikel wollen wir klären, was Unveränderlichkeit genau
bedeutet (es gibt verschiedene Arten davon) und was das Schlüsselwort
final damit zu tun hat.
Was heißt eigentlich „unveränderlich“?In den letzten beiden Artikeln haben wir unveränderliche Klassen diskutiert. Das sind Klassen, deren Objekte sich nicht ändern können und die auch nicht von Außen verändert werden können. Dabei haben wir die Unveränderlichkeit stets auf den Zustand des Objekts bezogen. Der Begriff der „Unveränderlichkeit“ kann aber auch anders verstanden werden.
Ein Klasse hat immer zwei wesentliche Aspekte: Daten und Code. Die Daten
sind die Felder. Sie bestimmen den Zustand („State“) der Objekte. Der Code
sind die Methoden. Sie bestimmen das Verhalten („Behavior“) der Objekte.
Entsprechend gibt es zwei Arten von Unveränderlichkeit.
Von Polymorphismus spricht man im Zusammenhang mit Referenzen. Objekte sind niemals polymorph, nur Referenzen können polymorph sein. Eine Referenzvariable kann auf Objekte verschiedenen Typs zeigen. Dazu muß die Variable eine Referenz auf einen Supertyp (Superklasse oder Superinterface) sein und die referenzierten Objekte müssen von einem Subtyp sein. Wenn sich abhängig vom Typ des referenzierten Objekts das Verhalten der Variable verändert, dann spricht man polymorphem Verhalten. Polymorphes Verhalten kann daher nur im Zusammenhang mit Vererbung auftreten und ergibt sich dadurch, daß Subtypen Methoden des Supertyps redefinieren und anders implementieren, als es der Supertyp tut. Später, beim Ablauf des Programms, wird dann anhand des Typs des referenzierten Objekts entschieden, welche Implementierung einer Methode (die des Sub- oder die des Supertyps) angestoßen wird. Auf diese Weise ergibt sich das polymorphe (d.h. vielgestaltige) Verhalten der Referenzvariablen. Als polymorphe Klassen bezeichnet man in diesem Zusammenhang Klassen, deren Methoden von Subklassen redefiniert werden. Da Polymorphismus mit Vererbung zu tun hat, liegt die Schlußfolgerung nahe, daß nicht-polymorphe Klassen jene Klassen sind, von denen man nicht ableiten kann, weil es ohne Subklassen keine redefinierten Methoden geben kann. Hier kommt das Schlüsselwort final ins Spiel: wenn eine Klasse mit final qualifiziert ist, dann kann man von dieser Klasse nicht ableiten. Deshalb gibt es das Mißverständnis, final-Klassen seinen nicht-polymorph. Das stimmt leider nicht so ganz.
Die Verwirrung stammt vermutlich daher, daß die String-Klasse
final ist und gleichzeitig unveränderlich in jeder Hinsicht: sie ist
sowohl „immutable“ als auch „nicht-polymorph“. Im letzten Artikel
(siehe /
KRE1
/) haben wir gesehen, daß Immutability
praktisch nichts mit final zu tun hat. Immutability ist eine semantische
Eigenschaft einer Klasse, die explizit programmiert werden muß und
die sich nicht durch die Syntax der Sprache ausdrücken läßt.
Polymorphismus hingegen hat mit final zu tun, aber nicht in der Art, daß
jede final-Klasse automatisch nicht-polymorph wäre.
Die Beziehung zwischen final und PolymorphismusEin polymorpher Typ zeigt vielgestaltiges Verhalten. Das kann wie oben bereits beschrieben durch das Überschreiben von Methoden in Subklassen erreicht werden. Diese Definition von polymorphem Verhalten ist sehr eng gefaßt und etwas vereinfacht. Es ist die Definition von Polymorphismus, die für Einführungen in die Objektorientierung üblicherweise verwendet wird, um Neulingen den Begriff des Polymorphismus zu erläutern. Wir wollen den Begriff des Polymorphismus für die nachfolgenden Betrachtungen etwas allgemeiner fassen. Polymorphes Verhalten kann nämlich auch weniger direkt als durch die Redefinition von Methoden in Klassenhierarchien entstehen. Selbst Klassen, deren Methoden nicht überschrieben werden, können polymorphes Verhalten zeigen, wenn nämlich ihr Zustand polymorph ist. Deshalb können auch final-Klassen sehr wohl polymorphes Verhalten zeigen.Sehen wir uns das genauer an. Wie könnte eine polymorphe final-Klasse aussehen? Betrachten wir eine Klasse, die in ihrem Konstruktor ein Argument von einem Supertyp akzeptiert und Methoden des Arguments aufruft.
public final class SampleClass {
SampleClass(SuperType arg) {
Nehmen wir außerdem an, die Methode SuperType.doSomething() sei polymorph, d.h. es gibt Subtpyen, die diese Methode redefiniert haben. Dann wird die Methode SampleClass.someInnocentMethod() unterschiedliches Verhalten an den Tag legen, abhängig vom Typ des Konstruktorarguments.
SampleClass sample1 = new SampleClass(new SuperType());
Sample1.someInnocentMethod(); // calls SuperType.doSomething()
Obwohl die final-Klasse SampleClass keine abgeleiteten Klassen haben kann, ist sie dennoch polymorph. Das liegt daran, daß sie eine Referenz auf ein polymorphes Objekte verwendet. Polymorphismus entsteht also nicht allein durch das Überschreiben von Methoden in Klassenhierarchien, sondern bereits dann, wenn eine Klasse eine andere polymorphe Klasse verwendet. Die Schlußfolgerung, daß eine final-Klasse nicht-polymorph sei, ist also falsch. Das gleiche gilt im übrigen auch für final-Methoden. Betrachten wir in diesem Zusammenhang ein weiteres Mißverständnis. Sehen wir uns eine non-final-Klasse an, die sowohl final- als auch non-final-Methoden hat. Bisweilen wird vermutet, daß die non-final-Methoden polymorph wären, wohingegen die final-Methoden nicht-polymorph seien. Auch das stimmt nicht, wie folgendes Beispiel zeigt:
public class SuperClass {
Die Methode someInnocentMethod() ist zwar final, aber sie ruft die non-final-Methode doSomething() auf und hat deshalb unterschiedliches Verhalten abhängig vom Typ des Objekts, auf dem sie aufgerufen wird. Im Grunde liegt derselbe Fall vor wie im Beispiel oben: eine final-Methode ruft über eine Referenz, in diesem Fall die this-Referenz, eine polymorphe Methode auf und ist damit selbst polymorph.
Wie man sieht, führt die Qualifizierung von Klassen mit dem Schlüsselwort
final nicht zur Unveränderlichkeit der Klassen. Eine final-Klasse
hat weder einen unveränderlichen Zustand noch ist ihr Verhalten unveränderlich
im Sinne von nicht-polymorph. Die Qualifizierung von Klassen und
Methoden mit dem Schlüsselwort final führt lediglich dazu, daß
im Falle einer final-Klasse keine Subklassen definiert werden können
und im Falle von Methoden, daß die Methode nicht in einer Subklasse
überschrieben werden kann. Das ist auch genau das, was die Sprachspezifikation
festlegt.
Nun ist in der Praxis aber trotzdem so, daß viele Programmierer von einer final-Klasse erwarten, das sie nicht-polymorph ist. Diese Erwartungshaltung stammt einerseits von Beispielen aus dem JDK wie der String. Diese Auffassung wird außerdem in Büchern verbreitet. Beispielsweise sagen Arnold, Gosling und Holmes in ihrem Java-Standardwerk „The Java Programming Language“ (/ ARN /) in Abschnitt 3.6: If a method is final, you can rely on its implementation details (unless it invokes non-final methods, of course). … If you make a method final, you should really intend that its behavior be completely fixed.Mit “completely fixed” ist hier „unveränderlich“ im Sinne von „nicht-polymorph“ gemeint. Wie implementiert man nicht-polymorphe Klassen?Nicht-polymorphes Verhalten muß aktiv und bewußt implementiert werden; es genügt nicht, Methoden als final zu deklarieren.Damit das Verhalten einer Methode nicht-polymorph ist, muß man darauf achten, · daß die Methode nichts verwendet, was seinerseits polymorph ist, und · daß die Methode selbst final ist, sonst könnte sie als Ganzes überschrieben werden. Eine gesamte Klasse ist nicht-polymorph, · wenn alle ihre Methoden nicht-polymorph sind, und · wenn die Klasse selbst final ist, sonst könnten beim Ableiten polymorphe Methoden hinzugefügt werden. Das heißt, für die Implementierung einer nicht-polymorphen Klasse deklariert man zunächst einmal die Klasse als final. Damit sind dann implizit sämtliche Methoden final. Dann muß man sicherstellen, daß die Methoden keine polymorphen Operationen aufrufen. Woher weiß man, ob eine Operation, die aufrufen werden soll, polymorph ist oder nicht? So genau weiß man das leider meistens nicht. Man kann zumindest sicher sein, daß alle Operationen auf primitiven Typen nicht-polymorph sind, weil es für die primitiven Typen keine Vererbungsbeziehungen gibt und es in Java auch gar keine Referenzvariablen von primitivem Typ gibt. Alle Methoden, die über Referenzen aufgerufen werden, können hingegen polymorph sein. Hier muß man in der jeweilige JavaDoc nachlesen, ob die Methode polymorph ist oder nicht. Die Tatsache, daß die Methode final ist, ist lediglich ein Hinweis, aber keine Garantie. In der Praxis sind nur ganz wenig Klassen nicht-polymorph, weil die meisten Klassen irgend etwas Polymorphes verwenden. Lediglich ganz grundlegende Klassen wie String, Integer, Long, Boolean, etc. aus dem java.lang-Package sind nicht-polymorph. Wenn man ihre Implementierungen studiert, stellt man fest, daß sie ausschließlich primitive Typen oder andere nicht-polymorphe Typen verwenden. String beispielsweise verwendet nur primitive Typen und Locales, die ihrerseits nicht-polymorph sind. String ist eine final, nicht-polymorphe und immutable Klasse. Ebenso ist es bei Integer. In den Listings 1 und 2 finden sich exemplarische Implementierungen für eine veränderliche und eine unveränderliche Point-Klasse, wobei die unveränderliche Klasse unveränderlich in beiden Dimensionen ist , d.h. nicht-polymorph und immutable. Mischformen wie polymorphe Klassen mit unveränderlichen Zustand oder nicht-polymorphe Klassen mit veränderlichen Zustand sind ebenfalls denkbar und können auch sinnvoll sein. Wichtig ist jedoch, daß man bei der Implementierung von final-Klassen und -Methode darauf achtet, daß der Benutzer typischerweise ein nicht-polymorphes Verhalten erwartet und daß dieses nicht automatisch durch die Qualifizierung mit final gegeben ist. Wenig intuitiv wäre z.B. eine final-Klasse, die sowohl polymorph als auch mutable ist, wie etwa die folgende Klasse: import java.awt.Point;
public final class ColoredPoint {
ColoredPoint (Point p, int c) {
// more constructors
public Point getLocation() {
// more methods }
Hier ist jede der gezeigten Methoden zwar final, weil die Klasse als
Ganzes final ist, aber jede der Methoden ist polymorph, weil die benutzte
Klasse java.awt.Point polymorph ist. Das ist nicht unbedingt das,
was eine Benutzer erwartet, wenn er eine final-Klasse sieht.
Zusammenfassung und AusblickDie Qualifizierung von Klassen und Methoden mit dem Schlüsselwort final bedeutet, daß die final-Klasse keine Subklassen haben kann und daß die final-Methoden nicht redefiniert werden können. Es bedeutet aber nicht, daß final-Klassen unveränderlich sind. Sowohl der Zustand als auch das Verhalten von final-Klassen können veränderlich sein. Unveränderlichkeit ist immer eine semantische Eigenschaft einer Klasse, die durch Mittel der Sprache nicht direkt ausgedrückt werden kann. In der Praxis sind nicht-polymorphe Typen selten – wesentlich seltener als das Mißverständnis, final-Klassen seien unveränderlich im Sinne von „immutable“ und/oder „nicht-polymorph“.
Im nächsten Artikel (siehe /
KRE2
/) kommen
wir noch einmal auf polymorphen Methoden zurück und erläutern,
warum Konstruktoren keine polymorphen Methoden der eigenen Klasse aufrufen
sollten.
Listings
Literaturverweise
|
|||||||||||||||||||
© Copyright 1995-2008 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/10.NonPolymorphicClasses/10.NonPolymorphicClasses.html> last update: 26 Nov 2008 |