Galileo Computing < openbook >
Galileo Computing - Professionelle Buecher. Auch fuer Einsteiger.
Galileo Computing - Professionelle Buecher. Auch fuer Einsteiger.


Java ist auch eine Insel von Christian Ullenboom
Buch: Java ist auch eine Insel (Galileo Computing)
gp Kapitel 6 Eigene Klassen schreiben
gp 6.1 Eigene Klassen definieren
gp 6.1.1 Methodenaufrufe und Nebeneffekte
gp 6.1.2 Argumentübergabe mit Referenzen
gp 6.1.3 Die this-Referenz
gp 6.1.4 Überdeckte Objektvariablen nutzen
gp 6.2 Assoziationen zwischen Objekten
gp 6.3 Pakete
gp 6.3.1 Hierarchische Strukturen
gp 6.3.2 Paketnamen
gp 6.3.3 Eine Verzeichnisstruktur für eigene Projekte
gp 6.4 Privatsphäre und Sichtbarkeit
gp 6.4.1 Wieso nicht freie Methoden und Variablen für alle?
gp 6.4.2 Privat ist nicht ganz privat. Es kommt darauf an, wer's sieht
gp 6.4.3 Zugriffsmethoden für Attribute definieren
gp 6.4.4 Zusammenfassung zur Sichtbarkeit
gp 6.4.5 Sichtbarkeit in der UML
gp 6.5 Statische Methoden und Variablen
gp 6.5.1 Warum statische Eigenschaften sinnvoll sind
gp 6.5.2 Statische Eigenschaften mit static
gp 6.5.3 Statische Eigenschaften als Objekteigenschaften nutzen
gp 6.5.4 Statische Eigenschaften und Objekteigenschaften
gp 6.5.5 Statische Variablen zum Datenaustausch
gp 6.5.6 Warum die Groß- und Kleinschreibung wichtig ist
gp 6.5.7 Konstanten mit dem Schlüsselwort final bei Variablen
gp 6.5.8 Problem mit finalen Klassenvariablen
gp 6.5.9 Typsicherere Konstanten
gp 6.5.10 Statische Blöcke
gp 6.6 Objekte anlegen und zerstören
gp 6.6.1 Konstruktoren schreiben
gp 6.6.2 Einen anderen Konstruktor der gleichen Klasse aufrufen
gp 6.6.3 Initialisierung der Objekt- und Klassenvariablen
gp 6.6.4 Finale Werte im Konstruktor setzen
gp 6.6.5 Exemplarinitialisierer (Instanzinitialisierer)
gp 6.6.6 Zerstörung eines Objekts durch den Müllaufsammler
gp 6.6.7 Implizit erzeugte String-Objekte
gp 6.6.8 Zusammenfassung: Konstruktoren und Methoden
gp 6.7 Veraltete (deprecated) Methoden/Konstruktoren
gp 6.8 Vererbung
gp 6.8.1 Vererbung in Java
gp 6.8.2 Einfach- und Mehrfachvererbung
gp 6.8.3 Gebäude modelliert
gp 6.8.4 Konstruktoren in der Vererbung
gp 6.8.5 Sichtbarkeit
gp 6.8.6 Das Substitutionsprinzip
gp 6.8.7 Automatische und explizite Typanpassung
gp 6.8.8 Finale Klassen
gp 6.8.9 Unterklassen prüfen mit dem Operator instanceof
gp 6.8.10 Methoden überschreiben
gp 6.8.11 super: Aufrufen einer Methode aus der Oberklasse
gp 6.8.12 Nicht überschreibbare Funktionen
gp 6.8.13 Fehlende kovariante Rückgabewerte
gp 6.9 Die oberste aller Klassen: Object
gp 6.9.1 Klassenobjekte
gp 6.9.2 Objektidentifikation mit toString()
gp 6.9.3 Objektgleichheit mit equals() und Identität
gp 6.9.4 Klonen eines Objekts mit clone()
gp 6.9.5 Hashcodes
gp 6.9.6 Aufräumen mit finalize()
gp 6.9.7 Synchronisation
gp 6.10 Die Oberklasse gibt Funktionalität vor
gp 6.10.1 Dynamisches Binden als Beispiel für Polymorphie
gp 6.10.2 Keine Polymorphie bei privaten, statischen und finalen Methoden
gp 6.10.3 Polymorphie bei Konstruktoraufrufen
gp 6.11 Abstrakte Klassen
gp 6.11.1 Abstrakte Klassen
gp 6.11.2 Abstrakte Methoden
gp 6.11.3 Über abstract final
gp 6.12 Schnittstellen
gp 6.12.1 Ein Polymorphie-Beispiel mit Schnittstellen
gp 6.12.2 Die Mehrfachvererbung bei Schnittstellen
gp 6.12.3 Erweitern von Interfaces - Subinterfaces
gp 6.12.4 Vererbte Konstanten bei Schnittstellen
gp 6.12.5 Vordefinierte Methoden einer Schnittstelle
gp 6.12.6 CharSequence als Beispiel einer Schnittstelle
gp 6.13 Innere Klassen
gp 6.13.1 Statische innere Klassen und Schnittstellen
gp 6.13.2 Mitglieds- oder Elementklassen
gp 6.13.3 Lokale Klassen
gp 6.13.4 Anonyme innere Klassen
gp 6.13.5 Eine Sich-Selbst-Implementierung
gp 6.13.6 this und Vererbung
gp 6.13.7 Implementierung einer verketteten Liste
gp 6.13.8 Funktionszeiger
gp 6.14 Gegenseitige Abhängigkeiten von Klassen


