Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | NEWSLETTER | CONTACT | Twitter | Lanyrd | Linkedin
 
HOME 

  OVERVIEW

  BY TOPIC
    JAVA
    C++

  BY COLUMN
    EFFECTIVE JAVA
    EFFECTIVE STDLIB

  BY MAGAZINE
    JAVA MAGAZIN
    JAVA SPEKTRUM
    JAVA WORLD
    JAVA SOLUTIONS
    JAVA PRO
    C++ REPORT
    CUJ
    OTHER
 

GENERICS 
LAMBDAS 
IOSTREAMS 
ABOUT 
NEWSLETTER 
CONTACT 
Non-Polymorphic Classes - final in Conjuction with Classes and Methods

Non-Polymorphic Classes - final in Conjuction with Classes and Methods
final-Klassen und final-Methoden
Über die Bedeutung des Schlüsselworts final im Zusammenhang mit Klassen und Methoden

JavaSPEKTRUM, September 2003
Klaus Kreft & Angelika Langer

Dies ist das Manuskript eines Artikels, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im JavaSPEKTRUM erschienen ist.  Die übrigen Artikel dieser Serie sind ebenfalls verfügbar ( click here ).

 

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.
Immutability
Wenn sich die Daten nicht ändern können, dann ist die Klasse unveränderlich.  Solche Klassen haben wir in den letzten beiden Artikeln betrachtet.  Wir haben diese Art von Klassen als „immutable“ Klassen bezeichnet.
Polymorphismus
Von Polymorphismus spricht man, wenn das Verhalten veränderlich ist.  Dabei gibt es verschiedene Umstände, unter den sich das Verhalten eines Objekts ändern kann.  Zum einen kann verändertes Verhalten daher rühren, daß sich der Zustand des Objekts geändert hat.  Veränderliches Verhalten ergibt sich aber auch, wenn die Methoden einer Klasse parametrisiert sind und beim Aufruf der Methode Argumente übergeben werden.  Dann zeigen die parametrisierten Methoden veränderliches Verhalten abhängig von den Aufrufparametern.  Diese Arten von veränderlichem Verhalten bezeichnet man nicht als polymorphes Verhalten.

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 Polymorphismus

Ein 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 {
  private SuperType field;

  SampleClass(SuperType arg) {
     field = arg;
  }
  public void someInnocentMethod() {
     field.doSomething();
  }
}

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());
SampleClass sample2 = new SampleClass(new SubType());

Sample1.someInnocentMethod();  // calls SuperType.doSomething()
Sample2.someInnocentMethod();  // calls SubType.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 {
  public void doSomething() {
    …
  }
  public final void someInnocentMethod() {
     doSomething();
  }
}

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.
 
Zitat aus der Java Language Specification
 
8.1.1.2 final Classes

A class can be declared final if its definition is complete and no subclasses are desired or required. A compile-time error occurs if the name of a final class appears in the extends clause (§8.1.3) of another class declaration; this implies that a final class cannot have any subclasses. A compile-time error occurs if a class is declared both final and abstract, because the implementation of such a class could never be completed (§8.1.1.1). Because a final class never has any subclasses, the methods of a final class are never overridden (§8.4.6.1).

8.4.3.3 final Methods

