Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


Java 2 von Friedrich Esser
Designmuster und Zertifizierungswissen
Zum Katalog
gp Kapitel 5 Vererbung
  gp 5.1 Deklaration von Subklassen
  gp 5.2 Overriding vs. Overloading
  gp 5.3 Superklasse Object
  gp 5.4 Vererbung und Modifikatoren
  gp 5.5 Konstruktoren
  gp 5.6 Pros und Kons der Initialisierung
  gp 5.7 Initialisierer
  gp 5.8 Overriding vs. Shadowing
  gp 5.9 Speicherverwaltung
  gp 5.10 Kapselung (Encapsulation)
  gp 5.11 Zusammenfassung
  gp 5.12 Testfragen

Kapitel 5 Vererbung

Zur UML-Spezialisierung des letzten Kapitels wird nun die zugehörige Java-Technologie vorgestellt.
Schwerpunkte bilden die Unterschiede von Overriding zu Overloading bzw. Shadowing, der Einfluss von Modifikatoren auf die Vererbung sowie Objekt-Anlage und -Zerstörung mittels Konstruktoren, Initialisierung und Finalization.
Vererbung, Modifikatoren und Objekt-Erzeugung sind mit wichtigen Regeln verbunden, die anhand von Beispielen, Diagrammen und Idiomen erklärt werden.

Spezialisierung wird in Java mittels Vererbung (Inheritance) realisiert.

Innerhalb der Klassenhierarchie werden generelle Klassen als Ober- oder Superklassen bezeichnet. Wird aus einer Klasse eine spezielle abgeleitet, wird diese direkte Unterklasse bzw. Subklasse genannt.

Subklasse vs.
echte Subklasse

Da in Java genau genommen jede Klasse Subklasse von sich selbst ist, müssten alle anderen echte oder strikte Subklassen genannt werden. Im Weiteren wird aber der Einfachheit halber unter einer Super- bzw. Subklasse immer eine echte bzw. strikte verstanden.


Galileo Computing

5.1 Deklaration von Subklassen  downtop

Mit Hilfe des Schlüsselwortes extends realisiert Java Subklassen als einfache Vererbung (single inheritance), d.h., zu einer Subklasse gehört nur genau eine Superklasse.

Deklaration einer Subklasse

Dagegen ist die Implementierung von beliebig vielen Interfaces mittels des Schlüsselworts implements möglich :

[cModifiers] class SubClassName 
[extends SuperClass]
[implements Interface1, ...]
{...}

Die Prinzipien der Generalisierung/Spezialisierung (siehe 4.1), insbesondere die Substitutions-Regel und die dynamische Polymorphie, werden durch die Vererbung in Java wie folgt realisiert.


Subklassen erben Instanz-Variable der Superklasse.

Abbildung
Abbildung 5.1   Die Subklasse D erbt Instanz-Variable von B

Es sei D eine direkte Subklasse von B (siehe Abb. 5.1). Dann gilt folgende Regel:

Icon
Zugriff auf Member der Superklasse

Vererbungs-Regel: Die Klasse D, und somit jede Instanz von D, »erbt« alle Instanz-Variablen von B. D kann direkt auf alle statischen oder nicht statischen Variablen und Methoden zugreifen, die in B nicht private erklärt sind.

Beispiel (zu Abb. 5.1)

class B {
  static String bs= "static";
  private char bc= 'b';   // wird vererbt, aber ohne Zugriff
  int bi=2
  static String bsf() { return bs+" bsf"; }
  private char bpf() { return Character.toUpperCase(bc); }
  String bf() { return String.valueOf(bc) + bpf()+ bi; }
}
class D extends B {
  void test() { // Zugriff auf (nicht) statische 
Member
    System.out.println(bs+","+bi+","+bsf()+","+bf());
    // System.out.println(bc + "," + bpf());   ß C-Fehler
  }
}

Der Aufruf von test() über eine Instanz von D ergibt dann:

new D().test(); // :: static,2,static bsf,bB2

Obwohl in D keine Member angelegt werden (die Klasse scheint also leer zu sein), erbt sie alle Instanz-Variablen von B und kann auf alle (nicht private) Member von B zugreifen, als wären sie in D selbst deklariert.

Die o.a. Regel gilt nicht nur für eine direkte Superklasse, sondern auch für eine Klassen-Hierarchie.

Vererbung ist transitiv

gp  Die Member-Vererbung ist transitiv.

Abbildung
Abbildung 5.2   Vererbungs-Hierarchie

Ist D Subklasse von Superklassen Bi, so enthält D alle Instanz-Variablen der Superklassen Bi (siehe Abb. 5.2).

Die Klasse D kann auch auf alle (nicht private) Member der Superklassen Bi zugreifen, wobei dies allerdings uneingeschränkt nur für die direkte Superklasse B1 gilt (siehe hierzu 5.8.3).

Die Substitutions-Regel aus dem vierten Kapitel führt zur

Icon

Widening-Conversion-Regel: Referenzen bzw. Instanzen von D können überall da eingesetzt werden, wo als Typ irgendeine Superklasse B erwartet wird.

Referenzen von Superklassen zeigen auf alle Instanzen in der Klassen-Hierarchie

Insbesondere kann

gp  eine Referenz vom Typ B auf eine Instanz von D verweisen:
    B b= new D();
gp  ein Parameter vom Typ B durch ein Argument von D ersetzt werden:
    method(new D());  
    // Methode ist deklariert als: void method(B b) {...}

Erklärung: Da eine Instanz von D alle Felder von B enthält und alle Methoden von B »versteht«, kann sie jederzeit anstelle einer Instanz von B stehen. Die Umkehrung gilt natürlich nicht, da die Superklasse B von zusätzlichen Feldern und Methoden in D nichts »weiß«.

Member-Vererbung und Widening-Conversion haben weit reichende Folgerungen, die im Weiteren detaillierter besprochen werden.


Galileo Computing

5.2 Overriding vs. Overloading  downtop

Die Neuimplementierung einer geerbten Methode nennt man Overriding (Überschreiben).

Overriding
in Subklassen

gp  Overriding gibt es nur in Subklassen.
gp  Overriding verlangt nicht nur dieselbe Signatur (siehe 3.4.3), sondern auch denselben Rückgabetyp wie die Methode der Superklasse.

Abbildung
Abbildung 5.3   Overriding einer Methode f()

Icon
Overriding stellt immer die passende Methode sicher

Overriding-Regel: Überschreibt die Klasse D eine Instanz-Methode von B, so wird zu einer Instanz von D nur die Methode von D und nicht die von B aufgerufen.

Diese Overriding-Regel gilt uneingeschränkt! Auch über eine Referenz vom Typ B oder mittels Casts nach B ist es nicht möglich, zu einer Instanz von D eine Instanz-Methode von B aufzurufen.

Beispiel (zu Abb. 5.3)

class B {          void f(int i) {System.out.println(i);} 
 }
class D extends B {void f(int i) {System.out.println(i*i);}}
class OverridingTest {
  static void show (B b) { b.f(2); }
  static void test () {
    D d= new D();
    B b= d;       // Instanz von D über b referenzieren
    show (b);     // trotz Argument b  :: 4
((B)d).f(2); // trotz Cast nach B :: 4
} }

Der Aufruf von OverridingTest.test(); erzeugt als Ergebnis immer 4.

Overriding ist
ein sinnvoller Mechanismus

Erklärung: Die Overriding-Regel ist sinnvoll. Eine Methode in einer Subklasse wird nur dann überschrieben, wenn das Verhalten der Methode der Superklasse nicht mehr gültig ist. Würde also die Overriding-Regel nicht gelten, wäre es mittels Casts oder Referenz möglich, zu Objekten ungültige Methoden aufzurufen.

Overloading

Overloading:
Statische Polymorphie

Im Gegensatz zu Overriding bedeutet Overloading (Überladen) in einer Klasse die Wiederverwendung des Namens für Methoden mit verschiedener Signatur.

Overriding:
Dynamische
Polymorphie

Der Compiler kann bereits beim Übersetzen anhand der unterschiedlichen Signaturen die einzelnen Methoden mit gleichem Namen unterscheiden.

gp  Overriding ist somit unbedingt von Overloading zu unterscheiden.

Die Signatur-Regel in Abschnitt 3.4.3 wird für Klassen-Hierarchien erweitert.

Icon
Signatur in Hierarchien

Erweiterte Signatur-Regel: In einer Klassen-Hierarchie können keine zwei Methoden mit derselben Signatur vorkommen, es sei denn, eine Methode in der Subklasse überschreibt eine andere in einer Superklasse.

Beispiel