Galileo Computing

6.6 Objekte anlegen und zerstörendowntop

Wenn Objekte mit dem new-Operator angelegt werden, reserviert die Speicherverwaltung des Laufzeitsystems auf dem System-Heap Speicher. Wird das Objekt nicht mehr referenziert, so räumt der Garbage-Collector (GC) in bestimmten Abständen auf und gibt den Speicher an das Laufzeitsystem zurück.


Galileo Computing

6.6.1 Konstruktoren schreibendowntop

Ein Konstruktor wird automatisch aufgerufen, wenn ein Objekt mit dem new-Operator angelegt wird. Mit einem eigenen Konstruktor lässt sich erreichen, dass ein Objekt nach seiner Erzeugung einen sinnvollen Anfangszustand aufweist. Dies kann bei Klassen, die Variablen beinhalten, notwendig sein, da sie ohne vorherige Zuweisung beziehungsweise Initialisierung keinen Sinn machen würden.

Die Konstruktoren tragen denselben Namen wie die Klasse und sehen wie eine Methode ohne Rückgabewert aus. Da mitunter mehrere Konstruktoren mit unterschiedlichen Parameterlisten vorkommen, ist der Konstruktor oft überladen. So soll es auch bei dem Konstruktor für Diskotheken sein. Die Disko soll sich mit einer Quadratmeteranzahl initialisieren lassen und auch mit einer Anzahl Menschen und Quadratmetern.

Listing 6.17 v4/Disko.java

package v4;
public class Disko
{
  private int anzahlLeute;   // Anzahl Leute in der Disko
  private int quadratmeter;  // Größe der Disko
  /**
   * Erzeugt ein neues Disko-Objekt.
   * 
   * @param quadratmeter  Quadratmeter der neuen Disko.
   */
  Disko( int quadratmeter )
  {
    this.quadratmeter = quadratmeter;
  }
  /**
   * Erzeugt ein neues Disko-Objekt.
   * 
   * @param anzahlLeute   Startanzahl Personen in der neuen Disko.
   * @param quadratmeter  Quadratmeter der neuen Disko.
   */
  Disko( int anzahlLeute, int quadratmeter )
  {
    this.anzahlLeute  = anzahlLeute;
    this.quadratmeter = quadratmeter;
  }
}

Damit ergibt sich folgendes UML-Diagramm:

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 6.6 Konstruktoren im UML-Diagramm.
Das void an den Konstruktoren ist unüblich. Und ein Fehler im Tool.

Der Standard-Konstruktor

Wenn wir in unseren Klassen keinen Konstruktor angeben, so legt der Compiler automatisch einen Standard-Konstruktor an. Schreiben wir nur

class Disko
{
}

macht daraus der Compiler

class Disko
{
  Disko() { }
}

Wenn es jedoch mindestens einen ausprogrammierten Konstruktor gibt, wird dieser Standard-Konstruktor nicht mehr automatisch angelegt. Wollen wir daher ein Objekt einfach mit dem Standard-Konstruktor new Klassenname() erzeugen, so müssen wir einen parameterlosen Standard-Konstruktor per Hand hinzufügen. Dass der Standard-Konstruktor dann nicht angelegt wird, hat seinen guten Grund: Es ließe sich sonst ein Objekt anlegen, ohne dass vielleicht wichtige Variablen initialisiert worden wären.