A method can be declared final to prevent subclasses from overriding or hiding it. It is a compile-time error to attempt to override or hide a final method. A private method and all methods declared in a final class (§8.1.1.2) are implicitly final, because it is impossible to override them. It is permitted but not required for the declarations of such methods to redundantly include the final keyword.  It is a compile-time error for a final method to be declared abstract.

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 {
    private Point point;
    private int   color;

    ColoredPoint (Point p, int c) {
      point = p.clone();
      color = c;
    }

    // more constructors

  public Point getLocation() {
     return point.getLocation();
  }
  public void setLocation(Point p) {
     point.setLocation(p);
    }

    // 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 Ausblick

Die 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

 
Listing 1: Eine polymorphe, mutable Point-Klasse  Listing 2: Eine nicht-polymorphe, immutable Point-Klasse

public class Point implements Cloneable {
   public int x;
   public int y;

   public Point() {
     this(0, 0);
   }
   public Point(Point p) { 
     this(p.x, p.y);
   }
   public Point(int x, int y) { 
     this.x = x; 
     this.y = y;
   }
   public double getX() { 
     return x;
   }
   public double getY() { 
     return y;
   }
   public Point getLocation() { 
     return new Point(x, y);
   }
   public void setLocation(Point p) { 
     setLocation(p.x, p.y);
   }
   public void setLocation(int x, int y) { 
     this.x = x; 
     this.y = y;
   }
   public void translate(int dx, int dy) { 
     this.x += dx; 
     this.y += dy;
   }
   public double distance(Point pt) { 
     double PX = pt.getX() - this.getX(); 
     double PY = pt.getY() - this.getY(); 
     return Math.sqrt(PX * PX + PY * PY);
   }
   public Object clone() { 
     try {
       return super.clone(); 
     } catch (CloneNotSupportedException e) {
       // this shouldn't happen, since we are Cloneable
       throw new InternalError(); 
     }
   }
   public int hashCode() { 
     int hc = 17; 
     int hashMultiplier = 59; 
     hc = hc*hashMultiplier + x; 
     hc = hc*hashMultiplier + y; 
     return hc;
   }
   public boolean equals(Object obj) { 
     if (obj == null)
       return false; 
     if (getClass() != obj.getClass()) 
       return false; 
     Point pt = (Point)obj;
     return (x == pt.x) && (y == pt.y);
   }
   public String toString() { 
     return getClass().getName()
            + "[x=" + x + ",y=" + y + "]";
   }
}
 


public final class Point implements Cloneable {
   public int x;
   public int y;

   public Point() {
     this(0, 0);
   }
   public Point(Point p) { 
     this(p.x, p.y);
   }
   public Point(int x, int y) { 
     this.x = x; 
     this.y = y;
   }
   public double getX() { 
     return x;
   }
   public double getY() { 
     return y;
   }
   public Point getLocation() { 
     return new Point(x, y);
   }
 
 

   // mutating methods eliminated
 
 
 
 
 
 

   public double distance(Point pt) { 
     double PX = pt.getX() - this.getX();
     double PY = pt.getY() - this.getY(); 
     return Math.sqrt(PX * PX + PY * PY);
   }
   public Object clone() { 
     try {
       return super.clone(); 
     } catch (CloneNotSupportedException e) { 
       // this shouldn't happen, since we are Cloneable
       throw new InternalError(); 
     }
   }
   public int hashCode() { 
     int hc = 17; 
     int hashMultiplier = 59;
     hc = hc*hashMultiplier + x;
     hc = hc*hashMultiplier + y;
     return hc;
   }
   public boolean equals(Object obj) { 
     if (obj == null)
       return false;
     if (getClass() != obj.getClass())
       return false; 
     Point pt = (Point)obj;
     return (x == pt.x) && (y == pt.y);
   }
   public String toString() { 
     return getClass().getName()
            + "[x=" + x + ",y=" + y + "]";
   }
}

Literaturverweise

 
/KRE1/ Unveränderliche Typen in Java (Teil 1-2) 
Klaus Kreft & Angelika Langer
JavaSpektrum, März + Juli 2003
http://www.AngelikaLanger.com/Articles/EffectiveJava/08.Immutability-Part1/08.Immutability-Part1.html
http://www.AngelikaLanger.com/Articles/EffectiveJava/09.Immutability-Part2/09.Immutability-Part2.html
/KRE2/ Aufruf polymorpher Methoden im Konstruktor
Klaus Kreft & Angelika Langer
JavaSpektrum, November 2003
http://www.AngelikaLanger.com/Articles/EffectiveJava/11.PolyMethodsInCtor/11.PolyMethodsInCtor.html
/ARN/ The Java Programming Language, 3rd Ed.
Ken Arnold, James Gosling, David Holmes
Addison-Wesley, 2000

 
 

If you are interested to hear more about this and related topics you might want to check out the following seminar:
Seminar
 
Effective Java - Advanced Java Programming Idioms 
4 day seminar ( open enrollment and on-site)
 
  © 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