class B {
  String s= "B";
  void f(int i) { System.out.println(i); }
  void g() {}
}
class D extends B {
  // int g() {}                      ß Compiler-Fehler 
   ¨
  void f(byte b) { System.out.println(b); }                ¦
  void f(int i)  { System.out.println(s+" "+i); }
}
public class Test {
  public static void main(String[] args) {
    D d= new D();
    B b= d;                                                ¬
    b.f((byte)1);                               
                   ::  B 1      Ø
    d.f((byte)1);                                                  :: 
 1   
d.f(1); :: B 1
} }

Erklärung:

Zu ¨: Nach der Signatur-Regel hat die Methode int g() in D dieselbe Signatur wie void g() in B, aber überschreibt sie nicht. Dies erzeugt einen Compiler-Fehler.

Zu ¦: Beide Methoden f() in D sind erlaubt. Die erste hat unterschiedliche Signatur, die zweite überschreibt f() in B.

Zu Æ: Aufgrund von Widening Conversion kann eine Instanz von D überall da stehen, wo ein Typ B erwartet wird.

Zu Ø: Eine Referenz b vom Typ B kennt nur eine Methode f(int i), ruft aber nach der Overriding-Regel die Methode f(int i) von D auf.

Die Referenz d vom Typ D kennt zwei verschiedene Methoden mit Namen f und ruft deshalb die zum Argument passende auf.


Galileo Computing

5.3 Superklasse Object  downtop

Object, die
ultimative Superklasse

Jede Klasse, die man deklariert, hat eine Superklasse, auch wenn keine Superklasse mittels extends angegeben wird.

gp  Alle Klassen haben als Superklasse die Klasse Object, die selbst keine Superklasse besitzt.

Damit erbt jede Klasse automatisch alle elf Instanz-Methoden der Klasse Object. Hierzu gehören auch die beiden schon besprochenen wichtigen Methoden:

         public boolean equals(Object 
obj)
         public String toString()

Um deren Verhalten an die jeweilige Klasse anzupassen, müssen diese Methoden überschrieben werden.

Icon
Verwechslung von Overloading und Overriding

class Complex {
  double re,im;
  //...
  public boolean equals(Complex c) {
    return re==c.re && im==c.im;
  }
  //...
}

Obwohl die Methode equals() gut aussieht, hat sie die Methode aus Object nicht überschrieben, sondern nur überladen. Dies kann – je nach Code – zu subtilen semantischen Fehlern führen.


Galileo Computing

5.4 Vererbung und Modifikatoren  downtop

Nachfolgend werden die Modifikatoren in Hinblick auf die Vererbung besprochen, d.h., welchen Einfluss sie auf die Vererbung haben bzw. welche Semantik damit verbunden ist.


Galileo Computing

5.4.1 abstract  downtop

In Java hat der Modifikator abstract dieselbe Bedeutung wie in der UML (siehe 4.2.6) und kann auf Klassen und Instanz-Methoden angewendet werden.

abstract: keine Instanzen möglich

Von einer Klasse, die abstrakt erklärt wird, können keine Instanzen angelegt werden. Wird dagegen eine Methode abstrakt erklärt, hat sie keinen Methoden-Rumpf, d.h. keine Implementierung.

Icon

Eine Klasse muss abstrakt erklärt werden, wenn sie unvollständig ist, genauer gesagt, wenn sie

abstract:
notwendig bei Unvollständigkeit

gp  eine abstrakte Methode enthält oder erbt, die sie nicht implementiert.
gp  im Klassenkopf ein Interface enthält, deren Methoden sie aber nicht vollständig implementiert.

Ansonsten kann man eine Klasse abstrakt erklären, auch wenn sie keine abstrakten Methoden enthält.

Dies macht dann Sinn, wenn die Klasse zwar eine Superkasse ist, aber noch so unvollständig ist, dass eigene Instanzen ziemlich nutzlos sind.


Galileo Computing

5.4.2 final  downtop

final: keine Subklassen möglich

Zu einer final deklarierten Klasse kann es keine Subklassen mehr geben. Eine final deklarierte Methode kann in einer Subklasse nicht mehr überschrieben werden.

Dieser Modifikator wird dafür eingesetzt, sicherzustellen, dass aufgrund einer Vererbung nichts mehr geändert werden kann. Deshalb ist final für Klassen bzw. Methoden mit Vorsicht einzusetzen, da es auch Erweiterungen ausschließt. Andererseits dient es zur Geschwindigkeits-Optimierung (siehe 5.8.1).

Die Klassen Math, System und die Wrapper der primitiven Typen sind häufig benutzte final deklarierte Klassen im Package java.lang.


Galileo Computing

5.4.3 Zugriffs-Modifikatoren (Access-Modifier)  downtop

Zugriffs-
Modifikatoren: für Package-Ebene und Member

Die drei Modifikatoren public, protected, private sowie der »ohne Namen« regeln den Zugriff auf Klassen, Interfaces und Member.

Es ist zwischen dem Zugriff auf Klassen und Interfaces, die auf Package-Ebene deklariert sind, und dem Zugriff auf ihre Member – Felder und Methoden – zu unterscheiden.

Zugriff auf Klassen/Interfaces (auf Package-Ebene)

Für Klassen und Interfaces auf Package-Ebene ist entweder kein Modifikator oder nur public erlaubt.

Auf Package-Ebene: nur public oder default

Ohne Modifikator können Klassen und Interfaces nur in dem Package verwendet werden, in dem sie deklariert sind. Dies nennt man (default) Package-Zugriff oder auch friendly Zugriff.

Wird eine Klasse/Interface public deklariert, kann sie in jedem Package verwendet werden.


Abbildung
Abbildung 5.4   Inter-Package-Zugriff (UML-Darstellung)

Zugriff auf Interface-Member

Interface-Member: immer public Methoden: abstract Felder: static final

Interface-Methoden sind implizit public abstract und können auch nur explizit public und/oder abstract deklariert werden (was somit ziemlich unnötig ist).

Interface-Felder sind implizit public static final und können auch nur explizit public und/oder static und/oder final deklariert werden (was somit wiederum unnötig ist).

Damit bieten alle Member eines Interfaces überall da Zugriff, wo das Interface selbst Zugriff bietet (d.h. im Package oder auch außerhalb).

Zugriff auf Klassen-Member

Klassen-Member:
drei Modifikatoren – vier Zugriffsarten

Der Zugriff auf Klassen-Member kann mit Hilfe der Zugriffs-Modifikatoren (Access-Modifiers) kontrolliert werden:

gp  public: Die Member einer Klasse bieten überall da Zugriff, wo auch die Klasse Zugriff bietet (im Package oder auch außerhalb).
gp  protected: Die Member einer Klasse bieten im Package Zugriff und auch innerhalb einer Subklasse außerhalb des Packages, sofern die Klasse public erklärt ist.
gp  kein Modifikator (default/friendly/Package-Zugriff): Die Member einer Klasse bietenr alle Klassen desselben Packages Zugriff.
gp  private: Die Member bieten nur innerhalb der eigenen Klasse Zugriff.

Damit werden also vier verschiedene Stufen der Zugriffsrestriktion auf Member definiert.

Überschreiben der Zugriffs-Modifikatoren

Die Modifikatoren können nach der Stärke ihrer Zugriffsrestriktion geordnet werden (siehe Abb. 5.5).

Ordnung der Zugriffsarten

Diese Ordnung ist dann wichtig, wenn man in einer Subklasse für eine überschriebene Methode die Zugriffsart abweichend von derjenigen in der Superklasse setzen will.


Abbildung
Abbildung 5.5   Erlaubte Richtung der Zugriffsänderung bei Overriding

Icon

Bei Overriding gilt folgende Regel für Zugriffs-Modifikatoren.

Änderung der Zugriffsart in Subklassen

Access-Widening: Wird eine Methode überschrieben, kann der Zugriffs-Modifikator der Superklasse in der Subklasse nur mit einem Zugriffs-Modifikator überschrieben werden, wenn dieser gleich oder weniger restriktiv ist (d.h. in Pfeilrichtung bei Abb. 5.5).

Diese Regel ist für Widening-Conversion notwendig, d.h. um zu verhindern, dass Methoden durch Überschreiben plötzlich keinen Zugriff mehr bieten.

Package-Grenzen-Problematik bei protected:

Werden Subklassen über Package-Grenzen hinweg deklariert, hat der Modifikator protected einen kleinen logischen »Twist«.

protected: Vorsicht bei unterschiedlichen Packages

Der Modifikator protected lässt den Zugriff nur auf direkt geerbte Member desselben Objekts zu, nicht jedoch auf protected Member in anderen Objekten derselben Superklasse.