Das gilt zum Beispiel in unserem Disko-Beispiel: Dort gibt es keinen Standard-Konstruktor; das Anlegen mit new Disko() ist nicht möglich. Mit den parametrisierten Konstruktoren erzwingen wir, dass die nötigen Werte für den Startzustand einer Disko vorhanden sind.

Wie ein nützlicher Konstruktor aussehen kann

Besitzt ein Objekt eine Reihe von Attributen, so wird ein Konstruktor in der Regel diese Attribute initialisieren wollen. Wenn wir eine Unmenge von Attributen in einer Klasse haben, sollten wir dann auch endlos viele Konstruktoren schreiben? Besitzt eine Klasse Attribute, die durch setXXX()- getXXX()-Methoden gesetzt werden, so ist es nicht unbedingt nötig, dass diese Attribute im Konstruktor gesetzt werden. Ein Standard-Konstruktor, der das Objekt in einen Initialzustand setzt, ist angebracht; anschließend können die Zustände mit den Zugriffsfunktionen verändert werden. Das sagt auch die JavaBean-Konvention. Praktisch sind sicherlich auch Konstruktoren, die die häufigsten Initialisierungsszenarien abdecken. Das Punkt-Objekt der Klasse java.awt.Point lässt sich mit dem Standard-Konstruktor erzeugen, aber auch mit einem parametrisierten, der gleich die Koordinatenwerte entgegennimmt.

Besitzt ein Objekt Attribute, die nicht über Setze-Funktionen modifiziert werden können, diese Werte aber bei Objekterzeugung wichtig sind, so bleibt nichts anderes übrig, als die Werte im Konstruktor einzufügen. (Eine set-Funktion, die nur einmalig eine Schreiboperation zulässt, ist nicht wirklich schön.) So arbeiten zum Beispiel Werteobjekte, die einmal im Konstruktor einen Wert bekommen und ihn beibehalten. In der Java-Bibliothek gibt es eine Reihe solcher Klassen, die keinen Standard-Konstruktor besitzen, und nur einige parametrisierte, die Werte erwarten. Die im Konstruktor übergebenen Werte initialisieren das Objekt, und es behält diese Werte das ganze Leben lang. Zu den Klassen gehören etwa Color, File, Font und Integer.

Weiterhin ist ein Konstruktor außerordentlich praktisch, der seinesgleichen entgegennimmt. Ein Beispiel: Die Klasse Disko bekommt einen Konstruktor, der eine andere Disko als Parameter entgegennimmt. Auf diese Weise lässt sich eine schon initialisierte Disko als Attributvorlage nutzen. Alle Eigenschaften der existierenden Disko sollen auf die neue Disko übertragen werden. Die Implementierung kann so aussehen:

Listing 6.18 v5/Disko.java

package v5;
class Disko
{
  int anzahlLeute;    // Anzahl Leute in der Disko
  int quadratmeter;   // Größe der Disko
  /**
   * Erzeugt ein neues Disko-Objekt.
   */
  Disko()
  {
  }
  /**
   * Erzeugt ein neues Disko-Objekt.
   * 
   * @param disko  Existierendes Disko-Objekt.
   */
  Disko( Disko disko )
  {
    anzahlLeute  = disko.anzahlLeute; 
    quadratmeter = disko.quadratmeter;
  }
  public String toString()
  {
    return "Leute: " + anzahlLeute + " Größe: " + quadratmeter;
  }
}

Abbildung
Hier klicken, um das Bild zu Vergrößern

Die main()-Funktion soll jetzt eine neue Disko cave54 erzeugen und anschließend eine neue Disko käfig2 mit den Werten von cave54 initialisieren.

public static void main( String args[] )
{
  Disko cave54 = new Disko();
  cave54.anzahlLeute = 244;
  cave54.quadratmeter = 96;
  System.out.println( cave54 );
  Disko käfig2 = new Disko( cave54 ); // Leute: 244 Größe: 96
  System.out.println( käfig2 );       // Leute: 244 Größe: 96
}

Galileo Computing

6.6.2 Einen anderen Konstruktor der gleichen Klasse aufrufendowntop

Mitunter werden zwar verschiedene Konstruktoren angeboten, aber nur in einem Konstruktor verbirgt sich die tatsächliche Initialisierung des Objekts. Nehmen wir unser Beispiel mit dem Konstruktor, der eine Disko als Parameter nimmt.