Beispiel

Im linken Kasten ist eine Klasse Base im Package p1 erklärt. Im rechten Kasten ist eine Klasse Derived im Package p2 von Base abgeleitet. Die Klasse Derived kann damit zwar auf das instanzeigene protected int i zugreifen, nicht jedoch auf ein i einer anderen Instanz.

package p1;
public class Base {
  protected int i= 2;
}
package p2;
class Derived extends p1.Base {
  void foo(p1.Base o) {
    System.out.println(i+ o.i);   ¨
  }
}

Erklärung: Die Zeile ¨ erzeugt einen Compiler-Fehler, verursacht durch o.i. Der Zugriff auf das eigene geerbte protected Feld i ist erlaubt, nicht jedoch auf das Feld i eines anderen Objekts derselben Klasse.


Galileo Computing

5.4.4 Unverträglichkeiten von Modifikatoren  downtop

Klassen:
abstract vs. final

Bei Klassen kann der Modifikator final (keine Subklasse erlaubt!) nicht zusammen mit abstract (Subklasse notwendig!) verwendet werden.

Member:
abstract vs. final, static, private

Bei Methoden können die Modifikatoren final, static und private nicht zusammen mit abstract verwendet werden. Abstrakte Methoden verlangen ein Overriding, wogegen final und private dies gerade ausschließen.

Die Modifikatoren abstract und static schließen sich dadurch aus, dass statische Methoden nicht im Sinne von Instanz-Methoden überschrieben , sondern nur verdeckt werden können (siehe hierzu 5.8.1).


Galileo Computing

5.5 Konstruktoren  downtop

Konstruktoren: eine eigene Methoden-Familie

Bei der Anlage einer Instanz wird direkt nach Allokation des Speichers durch die JVM ein Konstruktor (constructor) aufgerufen, der die Initialisierung des neu erschaffenen Objekts übernimmt.

Konstruktoren bilden syntaktisch eine eigene Gruppe von Methoden und unterliegen damit auch anderen Regeln als die normalen Instanz-Methoden (siehe auch 3.4.3 und 5.5.3).

Icon
Klassen ohne Konstruktoren sind unmöglich

Es gilt folgende einfache Regel:

gp  Es gibt keine Klasse ohne Konstruktor. In jeder Klasse, auch in einer abstrakten, existiert zumindest ein Konstruktor.

Galileo Computing

5.5.1 Default-Konstruktor  downtop

Default-
Konstruktor: vom System geschrieben

Der genannten Regel wird dadurch entsprochen, dass in einer Klasse, in der kein Konstruktor explizit deklariert ist, der Compiler automatisch einen so genannten Default-Konstruktor (default constructor) anlegt.

Ein Default-Konstruktor hat keine Parameter und hat denselben Zugriffs-Modifikator wie seine Klasse.

Somit können ohne Anlage eines Konstruktors von jeder Klasse C Instanzen mit new C() angelegt werden.


Galileo Computing

5.5.2 Deklaration und Initialisierungs-Regeln  downtop

Deklaration eines Konstruktors

Legt man explizit Konstruktoren an, müssen sie immer den Namen der Klasse tragen und dürfen keinen Typ als Rückgabe definieren. Damit hat ein Konstruktor die folgende Deklaration:

[accessModifier] ClassName (parameterList) [throwsList] {...}

Beispiele

No-Arg-Konstruktor: ein
Konstruktor ohne Argumente

class Point {
  int x, y;
  Point() {}               // analog zum Default-Konstruktor
  Point(int x, int y) { this.x= x; this.y= y; }
}
abstract class Figure {     // abstrakt!
  Point base;
  Figure (int x,int y) {
    base= new Point(x,y);  // legt einen Basispunkt (x,y) an
  }
}

Icon

Folgende Konstruktoren-Regeln sind zu beachten:



Zusammenstellung aller wichtigen Regeln zum Konstruktor

1. Wird in einer Klasse (explizit) ein Konstruktor mit Parameter deklariert, legt der Compiler keinen Default-Konstruktor mehr an.
2. Ruft ein Konstruktor einen anderen derselben Klasse auf, so erfolgt dies mit der Anweisung

this(argumentList);

wobei argumentList zur parameterList des aufgerufenen Konstruktors passen muss.
3. Ruft ein Konstruktor einen anderen der direkt übergeordneten Superklasse auf, so erfolgt dies mit der Anweisung

super(argumentList);

wobei argumentList zur parameterList des aufgerufenen Kon-struktors passen muss.
4. Sofern verwendet, muss this(); oder super(); die erste Anweisung in einem Konstruktor sein, d.h., sie können nicht beide gleichzeitig verwendet werden.
5. Verwendet man weder this() noch super(), fügt der Compiler als erste Anweisung super(); ein, d.h., es wird zuerst der No-Arg-Konstruktor der direkt übergeordneten Superklasse aufgerufen.
6. Für die Felder, die nicht im Konstruktor initialisiert werden, wird vom Compiler automatisch Code generiert. Dieser Code wird nach der Ausführung des Superklassen-Konstruktors und vor dem Code des eigenen Konstruktors ausgeführt. Dabei wird die Reihenfolge der Variablen beachtet.
7. Konstruktoren werden nicht vererbt, jede Subklasse muss ihre eigenen Konstruktoren anlegen.
8. Blank final Instanz-Variablen müssen im Konstruktor oder Instanz-Initialisierer (siehe 5.7.1) einen Wert zugewiesen bekommen.

Reihenfolge der Konstruktoren
bei Klassen- Hierarchie

Eine Instanz enthält alle internen Instanzen der Superklassen, die bei einer mehrstufigen Hierarchie wie Schalen einer Zwiebel ineinander liegen.

Vom Compiler wird sichergestellt, dass zuerst immer die Felder der Superklasse initialisiert werden, und zwar (von innen nach außen) in der Reihenfolge der Klassen-Hierarchie. Dies gewährleistet die fünfte Regel (siehe Abb. 5.6).

Beispiel

Abfolge der Konstruktoren-Aufrufe zu den Klassen in Abb. 5.6.

Zwiebelschalen-Modell mit
Konstruktoren-Reihenfolge


Abbildung
Abbildung 5.6   Reihenfolge der Konstruktoren-Aufrufe
class B {
  protected int b= 3;
  B() { System.out.print("B b:"+b); }  // siehe 6. Regel        ¨
}
class C extends B {
  protected int c;
  C(int i) {System.out.print("C "); c= b+i; } // siehe 5. Regel ¦
}
class D extends C{
  protected int d;
  D(int i,int j) { super(j);          // siehe 3. & 4. Regel
    System.out.print("D "); d= c+i;  
    System.out.println("b:"+b+" c:"+c+" d:"+d);
  }
}
public class Test {
  public static void main(String[] args) {
    new D(1,2);                     // :: B b:3 C D c:5 d:6
  }
}

Erklärung: In ¨ ist b bereits mit 3 initialisiert, in ¦ steht aufgrund der fünften Regel ein implizites super(); als erste Anweisung. Jede Subklasse kann im Konstruktor bereits auf die initialisierten Werte ihrer Superklasse zugreifen.


Galileo Computing

5.5.3 Signatur-Regel für Konstruktoren  downtop

Unterscheidung der Konstruktoren anhand der Signatur

Das Overloading der Konstruktoren unterliegt der einfachen Signatur-Regel, d.h., Konstruktoren müssen untereinander anhand der Sequenz ihrer Parametertypen zu unterscheiden sein (siehe auch 5.6.2, Fall 2).


Galileo Computing

5.5.4 Zusammenarbeit von Konstruktoren  downtop

super() erlaubt Zusammenarbeit der Konstruktoren in der
Hierarchie

Konstruktoren können über Klassengrenzen hinweg zusammenarbeiten.

Beispiel

class Point {
   // wie in 5.5.2
}
class Figure { 
  Point base;
  // Figure() {}                                
            ¨
  Figure (int x,int y) {
    // Figure enthält einen Punkt, also nicht 
    // super(x,y) sondern new Point() 
    base= new Point(x,y); 
  }
}

Vorsicht bei Default-
Konstruktoren

class Circle extends Figure {
   int radius;            // Compiler-Fehler: 2. & 5. Regel
   // Circle(int x,int y,int r) { super(x,y); radius=r; }   ¦
}                             

Erklärung: Nach der zweiten Regel gibt es in Figure keinen Default-Konstruktor. In Circle gibt es nur einen Default-Konstruktor, der nach der fünften Regel als erste Anweisung implizit ein super() enthält. Dies führt zur Compiler-Meldung »constructor Figure() not found«.

Der Fehler kann durch Entfernen der Kommentar-Symbole in Zeile ¨ und/oder ¦ behoben werden:

Fügt man nur den Konstruktor in ¨ ein (und nicht ¦), stellt man fest, dass er nicht sehr gut passt:

System.out.println(new Circle().base.x);  // Laufzeitfehler!

Figure() setzt nämlich keinen Basispunkt wie Figure(int x,int y), sondern überlässt die Initialisierung von base dem System:

System.out.println(new Circle().base);    // :: 
null

Galileo Computing

5.6 Pros und Kons der Initialisierung  downtop


Galileo Computing

5.6.1 Design-Pros  downtop

Icon

Die Initialisierung bei der Deklaration der Variablen steht im Gegensatz zur Initialisierung erst im Konstruktor. Die Frage ist dann, was zu bevorzugen ist. Die Antwort lautet »Klarheit«:

Direkte Initialisierung vs. Konstruktoren

Initialisierungs-Prinzip: Sofern nicht im Konstruktor ohnehin eine (Re-) Initialisierung stattfindet, sollten die Instanz-Felder direkt bei der Deklaration initialisiert werden.


Andererseits – werden die Felder in einem Konstruktor wieder auf andere Werte, z.B. die der Argumente gesetzt – verkehrt sich die Klarheit ins Gegenteil:

class BadIdea {
  String s="";                       // sinnlos!
  BadIdea(String s) { this.s= s; }
}

Icon

Innerhalb einer Klasse arbeiten Konstruktoren auf der Basis von Constructor-Chaining zusammen:

Constructor-
Chaining: Vermeidung von Code-Redundanz

Constructor-Chaining: Um Code-Replikationen zu vermeiden, sollten einfachere Konstruktoren komplexere aufrufen, wobei die einfacheren vor den komplexeren Konstruktoren deklariert werden.


Beispiel

class IntMatrix {
  int[][] m;
  IntMatrix(int dim)            { this(dim,dim); 
}
  IntMatrix(int rows, int cols) { this(rows,cols,0); }
  IntMatrix(int rows, int cols, int val) {
    m= new int[rows][cols];
    if (val!= 0) setVal(val);     // Aufruf einer Methode
  }
  void setVal(int val) {
    for (int i= 0; i<m.length; i++)
      for (int j=0; j<m[i].length; j++) m[i][j]= val;
  }
}

Die Klasse IntMatrix deklariert drei Konstruktoren, geordnet nach Detaillierung. Es können quadratische und nicht quadratische Matrizen mit bzw. ohne Wert angelegt werden:

   IntMatrix imat1= new IntMatrix();          // 
C-Fehler 
   IntMatrix imat2= new IntMatrix(3);
   IntMatrix imat3= new IntMatrix(3,4);    
   IntMatrix imat4= new IntMatrix(3,4,-1);

Der letzte Konstruktor demonstriert, dass in Konstruktoren durchaus andere Instanz-Methoden aufgerufen werden können.

Gemeinsame Initialisierungs-Methoden

Icon

Der Aufruf einer Instanz-Methode eignet sich auch vorzüglich, um das Problem zu lösen, dass man nur super() oder this() verwenden darf, aber nicht beides, obwohl dies manchmal sinnvoll wäre.

Das Codierungs-Prinzip ist einfach:

Umgehung der Beschränkung für super() und this()

gp  Man verwendet super() und ruft anschließend eine Initialisierungs-Methode auf, in die man den in den Konstruktoren gemeinsam genutzten Code ausgelagert hat.

Denn eine Initialisierungs-Methode muss man nicht unbedingt als erste Anweisung ausführen.


Galileo Computing

5.6.2 Design-Kons  downtop

Icon

Das Folgende ist zwar zulässig, aber zu vermeiden, da seine Wirkung nicht klar ist.

Vorsicht:
abstrakte Methoden im Konstruktor

Ein Konstruktor ruft eine abstrakte Methode auf, die Felder der Subklasse ändert.

Beispiel

abstract class Money {
  Money() { set(1.0); }            // Wirkung unklar!
  abstract void set(double money);
}
class Euro extends Money {
  private double euro /*= 0.0*/ ;        // siehe Erklärung
  void set(double money) { euro= money; }
  double get()           { return euro; }
}

Der Fall ist dubios, da primitive Typen wie euro eigentlich automatisch auf 0.0 initialisiert werden. Doch in diesem Fall sind die beiden Deklarationen unterschiedlich:

   private double euro;              ¨
   private double euro= 0.0;         ¦

Die Ausgabe ergibt:

   System.out.println(new Euro().get());  // ¨ 
:: 1.0 
                                          //  ¦  :: 0.0

Der Money-Konstruktor wird immer vor dem Euro-Konstruktor aufgerufen. Der Default-Konstrukor von Euro – explizit geschrieben – sieht wie folgt aus:

   Euro() { super(); }

Bei ¨ wird der Wert also auf 1.0 gesetzt.

Bei ¦ fügt, gemäß der sechsten Regel in Abschnitt 5.5.2, der Compiler nach super(); allerdings noch die Anweisung euro= 0.0; ein.

Konstruktoren vs. normale Methoden

Icon

Der Compiler unterscheidet zwischen Konstruktoren und normalen Instanz-Methoden.

Falle:
void-Konstruktoren

gp  Es können Instanz-Methoden deklariert werden, die den Namen der Klasse haben und sogar dieselbe Signatur wie ein Konstruktor.

Das ist zulässig , aber unsinnig, und geschieht meistens dadurch, dass man aus Versehen void vor einen Konstruktor geschrieben hat.

Beispiel

class Nonsense {
  Nonsense()  { System.out.println("Konstruktor-Nonsense"); }
  void Nonsense() { System.out.println("Methode-Nonsense"); }
}

Instanz-Methode und Konstruktor haben dieselbe Signatur und können nun wie folgt aufgerufen werden:

Nonsense u= new Nonsense();    // :: Konstruktor-Nonsense
u.Nonsense();                  // :: Methode-Nonsense

Galileo Computing

5.7 Initialisierer  downtop

Konstruktoren vs. anonyme Klassen

Da Konstruktoren immer den Namen der Klasse tragen müssen, entstand nach Einführung der anonymen Klassen in Java 1.1 das Problem der Namensfindung für einen expliziten Default-Konstruktor.

Des Weiteren sind Konstruktoren ungeeignet, um statische Variablen zu initialisieren (siehe 5.7.2).


Galileo Computing

5.7.1 Instanz-Initialisierer   downtop

Die Lösung des ersten Problems war ein Konstruktor ohne Namen für eine anonyme Klasse.

Alternative:
Instanz- Initialisierer

Ein Instanz-Initialisierer (instance initializer) besteht nur aus geschweiften Klammern, dem Initialisierungsblock:

        class C {
          { 
            // Initialisierungs-Block 
          }
        }

Instanz-
Initialisierer: Rolle eines Default- Konstruktors

Ein Instanz-Initialisierer hat keine Parameter, entspricht also dem Default-Konstruktor.

In einer Klasse können sogar mehrere Instanz-Initialisierer vorkommen. Sinnvoll und notwendig sind sie aber nur für anonyme Klassen und auch hier sollte man sich auf einen beschränken.

Icon

Es gelten folgende Regeln:

Abfolge bei:
Direkt-Initialisierung Instanz- Initialisierer Konstruktoren

1. Bei der Anlage einer Instanz wird der Code im Instanz-Initialisierer vor dem Code eines Konstruktors ausgeführt.
2. Die Variablen, die im Instanz-Initialisierer verwendet werden, müssen vor diesem deklariert sein.
3. Die Initialisierung von Variablen direkt bei der Deklaration und in Instanz-Initialisierern erfolgt in der Reihenfolge der Angaben.

Beispiel

Um die Regeln abzudecken, ist die folgende Klasse IntVector konstruiert und verstößt gegen gutes Design.

class IntVector {
  int[] v;
  { 
    System.out.println("Initialisierer");
    //standardVal= 1;                          ¨
    v= new int[standardDim];  // ok: static
  }
  IntVector () {
    System.out.println("default");
    java.util.Arrays.fill(v,standardVal);
  }
  static int standardDim= 5;  // siehe 5.7.2
  int standardVal= 1;
}

Erklärung: Nach der zweiten Regel kann standardVal im Initialisierer nicht verwendet werden. Ein Entfernen des Kommentars in ¨ führt zu einem Compiler-Fehler.

Wird eine Instanz von IntVector angelegt, z.B. durch

   new IntVector();  // 