Disko( Disko disko )
{
  anzahlLeute  = disko.anzahlLeute; 
  quadratmeter = disko.quadratmeter;
}

Jetzt gibt es aber auch einen anderen Konstruktor, der die Anzahl Leute und Quadratmeteranzahl entgegennimmt.

Disko( int anzahlLeute, int quadratmeter )
{
  this.anzahlLeute  = anzahlLeute;
  this.quadratmeter = quadratmeter;
}

Zu erkennen ist, dass beide im Wesentlichen die Objektvariablen initialisieren und eigentlich das Gleiche machen. Schlauer ist es, wenn der Konstruktor Disko(Disko) den anderen Konstruktor Disko(int, int) derselben Klasse - nicht den der Oberklasse - aufruft, um nicht gleichen Programmcode (die Initialisierung) mehrfach ausprogrammieren zu müssen. Dazu dient wieder das Schlüsselwort this.

Listing 6.19 v7/Disko.java

package v7;
class Disko
{
  int anzahlLeute;
  int quadratmeter;
  Disko( Disko Disko )
  {
    this( Disko.anzahlLeute, Disko.quadratmeter );
  }
  Disko( int anzahlLeute, int quadratmeter )
  {
    this.anzahlLeute  = anzahlLeute;
    this.quadratmeter = quadratmeter;
  }
}

Natürlich stellt sich die Frage, ob das so viel Gewinn ist. Hier ist in der Tat nicht viel weniger zu schreiben und auch nicht schneller ausgeführt. Zudem hat diese Implementierung mit der manuellen Initialisierung Nachteile. Nehmen wir an, wir hätten zehn Konstruktoren für alle erdenklichen Fälle in genau diesem Stil implementiert. Tritt der unerwünschte Fall ein, dass wir auf einmal in jedem Konstruktor etwas initialisieren müssen, so muss der Programmcode - etwa ein Aufruf der Methode init() - in jedem der Konstruktoren eingefügt werden. Dieses Problem umgehen wir einfach, indem wir die Arbeit auf einen speziellen Konstruktor verschieben. Ändert sich nun das Programm in der Weise, dass überall beim Initialisieren zusätzlicher Programmcode ausgeführt werden muss, ändern wir eine Zeile in dem konkreten von allen benutzten Konstruktor. Damit fällt für uns wenig Änderungsarbeit an: unter softwaretechnischen Gesichtspunkten ein großer Vorteil. Überall in den Java-Bibliotheken lässt sich diese Technik wieder erkennen.

Beispiel der Klasse java.awt.Point

Ein schönes einfaches Beispiel ist etwa die java.awt.Point-Klasse. Ein Ausschnitt:

public class Point extends Point2D implements java.io.Serializable
{
  public int x, 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;
  }
}

Einschränkungen

Beim Aufruf eines anderen Konstruktors mittels this() gibt es zwei wichtige Beschränkungen:

1. Der Aufruf von this() muss in der ersten Zeile stehen.
2. Als Parameter von this() können keine Objektvariablen übergeben werden. Insbesondere Eigenschaften aus der Oberklasse sind noch nicht präsent. Möglich sind aber statische finale Variablen (Konstanten).

Die erste Einschränkung besagt, dass das Erzeugen eines Objektes immer das Erste sein muss, was ein Konstruktor machen muss. Nichts darf vor der Initialisierung ausgeführt werden. Die zweite Einschränkung liegt darin begründet, dass die Objektvariablen erst nach dem Aufruf von this() initialisiert werden, so dass ein Zugriff unsinnig wäre - die Werte wären im Allgemeinen 0.

Listing 6.20 Musikanlage.java

public class Musikanlage
{
  static final int STANDARD = 1000;
  int standard = 1000;
  
  int watt;
  
  Musikanlage()
  {
//    this( standard );        // nicht zulässig
    this( STANDARD );      // das wäre OK  
  }
  Musikanlage( int watt )
  {
   this.watt = watt;
  }
}

Da Objektvariablen bis zu einem bestimmten Punkt noch nicht initialisiert sind, lässt uns der Compiler nicht darauf zugreifen - nur statische Variablen sind als Übergabeparameter erlaubt. Daher ist der Aufruf this(standard) nicht gültig, da standard eine Objektvariable ist; aber this(STANDARD) ist in Ordnung, da STANDARD eine statische Variable ist.


Galileo Computing

6.6.3 Initialisierung der Objekt- und Klassenvariablendowntop

Eine wichtige Eigenschaft von guten Programmiersprachen ist ihre Fähigkeit, keine uninitialisierten Zustände zu erzeugen. Bei lokalen Variablen achtet der Compiler auf die Belegung, ob vor dem ersten Lesezugriff schon ein Wert zugewiesen ist. Bei Objektvariablen und Klassenvariablen haben wir bisher festgestellt, dass automatisch die Variablen mit 0 oder mit einem Wert belegt werden. Wir wollen jetzt sehen, wie dies genau funktioniert.

Objektvariablen

Wenn der Compiler eine Klasse mit Objekt- oder Klassenvariablen sieht, dann müssen diese Variablen an irgendeiner Stelle initialisiert werden. Werden sie einfach definiert und nicht mit einem Wert initialisiert, so regelt die virtuelle Maschine die Vorbelegung. Spannender ist der Fall, wenn den Variablen explizit ein Wert zugewiesen wird (der auch 0 sein kann). Dann erzeugt der Compiler automatisch ein paar zusätzliche Zeilen.

Betrachten wir dies zuerst für eine Objektvariable.

Listing 6.21 InitObjectVariable.java

class InitObjectVariable
{
  int j = 1;
  InitObjectVariable()
  {
  }
  InitObjectVariable( int j )
  {
    this.j = j;
  }
  InitObjectVariable( int x, int y )
  {
  }
}

Die Variable j wird mit 1 belegt. Es ist wichtig zu wissen, an welcher Stelle Variablen ihre Werte bekommen. So erstaunlich das klingt, aber die Zuweisung findet im Konstruktor statt. Das heißt, der Compiler wandelt das Programm bei der Übersetzung eigenmächtig wie folgt um:

class InitObjectVariable
{
  int j;
  InitObjectVariable()
  {
    j = 1;
  }
  InitObjectVariable( int j )
  {
    this.j = 1;
    this.j = j;
  }
  InitObjectVariable( int x, int y )
  {
    j = 1;
  }
}

Wir erkennen, dass die Variable wirklich nur dann initialisiert wird, wenn auch ein Konstruktor aufgerufen wird. Die Zuweisung steht dabei in der ersten Zeile. Dies kann zur Falle werden, denn problematisch ist etwa die Reihenfolge der Belegung.

Manuelle Nullung

Genau genommen initialisiert die Laufzeitumgebung jede Objekt- und Klassenvariable erst einmal mit 0 und dann später mit einem Wert. Daher ist die Initialisierung auch ein bisschen langsamer, wenn die Nullung von Hand zusätzlich eingebaut wird, also etwa so:

class InitNullUnnötig
{
  int i = 0;
}

Der Compiler würde nur zusätzlich in jeden Konstruktor die Initialisierung i = 0 einsetzen.1

Klassenvariablen

Abschließend bleibt die Frage, wo Klassenvariablen initialisiert werden. Im Konstruktor macht dies keinen Sinn, da für Klassenvariablen keine Objekte angelegt werden müssen. Dafür gibt es den static{}-Block. Dieser wird immer dann ausgeführt, wenn der Klassenlader eine Klasse in die Laufzeitumgebung geladen hat. Für eine statische Initialisierung wird also wieder der Compiler etwas einfügen.


public class InitStaticVariable
{
  static int staticInt = 2;
}
public class InitStaticVariable
{
  static int staticInt;
  
  static
  {
    staticInt = 2;
  }
}


Galileo Computing

6.6.4 Finale Werte im Konstruktor setzendowntop

Bisher sind wir davon ausgegangen, dass finale Werte immer dann gesetzt werden müssen, wenn die Variable deklariert wird. Diese Ansicht können wir etwas erweitern. Bedenken wir, dass alle statischen Variablen in static-Blöcken und alle Objektvariablen in Konstruktoren initialisiert werden, so gilt dies ebenso für finale Werte. Auch sie werden im Konstruktor gesetzt. Wichtig ist, dass finale Werte auf jeden Fall gesetzt werden und dass nur ein Schreibzugriff möglich ist. Mit diesem Vorgehen lassen sich auch »variable« Konstanten angeben:

Listing 6.22 VariableConstant.java