:: Initialisierer 
default

wird zuerst der Code des Initialisierers ausgeführt, dann standardVal auf 1 gesetzt und erst dann der Code im No-Arg-Konstruktor ausgeführt.


Galileo Computing

5.7.2 Statischer Initialisierer  downtop

Statischer Initialisierer:
Ausführung beim Laden der Klasse

Statische Variablen existieren unabhängig von Instanzen und werden – bevor irgendwelche Instanzen existieren – bereits beim Laden der Klasse angelegt und initialisiert. Deshalb ist auch die Initialisierung von statischen Feldern in Konstruktoren unsinnig.

Sofern möglich, sollte die Initialisierung der statischen Felder sofort bei der Deklaration erfolgen. Bei einer komplexen Initialisierung ist dies nicht möglich und wird deshalb in einem statischen Initialisierer (static initializer) durchgeführt, der aus einem Block mit vorangestelltem static besteht:

class {
   static { // statischer Initialisierungs-Block }
}

Ein statischer Initialisierer hat keine Parameter und wird nur einmal ausgeführt, direkt nachdem die Klasse geladen wurde. Eine Klasse kann mehrere statische Initialisierer haben, einer reicht allerdings aus.

Icon

Es gelten folgende Regeln:

Nur statische Felder Abfolge
der statischen Initialisierung

1. Ein statischer Initialisierer hat – wie alle statischen Methoden – keinen Zugriff auf Instanz-Variablen.
2. Analog zur zweiten Regel in 5.7.1.
3. Analog zur dritten Regel in 5.7.1.
4. Blank final Klassen-Variablen müssen im statischen Initialisierer einen Wert zugewiesen bekommen.

Beispiel

Beim Laden der Klasse SinusTable berechnete Tabelle von Sinuswerten:

final class SinusTable {
  final static int MAXDEGREE= 90;
  final static double sin[]; // blank final
  static {
    sin= new double[MAXDEGREE+1];
    for (int i=1; i<=MAXDEGREE; i++)
      sin[i]= Math.sin(i*Math.PI/180.);
  }
  private SinusTable(){}; // keine Instanzen, keine Subklasse
}

Erklärung: Die Klasse SinusTable bedient sich genau eines private-Konstruktors.

Damit sind weder Instanzen erlaubt noch kann man eine Subklasse von SinusTable anlegen, da deren Konstruktor den SinusTable-Konstruktor aufrufen muss (was aber nicht geht!).10 

Die Tabelleneinträge können dann folgendermaßen abgefragt werden:

System.out.println(SinusTable.sin[0]); 
 // :: 0.0
System.out.println(SinusTable.sin[30]); // :: 0.49999999999999994

Galileo Computing

5.8 Overriding vs. Shadowing   downtop

Bei Vererbung verhalten sich Instanz-Methoden im Vergleich zu Feldern und statischen Methoden unterschiedlich.


Galileo Computing

5.8.1 Statischer vs. virtueller Aufruf von Methoden  downtop

Virtueller Methodenaufruf: JVM ermittelt korrekte Methode

Wie bereits besprochen, wird zur Laufzeit von der JVM – unabhängig vom Typ der Referenz – die zum Objekt gehörige Instanz-Methode ermittelt und ausgeführt. Diese Art nennt man

gp  virtuellen Methodenaufruf oder

Synonyme: dynamisch, late oder dynamic binding

gp  dynamischer Methodenaufruf oder
gp  late bzw. dynamic binding.

Diese Bezeichnungen stehen dafür, dass der Compiler beim Übersetzen einer Instanz-Methode in der Regel nicht weiß, welches Objekt von einer Variablen nun referenziert wird.

Statischer
Methodenaufruf: Compiler ermittelt die korrekte Methode

Dies steht im Gegensatz zum statischen Aufruf von Methoden bzw. early binding, wo bereits bei der Kompilierung feststeht, welche Methode ausgeführt werden soll, die dann auch bereits im Code eingebunden werden kann.

Statisch aufgerufene Methoden

Der dynamische Methodenaufruf zur Laufzeit kostet Zeit und wird nur dann benutzt, wenn auch ein Overriding vorkommen kann. In einigen Fällen ist Overriding jedoch verboten oder ausgeschlossen:

static/final/private-
Methoden erlauben statische Aufrufe

gp  Alle static, private oder final deklarierten Methoden bzw. alle Methoden einer final deklarierten Klasse benötigen keinen dynamischen Methodenaufruf.

Für diese Methoden steht nämlich schon zur Übersetzungszeit fest, dass sie nicht durch Methoden von Subklassen überschrieben werden können.

Interessant sind statische Methoden:

gp  Statische Methoden einer Superklasse werden in einer Subklasse durch eine identische statische Methode nicht überschrieben, sondern nur verdeckt (shadowed).11 

Über den Klassennamen oder per Cast kann man – im Gegensatz zu Instanz-Methoden – alle statischen Methoden aller Klassen der Hierarchie jederzeit aufrufen.

Icon
Statische Methode:
nur über Klassennamen aufrufen

Deshalb sollte man ein einfaches Idiom beachten:

gp  Statische Methoden sollte man nicht wie Instanz-Methoden über eine Referenz, sondern immer über den Klassennamen aufrufen.

Verstößt man gegen dieses Idiom, wird – im Gegensatz zu Instanz-Methoden – anhand der Referenz und nicht des Objekts die zugehörige Klasse ermittelt und deren statische Methode aufgerufen.

Dies führt zu schwer durchschaubarem Code, da statische Methoden wie Instanz-Methoden aussehen.

Statische Methoden:
Aufruf über Instanzen werden mit Unklarheit bestraft

Beispiel

Der nachfolgende Code zeigt die sehr verwirrende Art, static deklarierte Methoden wie Instanz-Methoden aufzurufen:

class BC {
  static void sf() {System.out.println("BC.sf()");   }
  void dynf()      {System.out.println("BC.dynf()"); }
}
class DC extends BC {
  static void sf(){System.out.println("DC.sf()");  } // shadowing
  void dynf()    {System.out.println("DC.dynf()");} // overriding
}
public class Test {
  static void test(BC bc) {
    bc.sf();         // besser: BC.sf();       :: BC.sf()
    ((DC)bc).sf();   // besser: DC.sf();       :: DC.sf()
    bc.dynf();       // dynamic Lookup!        :: DC.dynf()
  }
  public static void main(String[] args) {
    DC dc= new DC();
    BC bc= dc;
    dc.sf();         // besser: DC.sf();       :: DC.sf()
    bc.sf();         // besser: BC.sf();       :: BC.sf()
    bc.dynf();       // dynamic Lookup!              :: DC.dynf()
    test(bc);        // siehe oben
  }
}

Galileo Computing

5.8.2 Shadowing von Feldern und super  downtop

Wie statische Methoden werden auch Felder von Superklassen in Subklassen nicht überschrieben, sondern nur verdeckt.

Verdeckte Felder in Superklassen: Einsatz von Cast oder super

Sollte man in einer Subklasse einem Feld denselben Namen geben wie einem Feld in der Superklasse, erhält man zwei Felder mit gleichem Namen, auf die man mittels Casts zugreifen kann, sofern dies der Access-Modifier zulässt.

gp  Innerhalb einer Subklasse kann mit Hilfe von super.feld das verdeckte Feld der Superklasse aufgerufen werden, die in der Hierarchie am nächsten liegt.

Beispiel

Einmal wird innerhalb der Subklasse D, einmal von außen über Test auf verdeckte Felder der Superklassen B1 und B2 zugegriffen.

class B1 { 
  String s= "B1";
  int i=1;
}
class B2 extends B1 { 
  String s= "B2"; 
}

(Superklasse) this
hilft

class D extends B2 {
  String s= "D";
  int i=2;
  void f()      {
    System.out.println(super.s);       
    // :: B2
    System.out.println(super.i);       
    // :: 1
    System.out.println(((B2)this).s);  
    // :: B2 
    System.out.println(((B1)this).s);  
    // :: B1
  //System.out.println(super.super.s);     Fehler: nicht so!
  }
}
public class Test {
  public static void main(String[] args) {
    D d= new D();
    System.out.println(((B2)d).s+" "+((B1)d).s);  // :: B2 B1 
    d.f();
  }
}

Galileo Computing

5.8.3 Aufruf überschriebener Methoden mittels super  downtop

super: einzige Zugriffsmöglichkeit auf überschriebene Methoden

Innerhalb einer Subklasse kann man nicht nur auf Felder, sondern auch auf überschriebene Methoden der Superklasse zugreifen. Allerdings darf man dazu nur das Schlüsselwort super und keinen Cast verwenden.

super vs. super()

Außer dem Namen hat super mit dem Aufruf des Superklassen-Konstruktors super() nichts gemeinsam (siehe 5.5.2). Innerhalb einer Instanz-Methode ruft super.method() eine überschriebene Methode auf und kann auch überall stehen.

Auch für Methoden gilt – wie bei den Feldern – folgende Einschränkung:

super liefert nur die nächste
überschriebene Methode

gp  Innerhalb einer Subklasse kann mit Hilfe von super.method() die überschriebene Methode der Superklasse aufgerufen werden, die in der Hierarchie am nächsten liegt.

Somit ist es mit super.method() nicht möglich, eine Methode einer Basisklasse auszuwählen, die in der Klassenhierarchie mehrfach überschriebene Methode wurde.

Die Suche einer Methode method() mittels super.method() beginnt immer bei der direkten Superklasse und hört bei der ersten passenden Methode einer Superklasse auf.

class B1 {
  void f() {System.out.println("B1.f()"); };
  void g() {System.out.println("B1.g()"); };
}
class B2 extends B1 {
  void f() {
    System.out.println("B2.f()");
    super.f();          // muss nicht erste Anweisung sein
  }
}
class D extends B2 {
  void g() {
    super.f();                     // :: B2.f()
                                   // :: B1.f()
    super.g();                     // :: B1.g()
  }
}

Die Methode B1.f() kann von D nicht aufgerufen werden.


Galileo Computing

5.9 Speicherverwaltung  downtop

Das Thema »Speicherverwaltung« hätte aufgrund der recht häufigen Fehlbeurteilung eigentlich ein eigenes Kapitel verdient.

Destruktoren
gibt es NICHT!

In Java verläuft zwar die Objekt-Anlage mittels new und Konstruktoren analog zu C++, aber in Java gibt es kein Äquivalent zu Destruktoren bzw. delete(), so wie man es in C++ kennen und schätzen gelernt hat.12 

Garbage-Collection: die Befreiung von der Speicherbereinigung via Destruktor

Der Grund für eine automatische Speicherplatzfreigabe – und hierfür steht der Begriff Garbage Collection in Java – ist es, den (Anwendungs-) Programmierer von der sehr fehlerträchtigen Aufgabe der manuellen Speicherfreigabe zu entlasten.

Dadurch wird natürlich auch so manches Idiom überflüssig, was man in C++ zur Speicherbereinigung gelernt hat.

Nun lassen sich C++-Konvertiten nicht so leicht überzeugen, dass dieses vormals mühselige und schwierige Geschäft nun reibungslos automatisch geschieht.

Ist finalize() ein Destruktor?
Icon

Deshalb wird als Anwort auf die Frage, wie denn z.B. vor der automatischen Objekt-Entsorgung die Ressourcen-Freigabe vorzunehmen ist, die Methode finalize() als Destruktor in »Java für C++-Umsteiger« verkauft.13 

Wider den Mythos
Destruktor

Das Hauptanliegen dieses Abschnitts ist es, den Mythos der Methode finalize() als Destruktor bzw. De-Initialisierer zu zerstören und gangbare Alternativen aufzuzeigen.



Galileo Computing

5.9.1 Garbage Collection  downtop

Garbage Collection (GC) ist ein fester Bestandteil der jeweiligen JVM und kann vom Programmierer nicht direkt ausgetauscht bzw. beeinflusst werden.

Erreichbarkeit: Objekte werden noch gebraucht

GC ist nur vom Konzept her festgelegt und basiert auf dem Prinzip der Erreichbarkeit:

gp  Ein Objekt heißt erreichbar (reachable), wenn es noch irgendeine (gültige) Referenz im Programm gibt, die auf dieses Objekt verweist.

Unerreichbarkeit: Kriterium für
Entsorgung

Kann ein Objekt im Programm nicht mehr über eine lokale Variable, ein Feld oder Array-Element angesprochen werden, ist es unerreichbar und kann aus dem Speicher entfernt werden.

Ablauf der
automatischen Speicherbereinigung

In der JVM läuft der GC als ein nebenläufiger Prozess (Thread) mit niedriger Priorität, der die folgenden Aufgaben periodisch nacheinander erledigt:

1. Der GC untersucht alle Objekte im Speicher darauf, ob sie noch erreichbar sind.
2. Bevor ein nicht erreichbares Objekt von dem GC aus dem Speicher entfernt wird, ruft der GC die von Object geerbte Methode finalize() genau einmal auf.
3. Der Speicher, den das Objekt besetzt, wird freigegeben, sofern es noch immer unerreichbar ist.14

Speicherbereinigung ist Aufgabe der JVM und damit abhängig von ihr

Die spezifischen Details, vor allem der letzten beiden Punkte, hängen von der jeweiligen JVM ab.

gp  Das Verhalten des GCs unter einer Maschine ist nicht übertragbar auf die einer anderen oder auf eine zukünftige JVM.

In der Klasse Object ist finalize() wie folgt deklariert:

    protected void finalize() throws Throwable {}

Zu dieser quasi abstrakten Methode gibt es einen langen SUN-Kommentar, der neben der Beschreibung der Ausführung noch Hinweise auf die Verwendung gibt.

Genau diese Hinweise mögen auch ein Grund sein, anzunehmen, finalize() wäre ein Destruktor im Sinn von C++.


Galileo Computing

5.9.2 GC und Finalization  downtop

Nutzen der Speicherbereinigung mittels
finalize()

Will man in einer eigenen Klasse den GC nutzen,

gp  muss man in der Klasse finalize() überschreiben.
gp  müssen Instanzen der Klasse unerreichbar werden.

Beispiel

Nachfolgend wird in der Klasse TestGC anhand von finalize() getestet, wann der GC zumindest die ersten der beiden in 5.9.1 angegebenen Schritte ausführt.

Ein einfacher Test der Speicherbereinigung mit Hilfe von finalize()

class TestGC {
  static int alive; // Anzahl der Instanzen, für die GC
                    // finalize() noch nicht aufgerufen hat
  long[] larr;       
  TestGC(int arrSize) {
    larr= new long[arrSize];  alive++;          
  }
  protected void finalize() {       // 2.Schritt: 
    System.out.print(--alive+" ");  // alive anpassen
  }
}

Die nachfolgende Klasse legt elf Instanzen der Klasse Test an, wobei mit ARR_SIZE der Speicherverbrauch pro Instanz variiert werden kann.

public class FinalizeTest {
  final static int MAX_UNREACHABLE= 10;
  final static int ARR_SIZE= 10000;
  public static void main(String[] args) {
    System.out.print("Nicht entsorgt: ");
    for (int i=0; i<=MAX_UNREACHABLE; i++) {
      TestGC t= new TestGC(ARR_SIZE);
    }
    System.out.println("Ende: " +TestGC.alive);
  }
}

Erklärung: Da nur eine Referenz t zur Verfügung steht, wird nach jedem Iterationsschritt die vorherige Instanz unerreichbar, also zu einem GC-Kandidaten. Nach Beendigung der for-Schleife wird dies auch die letzte Instanz, da t eine lokale Variable ist. Alle Instanzen müssten also vor dem Programmende vom GC entsorgt werden.

Das Testergebnis auf der Konsole ist ernüchternd:

finalize(): ein nicht ermutigendes Ergebnis

Für ARR_SIZE<=1000  :: Aktiv: Ende: 11
Für ARR_SIZE=10000  :: Aktiv: 6 7 6 5 5 4 3 
Ende: 4
Für ARR_SIZE=100000 :: Aktiv: 1 1 1 1 0 1 1 
0 1 Ende: 2
                    :: 1
 

Die Speicherverwaltung der JVM ist unberechenbar

Fazit: Das Testergebnis ist nicht vorhersehbar und hängt – wie bereits gesagt – von der JVM und dem zur Verfügung stehenden Speicher ab und variiert von Aufruf zu Aufruf mit derselben ARR_SIZE.15 

finalize(): ungeeignet für Ressourcen-Freigabe

Spätestens hier wird klar, dass finalize() schlichtweg nicht geeignet ist, wichtige System-Ressourcen wie Netzwerkverbindungen, Dateien, Fenster etc. zuverlässig freizugeben.

gp  Ob der GC überhaupt läuft, d.h. für unerreichbare Instanzen die Methode finalize() aufruft, und in welcher Reihenfolge dies dann noch geschieht, ist unvorhersehbar.

Eine ziemlich verrückte Folgerung daraus ist Code, der so lange für eine bestimmte JVM verändert wird, bis der GC endlich auf einer Testmaschine für alle wichtigen Objekte finalize() aufruft.16 


Galileo Computing

5.9.3 Alternative zu finalize  downtop



Icon

Alternativen benötigt man nur dann, wenn man wichtige System-Ressourcen freigeben bzw. schließen muss. Ansonsten sollte man den GC in Ruhe arbeiten lassen.

Ressourcen-
Freigabe: release()-Methode und Cleanup-Idiom

gp  Zur Freigabe von System-Ressourcen wird der Code von finalize() in eine release()- oder close()-Methode ausgelagert werden, die explizit und nicht mehr unvorhersehbar aufgerufen werden kann.
gp  Um sicherzustellen, dass zu einem Objekt die release()-Methode ausgeführt wird, wird das Cleanup-Idiom (siehe 3.3.4) verwendet.17 

Diese Maßnahmen haben den Vorteil, dass die Freigabe nun unabhängig von der Objekt-Entsorgung ist, also auch bei Bedarf durchgeführt werden kann. Die release()-Methode wird zum Cleanup in den finally-Block gestellt.

Beispiel (Modifikation von TestGC)

Die Klasse TestGC in 5.9.2 wird mit einer release()-Methode ausgestattet. Die Anwendung, repräsentiert durch die Klasse FinalizeTest, wird mit Hilfe des Cleanup-Idioms angepasst:

Ein Beispiel:
Ressourcen-Freigabe mit release(), Cleanup

class TestGC {
  private static int alive;
  private long[] larr;
  private boolean isReleased; 
  TestGC(int arrSize) {
    larr= new long[arrSize];  
    alive++;
  }
  static int getAlive() { return alive; }
  void release() {
    if (!isReleased) {    // verhindert doppelte Ausführung
      // --- Freigabe/Schließen 
von System-Ressourcen ---
      System.out.print(--alive+" "); isReleased= 
true;
    }
  }
}
public class FinalizeTest {
  final static int MAX_UNREACHABLE= 10;
  final static int ARR_SIZE= 0;          // nun auch mit 0

Cleanup-Idiom