class VariableConstant
{
  final static int MWST;   // hier steht nicht = irgendwas
  final String ISBN;       // hier auch nicht.
  static
  {
    if ( 2 > 1 )
      MWST = 7;
    else
      MWST = 16;
  }
  VariableConstant()
  {
    ISBN = "3572100100";
  }
  public static void main( String args[] )
  {
    System.out.println( MWST );                         // 7
    System.out.println( new VariableConstant().ISBN );  // 3572100100
  }
}

Der Nachteil dieser Variante ist natürlich, dass die Lesbarkeit leidet. Der Leser muss sich mitunter erst durch viele Zeilen Quellcode kämpfen, bis er weiß, wie der Wert belegt ist. Daher sollte diese Programmiertechnik nur zum Einsatz kommen, wenn der Wert nicht direkt berechnet werden kann.


Galileo Computing

6.6.5 Exemplarinitialisierer (Instanzinitialisierer)downtop

Neben den Konstruktoren haben die Sprachschöpfer eine weitere Möglichkeit vorgesehen, um Objekte zu initialisieren. Diese Möglichkeit wird insbesondere bei anonymen, inneren Klassen wichtig, also bei Klassen, die sich in einer anderen Klasse befinden.

Ein Exemplarinitialisierer ist ein Konstruktor ohne Namen. Er besteht in einer Klassendefinition nur aus einem Paar geschweifter Klammern und gleicht einem statischen Initialisierungsblock ohne das Schlüsselwort static:

class Klasse
{
  {
     // Exemplarinitialisierer.
  }
}

Mit Exemplarinitialisierern Konstruktoren vereinfachen

Die Exemplarinitialisierer können gut dazu verwendet werden, Initialisierungsarbeit bei der Objekterzeugung auszuführen, was sonst entweder in einer Funktion oder - noch viel schlechter - in jedem Konstruktor reingesetzt werden würde. Nehmen wir an, wenn müssten beim Erzeugen eines Objekts erst immer eine halbe Sekunde schlafen und es gäbe zwei Konstruktoren:

class eSache
{
  eSache( int i )
  {
    try { Thread.sleep( 500 ); } catch( Exception e ) { }
  }
  eSache( String s )
  {
    try { Thread.sleep( 500 ); } catch( Exception e ) { }
  }
}

Mit dem Exemplarinitialisierer lässt sich der Programmcode vereinfachen:

class eSache
{
  {
    try { Thread.sleep( 500 ); } catch( Exception e ) { }
  }
  eSache( int i )
  {
  }
  eSache( String s )
  {
  }
}

Damit haben wir eine Quellcodeduplizierung im Quellcode vermieden. Allerdings hat die Technik gegenüber einer langweiligen Initialisierungsfunktion auch Nachteile.

gp Zwar ist im Quellcode die Duplizierung raus, aber in der Klassendatei steht sie wieder drin. Das liegt daran, dass der Compiler alle Anweisungen des Exemplarinitialisierers in jeden Konstruktor kopiert. Bei einer Funktion stände der Quellcode aber nur einmal dar. Realistisch gesehen dürfte es bei der Anzahl der Anweisungen kein ernsthaftes Problem sein, dass die Klassendatei ein wenig größer wird.
gp Das Zweite ist, dass Exemplarinitialisierer gerne übersehen werden. Ein Blick auf den Konstruktor verrät nicht, was er alles macht; ein Funktionsaufruf klärt das schnell auf. Die Initialisierung trägt damit nicht zur Übersichtlichkeit bei.
gp Ein weiteres Manko ist, dass die Initialisierung nur bei neuen Objekten, also mit new() durchgeführt wird. Wenn Objekte wieder verwendet werden sollen, dann ist eine Funktion, die das Objekt wie frisch erzeugt aufbaut, gar nicht so schlecht. Eine Funktion lässt sich immer aufrufen, und damit ist das Objekt wie neu.

Mehrere Exemplarinitialisierer

In einer Klasse können mehrere Exemplarinitialisierer auftauchen. Die Exemplarinitialisierer werden der Reihe nach durchlaufen, und zwar vor dem eigentlichen Konstruktor. Ihr Programmcode wird in alle Konstruktoren eingesetzt. Objektvariablen wurden schon initialisiert. Ein Programmcode wie der Folgende

Listing 6.23 WerIstAustin.java

public class WerIstAustin
{
  String austinPowers = "Mike Meyers";
  {
    System.out.println( "1 " + austinPowers );
  }
  
  WerIstAustin()
  {
    System.out.println( "2 " + austinPowers );
  }
}

wird vom Compiler also umgebaut zu