  public static void main(String[] args) {
    System.out.print("Released: ");
    for (int i=0; i<=MAX_UNREACHABLE; i++) {
      TestGC t= null;
      try {
t= new TestGC(ARR_SIZE); } // Cleanup-Idiom finally {
if (t!= null) t.release(); } } System.out.println("Ende: " +TestGC.getAlive()); } }

Die Ausgabe ist nun unabhängig von ARR_SIZE und setzt zuverlässig alle Ressourcen frei, wie die Ausgabe zeigt:

:: Released: 0 0 0 0 0 0 0 0 0 0 0 Ende: 0


Galileo Computing

5.9.4 Einsatz von finalize  downtop

Der Nachteil der letzten Lösung ist der explizit notwendige Aufruf einer release()- bzw. close()-Methode in der Anwendung. Jedoch verbietet diese Lösung nicht den zusätzlichen Einsatz von finalize(), die den unbestreitbaren Vorteil der Automatik hat.

Für unerreichbare Objekte, bei denen der Aufruf der release()-Methode einfach »vergessen« wurde, gibt zumindest der GC mit finalize() die Ressourcen frei – sofern er denn in Aktion tritt.18 

Im Trio:
Zusätzlich zu release() und Cleanup noch finalize()

Damit ist finalize() ein zusätzliches Sicherheitsnetz und nutzt dazu die bereits vorhandene release()-Methode, wobei sichergestellt sein muss, dass der Code in der release()-Methode nur einmal aufgerufen wird.

Beispiel (Weitere Modifikation von TestGC)

Die Klasse TestGC aus 5.9.3 wird nun zusätzlich mit einer finalize()-Methode ausgestattet:

class TestGC {
  private static int alive;
  private long[] larr;
  private boolean isReleased;
  TestGC(int arrSize) {
    larr= new long[arrSize]; alive++;
  }
  static int getAlive() { return alive; }

synchronized:
Schutz vor gleichzeitiger Ausführung

  synchronized void release() {
    if (!isReleased) {
      System.out.print(--alive+" "); isReleased= true;
    }
  }
  protected void finalize() 
throws Throwable {
    System.out.print("f ");
    release();
    super.finalize();   // siehe 3. Regel unten
  }
}

Ein Ergebnis,
das erfreut

Für ARR_SIZE=100000 interveniert nun ab und an der GC. Nachfolgend die Ausgabe:

:: Released: 
0 0 f 0 f 0 f 0 f f 0 0 f 0 f f 0 0 f 0 f Ende: 0

Idiom zu finalize

Wird mit finalize() gearbeitet, sollten noch folgende vier Punkte beachtet werden, da man ansonsten mit versteckten Fehlern bestraft werden kann:

Icon
finalize()-Konventionen unbedingt beachten

1. finalize() sollte nicht direkt aufgerufen werden, dies ist einzig Aufgabe des GC.
2. Die letzte Anweisung in der Implementierung von finalize() sollte immer super.finalize() sein.
3. Die zugehörige Instanz sollte in finalize() nicht wieder erreichbar gemacht werden.
4. Die release()-Methode, die finalize() aufruft, sollte synchronisiert sein.

Erklärung:

ad 1: Kombiniert man eine public release()-Methode mit einer protected finalize()-Methode, so ist dies wohl gewährleistet und der direkte Aufruf unnötig.

ad 2: Im Gegensatz zu Konstruktoren, bei denen immer der Superklassen-Konstruktor implizit ausgeführt wird, wenn er nicht explizit geschrieben wird, gilt dies nicht für finalize().19 

Sollten also irgendwelche »versteckten« Ressourcen freigesetzt werden müssen, die von Superklassen geerbt wurden, ist dies nur mit explizitem finalize()-Chaining zu realisieren.

ad 3: Wird in der finalize()-Methode eine gültige Referenz auf das Objekt gesetzt, ist es plötzlich wieder erreichbar. Dies veranlasst den GC dazu,

gp  das Objekt nicht aus dem Speicher zu entfernen

finalize() wird immer nur einmal aufgerufen

gp  nicht wieder finalize() aufzurufen, wenn das Objekt wieder unerreichbar wird und entsorgt wird.

Besonders der letzte Punkt muss unbedingt bedacht werden.

Sollte man mittels finalize() einen Pool von wiederverwendbaren Objekten aufbauen wollen, so ist dies keine gute Idee. Dies geht einfacher mit Soft-Referenzen.

ad 4: GC ist ein eigener Thread und ruft finalize() asynchron auf.


Galileo Computing

5.10 Kapselung (Encapsulationdowntop

Icon
Kapselung: Ausgangspunkt aller OO-Design- Prinzipien

Das Kapitel soll mit einer kurzen Besprechung einer wichtigen Design-Methodik – der Kapselung – beendet werden.

Encapsulation: Es gibt keine direkten Zugriffe auf alle modifizierbaren Felder einer Klasse.

Dies bedeutet, dass entweder alle Felder der Klasse final oder private deklariert sind.20  Zu den Feldern kommen eventuell noch Methoden hinzu, die man nur intern in der Klasse benötigt.

Nachteile der Kapselung

Kapselung:
Arbeits-/Zeit-Aufwand durch Getters und Setters

gp  Für alle modifizierbaren Felder müssen Zugriffs-Methoden (accessor methods) geschrieben werden. Nach Java-Konvention haben sie die Namen getX(), isX() und setX(), wobei X der Name des Felds ist.21 
gp  Da jeder Datenzugriff nur über eine Methode gehen kann, wird zusätzlich die Ausführungsgeschwindigkeit vermindert.

Vorteile der Kapselung

Kapselung:
code-unabhängig Schutz vor unerlaubten Werten

gp  Die Details der internen Implementierung der Klasse werden geschützt, wodurch jederzeit eine Änderung möglich ist, ohne dass alle Anwender der Klasse ihren Code ändern müssen.
gp  Sind nur bestimmte Wertebereiche erlaubt, können nur private deklarierte Felder und Zugriffs-Methoden gegen unerlaubte Werte schützen.

Fazit

Mit oder ohne Kapselung: geprüft werden muss ohnehin!

Jede klasseninterne und -externe Methode, die auf ungeschützte Felder zugreift, muss deren Gültigkeit prüfen, da sie ansonsten inkorrekte Ergebnisse liefern könnte.

Dies produziert dann genau wieder den Code bzw. Zeitverlust, den man durch Verzicht auf die Zugriffsfunktionen einsparen wollte.

Beispiel

Kapselung am Beispiel Teil

Die Klasse Teil zeigt exemplarisch den Einsatz von zwei setX()-Methoden.

Eigenschaften sind private

class Teil {
  private int nr;      // nicht alle Nr. sind erlaubt
  private String bez;  // keine Restriktion
  private int bArtId;  // sinnvoll: Eigenteil, Fremdteil
  // Konstruktoren und andere Methoden werden weggelassen
  public static boolean isValidNr(int nr) {
// kann recht komplex sein, hier eine Bereichsprüfung
     return 1000000<=nr && nr<=9999999;  
  }

Setter für die Teile-Nr.

  public boolean setNr 
(int nr) {
// Alternative zu boolean: Exception
    if (isValidNr(nr)) { 
      this.nr= nr; 
      return true; 
    }
    return false;
  }

Erweiterbarer Setter für
Beschaffungsart

  public boolean setBeschaffungsart 
(String bArt) {
    boolean b= true;
    bArt= bArt.toUpperCase();
    if (bArt.equals("E"))         bArtId= 1;  // Eigenteil
    else if(bArt.equals("F"))     bArtId= 2;  // Fremdteil
    // else if(bArt.equals("EF")) bArtId= 3;                ¨
    else b= false;
    return b;
  }
}

Ergebnis ohne Kapselung: Die Felder nr oder bArtId können jederzeit mit unsinnigen Werten besetzt werden. Jede Methode, die dann auf nr oder bArtId zugreift, muss somit eine Prüfung einbauen, ob die Felder gültige Werte enthalten.

Ergebnis mit Kapselung: Die interne Darstellung bArtId der Beschaffungsart ist nach außen transparent und kann jederzeit gewechselt werden. Stellt man fest, dass es auch Teile geben kann, die fremd und eigen gefertigt werden können, ist die Erweiterung der Zugriffs-Methode setBeschaffungsart() problemlos (siehe ¨).


Galileo Computing

5.11 Zusammenfassung  downtop

Die Umsetzung der Generalisierung in Sub- und Superklassen in Java ist mit vielen sprachlichen Details verbunden.

Neben der Vererbung aller Member ist das Overriding – das Überschreiben von Instanz-Methoden – einer der wichtigsten Mechanismen.

Overriding ist unbedingt von Overloading und Shadowing zu unterscheiden. Statische Methoden können im Gegensatz zu Instanz-Methoden nicht überschrieben werden, obwohl dies umgangssprachlich genauso bezeichnet wird.

Eine Gruppe von Modifikatoren regelt die Art des Aufbaus einer Klassenhierarchie und den Zugriff. Verschiedene Modifikatoren können sich sinnvoll ergänzen oder auch gegenseitig ausschließen.

Eine syntaktisch eigene Gruppe von Konstruktoren regelt die Initialisierung von Objekten. Konstruktor-Chaining ist innerhalb einer Klasse sinnvoll und notwendig in der Klassenhierarchie, um die Initialisierung in der richtigen Reihenfolge zu erzwingen.

Destruktoren wie in C++ sind in Java unbekannt. Java hat eine automatische Speicherverwaltung GC, die den manuellen Aufruf von Destruktoren verhindern soll.

Der GC ruft zwar eine finalize()-Methode vor der Entfernung von Objekten aus dem Speicher aus, aber eine Speicherbereinigung von unerreichbaren Objekten kann nicht erzwungen werden.

Um wichtige Ressourcen freizugeben, wurden entsprechende Alternativen diskutiert.

Kapselung, der Schutz der Klassen-Implementierung vor dem direkten Zugriff von außen, ist das letzte von vielen Idiomen und Design-Prinzipien, die in diesem Kapitel vorgestellt wurden.


Galileo Computing

5.12 Testfragen  toptop

Zu jeder Frage können jeweils ein oder mehrere Antworten bzw. Aussagen richtig sein.

class A { void f(boolean p) {System.out.print (p+" 
");}
          void f(int i)     {System.out.print (i+" ");} }
class B extends A { void f(boolean p){} }

Welche Aussagen sind zu folgendem Code-Fragment richtig?

     B b= new B(); A a= (A)b; 
     a.f(1==1);                // Zeile 2
     b.f(1==1);                // Zeile 3 
     b.f('1');                 // Zeile 4
   class A { void f(int i) {System.out.print (i);} 
}
   class B extends A{
     void f(int i)  {System.out.print (i+1); }
     void f(byte b) {System.out.print (b==0); }
     void f(char c) {System.out.print (c); }
   }

Welche Aussagen sind zum nachfolgenden Code-Fragment richtig?

      B b= new B(); A a= b;
    class A {
      int i;
      A(int i) { this.i=i; }
    }
    class B extends A {
      int i;
      // B() { }                        // Zeile 7
      // B() { this(0); }               // Zeile 8
      // B() { super(0); }              // Zeile 9
      B(int i) { super(i); this.i=i; }
    }
    class X {
       static int si;
       int i;
       int[] iarr={1,2,3};
       static { 
         // si= 1;                           // 6
         // i= 1;                            // 7
       }
       {
         // i=1;                             // 10
         // System.out.println(iarr[0]);     // 11
         // d= 0.0;                          // 12
       }
       X() {
         // System.out.println(i);           // 15
         // System.out.println(d);           // 16
         // d= 1.0;                          // 17
       }
       final double d;                       // 19
    }
   class A {
     char c='A';
     void f() { System.out.print(c+" "); }
     void g() { System.out.print("Hi "); }
   }
   class B extends A {
     char c='B';
     void f() { System.out.print(c+" "); }
   }
   class C extends B {
     // void f() {System.out.println(((A)this).c  );}    // 11
     // void f() {System.out.println(((A)this).f());}    // 12
     // void g() {super.g(); System.out.print(super.c);} // 13
   }

Welche Aussagen sind richtig?






1    Vgl. zur Implementierung Kapitel 6, Interfaces und Pattern.

2    Interfaces können nicht final deklariert werden.

3    Die Bedeutung von final für Variablen wurde bereits in 3.4.2 besprochen.

4    Vgl. Kapitel 6, Interfaces und Pattern.

5    Allerdings verwendet man bei statischen Methoden meistens auch den Begriff überschreiben.

6    Dies ist für C++-Programmierer recht ungewöhnlich, da nicht erlaubt.

7    Dank der Sprach-Designer von Java

8    Anonyme Klasse: Innere Klasse ohne Namen, siehe hierzu Kapitel 8, Innere Klassen

9    Deshalb kann bei der Klasse IntVector der Instanz-Initialisierer bzw. ein Konstruktor bereits auf statische Variablen zurückgreifen.

10    final dient somit nur der Klarheit und ist unnötig.

11    Leider spricht man auch bei statischen Methoden häufig von Overriding, obwohl dies nicht korrekt ist. Zu Shadowing siehe auch 5.8.2.

12    Ein Destruktor ist in C++ eine Instanz-Methode, die bei der Entsorgung nicht mehr benötigter Objekte garantiert aufgerufen wird. Im Gegensatz zu new gibt die Methode delete() den Speicher wieder frei.

13    Eventuell in Verbindung mit einem Aufruf von System.gc().

14    Denn durch finalize() könnte die Unerreichbarkeit ja aufgehoben werden.

15    Sollte die 1 in der letzten Zeile der letzten Ausgabe irritieren: Der GC ist ein asynchroner Thread, der auch noch nach dem main()-Thread aktiv sein kann und dadurch die Ausgabe in finalize() erzeugt (Näheres dazu in Kapitel 9, Threads).

16    Zum Beispiel durch Einfügen von sleep()-Anweisungen, um der GC-Thread Zeit zur Ausführung zu geben.

17    »Unbedingt« bedeutet auch die Ausführung nach Auftreten einer Ausnahme.

18    finalize() ist notwendig, aber nicht hinreichend (im mathematischen Sinn).

19    Dies ist ein weiteres Indiz gegen die Destruktor-Theorie.

20    protected als Zwischenform ist ein Zwitter, der von vielen als versteckter Bruch der Encapsulation angesehen wird.

21    Synonyme Begriffe für accessor methods sind deshalb: Getters und Setters

  

Perl – Der Einstieg




Copyright © Galileo Press GmbH 2001 - 2002
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken und speichern. Ansonsten unterliegt das <openbook> denselben Bestimmungen wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.
Die Veröffentlichung der Inhalte oder Teilen davon bedarf der ausdrücklichen schriftlichen Genehmigung von Galileo Press. Falls Sie Interesse daran haben sollten, die Inhalte auf Ihrer Website oder einer CD anzubieten, melden Sie sich bitte bei: stefan.krumbiegel@galileo-press.de


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, fon: 0228.42150.0, fax 0228.42150.77, info@galileo-press.de