public class WerIstAustin
{
  String austinPowers;
  WerIstAustin()
  {
    austinPowers = "Mike Meyers";
    System.out.println("1 " + austinPowers);
    System.out.println("2 " + austinPowers);
  }
}

Wichtig ist abschließend zu sagen, dass vor dem Zugriff auf eine Objektvariable im Exemplarinitialisierer diese auch definiert sein muss. So führt Folgendes zu einem Fehler:

class WerIstDrEvil
{
  {
//    System.out.println( drEvil );      // Ein Compilerfeher.
  }
  String drEvil = "Mike Meyers";
}

Galileo Computing

6.6.6 Zerstörung eines Objekts durch den Müllaufsammlerdowntop

Glücklicherweise werden wir beim Programmieren von der lästigen Aufgabe befreit, Speicher von Objekten freizugeben. Wird ein Objekt nicht mehr referenziert, dann wird der Garbage-Collector2 aufgerufen, und dieser kümmert sich um alles Weitere - der Entwicklungsprozess wird dadurch natürlich vereinfacht. Der Einsatz eines GCs verhindert zwei große Probleme:

gp Ein Objekt kann gelöscht werden, aber die Referenz existiert noch (engl. dangling pointer).
gp Kein Zeiger verweist auf ein bestimmtes Objekt, dieses existiert aber noch im Speicher (engl. memory leaks).

Dem GC wird es leicht gemacht, wenn nicht mehr benötigte Referenzen sofort mit null überschrieben werden (objRef = null), denn dann weiß der GC, dass zumindest ein Verweis weniger auf das Objekt existiert. War es der letzte Verweis, kann der GC dieses Objekt sofort entfernen, wenn weiterer Speicherplatz für neue Objekte benötigt wird.


Hinweis Einen Destruktor, so wie in C++, gibt es in Java nicht. Wohl können wir eine Funktion finalize() ausprogrammieren, in der Aufräumarbeiten erledigt werden. Die Methode erbt jede Klasse von Object. Im Gegensatz zu C++ mit einer manuellen Freigabe ist in Java keine Aussage über den Zeitpunkt möglich, zu dem die Routine aufgerufen wird - dies ist von der Implementierung des GCs abhängig. Es kann sein, dass finalize() überhaupt nicht aufgerufen wird, und zwar dann, wenn die VM Fantastillionen Megabyte Speicher hat und dann beendet wird. Insbesondere ist finalize() ungeeignet, um Ressourcen freizugeben, etwa File-Handles oder Grafik-Kontexte des Betriebssystems. finalize() wird nur aufgerufen, wenn Speicher knapp wird und tote Objekte freigegeben werden müssen. Gehen zum Beispiel nur die File-Handles aus, wird der GC nicht aktiv; es erfolgen keine finalize()-Aufrufe, und tote Objekte belegen weiter die knappen File-Handles.

Prinzipielle Arbeitsweise

Der GC erscheint hier als ominöses Ding, welches clever die Objekte verwaltet. Doch was ist der GC? Implementiert ist er als Thread (unabhängiger Prozess) mit niedriger Priorität. Er verwaltet die Wurzelobjekte, von denen aus das gesamte Geflecht der lebendigen Objekte erreicht werden kann. Dazu gehören die Wurzel des ThreadGroup-Baums und die lokalen Variablen aller aktiven Methodenaufrufe (Laufzeitkeller aller Threads). In regelmäßigen Abständen werden nicht benötigte Objekte markiert und entfernt.

Mittlerweile ist auch das Anlegen von Objekten unter der Java VM von Sun dank der Hot-Spot-Technologie schneller geworden. Hot-Spot ist seit Java 1.3 fester Bestandteil des Java SDK. Hot-Spot verwendet einen generationenorientierten GC, der ausnutzt, dass zwei Gruppen von Objekten mit deutlich unterschiedener Lebensdauer existieren. Die meisten Objekte sterben sehr jung, die wenigen überlebenden Objekte werden hingegen sehr alt. Die Strategie dabei ist, dass Objekte im »Kindergarten« erzeugt werden, der sehr oft nach toten Objekten durchsucht wird und in der Größe beschränkt ist. Überlebende Objekte kommen nach einiger Zeit aus dem Kindergarten in eine andere Generation, die nur selten vom GC durchsucht wird. Damit folgt der GC der Philosophie von Auffenberg, der sagte: »Verbesserungen müssen zeitig glücken; im Sturm kann man nicht mehr die Segel flicken«. Das heißt, der GC arbeitet ununterbrochen und räumt auf. Er beginnt nicht erst dann mit der Arbeit, wenn es zu spät ist und der Speicher schon voll ist.


Galileo Computing

6.6.7 Implizit erzeugte String-Objektedowntop

In den bisherigen Beispielen haben wir gesehen, dass ein Objekt mit dem new-Operator gebildet wird. Es gibt aber noch eine versteckte Objekterzeugung bei Zeichenketten. Betrachten wir folgende Zeilen:

Date d = new Date();
String s = "Chicken Run";
String t = "Chicken Run";

Beim Datum erzeugten wir ausdrücklich ein neues Date-Objekt. Die zweite Zeile erzeugt jedoch implizit ein String-Objekt, das das angegebene Zeichenketten-Literal speichert. In der dritten Zeile gilt nun etwas Besonderes. Um dies zu erkennen, müssen wir wissen, dass Zeichenketten-Literale in einem Konstantenpool der virtuellen Maschine gehalten werden. Gleiche Zeichenketten bei String-Objekten für Literale (und nur dort) werden daher auf die gleichen Referenzen gelenkt. Genau in diesem Fall lassen sich mit dem Vergleichsoperator = = die Zeichenketten vergleichen. In der dritten Zeile wird demnach also kein neues String-Objekt erzeugt, sondern die Referenz t ist mit der von s identisch.

Der letzte Fall einer impliziten Objekterzeugung hat wieder mit Zeichenketten zu tun: Der Plus-Operator zur Konkatenation von nicht konstanten Zeichenketten (konstante Zeichenketten werden vom Compiler zusammengefügt) erzeugt einen StringBuffer, dessen Bausteine mit append() angehängt werden. Nach der Aneinanderreihung wird der StringBuffer wieder zu einem String konvertiert:

String s = "Peter Lord " + '&' + " Nick Park";  // <a href="#ftn3">3
String s = new StringBuffer("Peter Lord ").append('&').
             append(" Nick Park").toString();

Galileo Computing

6.6.8 Zusammenfassung: Konstruktoren und Methodentoptop

Methoden und Konstruktoren haben einige Gemeinsamkeiten in der Signatur, aber auch einige wichtige Unterschiede wie den Rückgabewert oder den Gebrauch von this und super. Die folgende Tabelle fasst die Unterschiede und Gemeinsamkeiten noch einmal kompakt zusammen:4


Benutzung Konstruktoren Methoden
Modifizierer Sichtbarkeit public, protected, paketsichtbar und private. Können nicht abstract, final, native, static oder synchronized8 sein. Sichtbarkeit public, protected, paketsichtbar und private
Können abstract, final, native, static oder synchronized sein.
Rückgabewert Kein Rückgabewert, auch nicht void Rückgabetyp oder void
Bezeichnername Gleicher Name wie die Klasse. Beginnt daher in der Regel mit einem Großbuchstaben. Beliebig. In der Regel beginnt er mit einem Kleinbuchstaben.
this this() bezieht sich auf einen anderen Konstruktor der gleichen Klasse. Wird this()benutzt, muss this() in der ersten Zeile stehen. this ist eine Referenz in Objektmethoden, die sich auf das aktuelle Exemplar bezieht.
super Ruft einen Konstruktor der Oberklasse auf. Wird super() benutzt, muss super() in der ersten Zeile
stehen.
super ist eine Referenz, die auf
die Oberklasse zeigt. Damit lassen sich überschriebene Methoden aufrufen.
Vererbung Konstruktoren werden nicht vererbt. Sichtbare Methoden werden
vererbt.

Tabelle 6.1 Gegenüberstellung von Konstruktoren und Methoden






1 Wir wollen hier den Fall, dass der Konstruktor der Oberklasse i einen Wert ungleich 0 setzt, nicht betrachten.

2 Lange Tradition hat der Garbage-Collector unter LISP und unter Smalltalk, aber auch Visual Basic benutzt einen GC.

3 Die Erfinder von Wallace & Gromit und Chicken Run. Für den neuen Film haben 40 Kneter in drei Jahren zwei Tonnen Plastilin geformt.

4 Schon seltsam, dass synchronized nicht erlaubt ist, aber ein Konstruktor ist implizit synchronized.





Copyright (c) Galileo Press GmbH 2004
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. 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.


[Galileo Computing]

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