Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger

 << zurück
Java ist auch eine Insel von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 5
Java ist auch eine Insel

Java ist auch eine Insel
5., akt. und erw. Auflage
1454 S., mit CD, 49,90 Euro
Galileo Computing
ISBN 3-89842-747-1
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.2.1 Gegenseitige Abhängigkeiten von Klassen
  gp 6.3 Privatsphäre und Sichtbarkeit
    gp 6.3.1 Wieso nicht freie Methoden und Variablen für alle?
    gp 6.3.2 Privat ist nicht ganz privat: Es kommt darauf an, wer’s sieht
    gp 6.3.3 Zugriffsmethoden für Attribute definieren
  gp 6.4 Statische Methoden und statische Attribute
    gp 6.4.1 Warum statische Eigenschaften sinnvoll sind
    gp 6.4.2 Statische Eigenschaften mit static
    gp 6.4.3 Statische Eigenschaften über Referenzen nutzen?
    gp 6.4.4 Warum die Groß- und Kleinschreibung wichtig ist
    gp 6.4.5 Statische Eigenschaften und Objekteigenschaften
    gp 6.4.6 Statische Variablen zum Datenaustausch
    gp 6.4.7 Statische Blöcke als Klasseninitialisierer
  gp 6.5 Konstanten und Aufzählungen
    gp 6.5.1 Konstanten über öffentliche statische final-Variablen
    gp 6.5.2 Problem mit finalen Klassenvariablen
    gp 6.5.3 Typsicherere Konstanten
    gp 6.5.4 Aufzählungen mit enum in Java 5
    gp 6.5.5 enum-Konstanten in switch
  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 und in statischen Blöcken 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 Private Konstruktoren, Utility-Klassen, Singleton und Fabriken
  gp 6.7 Vererbung
    gp 6.7.1 Vererbung in Java
    gp 6.7.2 Einfach- und Mehrfachvererbung
    gp 6.7.3 Gebäude modelliert
    gp 6.7.4 Konstruktoren in der Vererbung
    gp 6.7.5 Sichtbarkeit protected
    gp 6.7.6 Das Substitutionsprinzip
    gp 6.7.7 Automatische und explizite Typanpassung
    gp 6.7.8 Typen testen mit dem binären Operator instanceof
    gp 6.7.9 Array-Typen und Kovarianz
    gp 6.7.10 Methoden überschreiben
    gp 6.7.11 Mit super eine Methode der Oberklasse aufrufen
    gp 6.7.12 Kovariante Rückgabetypen
    gp 6.7.13 Finale Klassen
    gp 6.7.14 Nicht überschreibbare Funktionen
    gp 6.7.15 Zusammenfassung zur Sichtbarkeit
    gp 6.7.16 Sichtbarkeit in der UML
    gp 6.7.17 Zusammenfassung: Konstruktoren und Methoden
  gp 6.8 Object ist die Mutter aller Oberklassen
    gp 6.8.1 Klassenobjekte
    gp 6.8.2 Objektidentifikation mit toString()
    gp 6.8.3 Objektgleichheit mit equals() und Identität
    gp 6.8.4 Klonen eines Objekts mit clone()
    gp 6.8.5 Hashcodes
    gp 6.8.6 Aufräumen mit finalize()
    gp 6.8.7 Synchronisation
  gp 6.9 Die Oberklasse gibt Funktionalität vor
    gp 6.9.1 Dynamisches Binden als Beispiel für Polymorphie
    gp 6.9.2 Keine Polymorphie bei privaten, statischen und finalen Methoden
    gp 6.9.3 Polymorphie bei Konstruktoraufrufen
  gp 6.10 Abstrakte Klassen und abstrakte Methoden
    gp 6.10.1 Abstrakte Klassen
    gp 6.10.2 Abstrakte Methoden
  gp 6.11 Schnittstellen
    gp 6.11.1 Ein Polymorphie-Beispiel mit Schnittstellen
    gp 6.11.2 Die Mehrfachvererbung bei Schnittstellen
    gp 6.11.3 Erweitern von Interfaces – Subinterfaces
    gp 6.11.4 Vererbte Konstanten bei Schnittstellen
    gp 6.11.5 Vordefinierte Methoden einer Schnittstelle
    gp 6.11.6 Abstrakte Klassen und Schnittstellen im Vergleich
    gp 6.11.7 CharSequence als Beispiel einer Schnittstelle
    gp 6.11.8 Die Schnittstelle Iterable
  gp 6.12 Innere Klassen
    gp 6.12.1 Statische innere Klassen und Schnittstellen
    gp 6.12.2 Mitglieds- oder Elementklassen
    gp 6.12.3 Lokale Klassen
    gp 6.12.4 Anonyme innere Klassen
    gp 6.12.5 this und Vererbung
    gp 6.12.6 Implementierung einer verketteten Liste
    gp 6.12.7 Funktionszeiger
  gp 6.13 Generische Datentypen
    gp 6.13.1 Einfache Klassenschablonen
    gp 6.13.2 Einfache Methodenschablonen
    gp 6.13.3 Umsetzen der Generics, Typlöschung und Raw-Types
    gp 6.13.4 Einschränken der Typen
    gp 6.13.5 Generics und Vererbung, Invarianz
    gp 6.13.6 Wildcards
  gp 6.14 Die Spezial-Oberklasse Enum
    gp 6.14.1 Methoden auf Enum-Objekten
    gp 6.14.2 enum mit eigenen Konstruktoren und Methoden
  gp 6.15 Dokumentationskommentare mit javaDoc
    gp 6.15.1 Einen Dokumentationskommentar setzen
    gp 6.15.2 Mit javadoc eine Dokumentation erstellen
    gp 6.15.3 HTML-Tags in Dokumentationskommentaren
    gp 6.15.4 Generierte Dateien
    gp 6.15.5 Weitere Dokumentationskommentare
    gp 6.15.6 javaDoc und Doclets
    gp 6.15.7 Veraltete (deprecated) Klassen, Konstruktoren und Methoden


Galileo Computing

6.6 Objekte anlegen und zerstören  downtop

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 schreiben  downtop

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, weil sie ohne vorherige Zuweisung beziehungsweise Initialisierung keinen Sinn ergeben würden.

Konstruktoren sehen zwar ähnlich wie Methoden aus, doch gibt es zwei Unterschiede:

gp  Sie tragen denselben Namen wie die Klasse und
gp  sehen wie eine Methode ohne Rückgabetyp aus, also noch nicht einmal mit void.

Konstruktoren können wie Methoden überladen, also mit unterschiedlichen Parameterlisten definiert sein. Dies gilt auch für den Diskotheken-Konstruktor. Die Disko soll sich mit einer Quadratmeteranzahl initialisieren lassen und auch mit einer Anzahl an Menschen und an 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 anzahlLeuteint quadratmeter )
  {
    this.anzahlLeute  = anzahlLeute;
    this.quadratmeter = quadratmeter;
  }
  }

Das UML-Diagramm für die Klasse sieht wie folgt aus – das void an den Konstruktoren ist unüblich und ein Fehler im Tool.

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

Abbildung 6.5   Konstruktoren im UML-Diagramm

Der Standard-Konstruktor

Wenn wir in unseren Klassen keinen Konstruktor angeben, legt der Compiler automatisch einen Standard-Konstruktor an. Der automatisch eingeführte Standard-Konstruktor hat immer die gleiche Sichtbarkeit wie die Klasse. Ist also die Klasse public, wird auch der automatisch eingeführte Konstruktor public sein. Ist die Klasse paketsichtbar, ist es auch der Konstruktor. 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, 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 das vielleicht wichtige Variablen initialisiert worden wären.

Das gilt etwa 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()-Methoden gesetzt und durch getXXX()-Methoden gelesen 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 Wrapper-Klassen wie Integer, Double oder Color, File, Font.

Konstruktor nimmt Objekt vom Typ der eigenen Klasse (Copy-Konstruktor)

Im Übrigen 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;
  }
  @Override
  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 aufrufen  downtop

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 über einen Parameter nimmt.

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

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

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

Zu erkennen ist, dass beide die Objektvariablen initialisieren und letztlich das Gleiche machen. Schlauer ist es, wenn der Konstruktor Disko(Disko) den Konstruktor Disko(int, int) der eigenen Klasse aufruft. Dann muss nicht gleicher Programmcode für die Initialisierung mehrfach ausprogrammiert werden. Die Sprache lässt eine solche Konstruktor-Verkettung mit dem Schlüsselwort this zu.

Listing 6.19   v6/Disko.java

package v6;
class Disko
{
  int anzahlLeute;
  int quadratmeter;
  Disko( int anzahlLeuteint quadratmeter )
  {
    this.anzahlLeute  = anzahlLeute;
    this.quadratmeter = quadratmeter;
  }
  Disko( Disko disko )
  {
      this( disko.anzahlLeutedisko.quadratmeter );
    }
}

Nehmen wir an, d sei ein bestehendes Disko-Objekt. Dann ist der Aufruf vom Konstruktor new Disko(d) gleichwertig zum direkten Aufruf von new Disko(d.anzahlLeute, d.quadrat-meter). Da stellt sich die Frage, ob das ein so großer Gewinn ist. Mit dem this() ist nicht weniger zu schreiben und auch nicht schneller ausgeführt. Doch diese manuelle Wiederholung hat auch 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 jeden 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 beim Initialisieren überall 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 xy;
  public Point() {
    this( 00 );
  }
  public Point( Point p ) {
    this( p.xp.y );
  }
  public Point( int xint y ) {
    this.x = x;
    this.y = y;
  }
}

Besonders der Aufruf this(0, 0) erscheint überflüssig, da Objektvariablen ohnehin schon mit null initialisiert werden. Ohne diese Zeile könnte der Standard-Konstruktor von möglichen Erweiterungen in Point(int, int) jedoch nicht profitieren.

Einschränkungen von this()

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

1. Der Aufruf von this() muss die erste Anweisung des Konstruktors sein.
       
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 ist, was ein Konstruktor leisten muss. Nichts darf vor der Initialisierung ausgeführt werden. Die zweite Einschränkung hat damit zu tun, 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 wg. Zugriff auf Objektvariable
    this( STANDARD );      // das wäre OK
  }
  Musikanlage( int watt )
  {
    this.watt = watt;
  }
}

Da Objektvariablen bis zu einem bestimmten Punkt noch nicht initialisiert sind – das erklärt der nächste Abschnitt –, 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; this(STANDARD) ist jedoch in Ordnung, weil STANDARD eine statische Variable ist.


Galileo Computing

6.6.3 Initialisierung der Objekt- und Klassenvariabledowntop

Eine wichtige Eigenschaft guter 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 die Variablen automatisch 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 einige zusätzliche Zeilen.

Betrachten wir dies zuerst für eine Objektvariable.

Listing 6.21   InitObjectVariable.java

class InitObjectVariable
{
  int j = 1;
  InitObjectVariable()
  {
    // nix neues
  }
  InitObjectVariable( int j )
  {
    this.j = j;
  }
  InitObjectVariable( int xint y )
  {
    // nix neues
  }
}

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 xint y )
  {
      j = 1;
    }
}

Wir erkennen, dass die Variable wirklich nur bei Aufruf des Konstruktors initialisiert wird. Die Zuweisung steht dabei in der ersten Zeile. Dies kann sich als Falle erweisen, denn problematisch ist etwa die Reihenfolge der Belegung.

Manuelle Nullung

Genau genommen initialisiert die Laufzeitumgebung jede Objekt- und Klassenvariable zunächst mit 0 und 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.

Klassenvariablen

Abschließend bleibt die Frage, wo Klassenvariablen initialisiert werden. Im Konstruktor ergibt 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 und in statischen Blöcken setzedowntop

Wie die Beispiele im vorangegangenen Kapitel zeigen, werden Objektvariablen erst im Konstruktor gesetzt und statische Variablen in einem static-Block. Diese Tatsache müssen wir jetzt mit finalen Variablen zusammenbringen, was uns dazu bringt, dass auch sie in Konstruktoren beziehungsweise in Initialisierungsblöcken zugewiesen werden. Im Unterschied zu nicht-finalen Variablen müssen finale Variablen auf jeden Fall gesetzt werden, und nur genau ein Schreibzugriff ist möglich.

Mit diesem Vorgehen lassen sich auch »variable« Konstanten angeben, deren Belegung sich erst zur Laufzeit ergibt. Im nächsten Beispiel soll eine Datei eine Konstante enthalten, die Hubble-Konstante.

Listing 6.22   hubblekonstante.txt

71

Die Hubble-Konstante bestimmt die Expansionsgeschwindigkeit des Universums und ist eine zentrale Größe in der Kosmologie. Dummerweise ist die genaue Bestimmung schwer und der Name Konstante eigentlich unpassend. Damit eine Änderung des Wertes nicht zur Neuübersetzung des Java-Programms führen muss, legen wir den Wert in eine Datei. Die Klasse liest in einem static-Block den Wert aus der Datei und belegt die finale statische Konstante.

Listing 6.23   LateConstant.java

import java.util.Scanner;
class LateConstant
{
    final static int  MWST  ;   // hier steht nicht = irgendwas
    final String      ISBN  ;   // hier auch nicht.
  static
  {
      MWST   = new Scanner(LateConstant.class.getResourceAsStream(
    "hubblekonstante.txt")).nextInt();
  }
  LateConstant()
  {
      ISBN   = "3572100100";
  }
  public static void main( String[] args )
  {
    System.out.println( MWST );                        // 71
    System.out.println( new LateConstant().ISBN );     // 3572100100
  }
}

Die Methode getResourceAsStream() liefert einen Datenstrom zum Dateiinhalt, den die Klasse Scanner als Eingabequelle zum Lesen nutzt. Die Objektmethode nextInt() liest anschließend eine Ganzzahl aus der Datei.


Galileo Computing

6.6.5 Exemplarinitialisierer (Instanzinitialisiererdowntop

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. In den Blöcken lässt sich Programmcode setzen, der sonst in jedem Konstruktor kopiert oder andernfalls in eine extra Funktion zentralisiert werden müsste. Mit dem Exemplarinitialisierer lässt sich der Programmcode vereinfachen, denn der gemeinsame Teil kann in diesen Block gelegt werden, und wir haben Quellcode-Duplizierung im Quellcode vermieden. Allerdings hat die Technik gegenüber einer langweiligen Initialisierungsfunktion auch Nachteile.

gp  Zwar ist im Quellcode die Duplizierung nicht mehr vorhanden, aber in der Klassendatei steht sie wieder drin. Das liegt daran, dass der Compiler alle Anweisungen des Exem-plarinitialisierers in jeden Konstruktor kopiert.
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 private Funktion wie initialize(), die das Objekt wie frisch erzeugt aufbaut, gar nicht so schlecht. Eine Funktion lässt sich immer aufrufen, und damit sind die Objektzustände wie neu.
gp  Die API-Dokumentation führt diesen Intialisierer nicht auf, vielleicht soll aber der Teil dokumentiert werden.

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.24   WerIstAustin.java

public class WerIstAustin
{
  String austinPowers = "Mike Myers";
  {
    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 Myers";
    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 Myers";
}

Galileo Computing

6.6.6 Zerstörung eines Objekts durch den Müllaufsammler  downtop

Glücklicherweise werden wir beim Programmieren von der lästigen Aufgabe befreit, Speicher von Objekten freizugeben. Wird ein Objekt nicht mehr referenziert, findet der Garbage-Collector dieses Objekt und 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 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.

Prinzipielle Arbeitsweise

Der GC erscheint hier als ominöses Ding, das die Objekte clever verwaltet. Doch was ist der GC? Implementiert ist er als unabhängiger Thread 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 HotSpot-Technologie schneller geworden. HotSpot ist seit Java 1.3 fester Bestandteil des Java SDK. HotSpot verwendet einen generationenorientierten GC, der ausnutzt, dass zwei Gruppen von Objekten mit deutlich unterschiedlicher 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 meinte: »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 mit der Arbeit, wenn es zu spät und der Speicher schon voll ist.


Galileo Computing

6.6.7 Implizit erzeugte String-Objekte  downtop

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.

Neue String-Objekte durch +

Der letzte Fall einer impliziten Objekterzeugung hat wieder mit Zeichenketten zu tun.

String s = "Peter Lord " + ’&’ + " Nick Park";  //<a href="#ftn3"><sup>3 
    </sup></a>

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 = new StringBuffer("Peter Lord ").
               append  (&).  append  (" Nick Park").toString();

Das zu wissen, ermöglicht gute Optimierungen, besonders in Schleifen.


Galileo Computing

6.6.8 Private Konstruktoren, Utility-Klassen, Singleton und Fabriken  toptop

Ein Konstruktor kann privat sein, was verhindert, dass von außen ein Exemplar dieser Klasse gebildet werden kann, was auf den ersten Blick ziemlich beschränkt erscheint, erweist sich als ziemlich clever, wenn damit die Exemplarbildung bewusst verhindert werden soll. Sinnvoll ist das etwa bei den so genannten Utility-Klassen. Das sind Klassen, die nur statische Funktionen besitzen, also Hilfsklassen sind. Beispiele für diese Hilfsklassen gibt es zur Genüge, zum Beispiel Math. Warum sollte es hier Exemplare geben? Für den Aufruf von max() ist das nicht nötig. Also wird die Bildung von Objekten erfolgreich mit einem privaten Konstruktor unterbunden.

Wenn ein Konstruktor privat ist, bedeutet das noch lange nicht, dass keine Exemplare mehr erzeugt werden können. Ein privater Konstruktor besagt nur, dass er von außen nicht sichtbar ist – aber die Klasse selbst kann ihn ebenso wie private Funktionen »sehen« und zur Objekterzeugung nutzen. Objektfunktionen kommen dafür nicht in Frage, da ähnlich wie beim Henne-Ei-Problem ja vorher ein Objekt nötig wäre. Es bleiben somit die statischen Funktionen.

Singleton

Ein Singleton stellt sicher, dass es von einer Klasse nur ein Exemplar gibt. Nützlich ist das für »Dinge«, die es nur genau einmal in einer Applikation geben soll, etwa einen Logger für Protokollierungen.

Listing 6.25   Logger.java

public final class Logger
{
  private static Logger logger;
    private Logger()
    {
  }
  public static synchronized Logger getInstance()
  {
    if ( logger == null )
      logger = new Logger();
    return logger;
  }
  public void log( String s )
  {
    System.out.println( s );
  }
}

Interessant sind einmal der private Konstruktor und zum anderen die Anfrage-Funktion. Gibt es noch kein Exemplar des Loggers, bildet die Funktion eines und weist es der Klassenvariablen zu. synchronized schützt bei parallelen Zugriffen (dazu später mehr).

Listing 6.26   LoggerUser.java

public class LoggerUser
{
  public static void main( String[] args )
  {
      Logger.getInstance().log( "Log mich!" );
    }
}

Fabrik-Methoden

Eine Fabrik geht noch einen Schritt weiter als ein Singleton. Sie erzeugt nicht exakt ein Exemplar, sondern unter Umständen auch mehrere. Die grundlegende Idee jedoch ist, dass der Anwender nicht über einen Konstruktor ein Exemplar erzeugt, sondern im Allgemeinen über eine statische Funktion. Dies hat den Vorteil, dass die Funktion

gp  alte Objekte aus einem Cache wiedergeben kann;
gp  den Erzeugungsprozess auf Unterklassen verschieben kann und
gp  null zurückgeben darf.

Ein Konstruktor erzeugt immer ein Exemplar der eigenen Klasse. Eine Rückgabe wie null kann ein Konstruktor nicht liefern, denn bei new wird immer ein neues Objekt gebaut. Fehler könnten nur über eine Exception angezeigt werden.

In der Java-Bibliothek gibt es eine Unmenge von Beispielen für Fabrik-Methoden. Durch eine Namenskonvention sind sie leicht zu erkennen: Meistens heißen sie getInstance(). Eine Suche in der API-Dokumentation fördert gleich 90 solcher Funktionen hervor. Viele sind parametrisiert, um der Funktion anzuzeigen, was sie genau erzeugen soll. Nehmen wir zum Beispiel die Fabrik-Methode vom java.util.Calendar:

gp  Calendar.getInstance()
gp  Calendar.getInstance( java.util.Locale )
gp  Calendar.getInstance( java.util.TimeZone )

Die nicht parametrisierte Funktion gibt ein Standard-Calendar-Objekt zurück. Calendar ist aber selbst eine abstrakte Basisklasse, und innerhalb der Funktion befindet sich Quellcode wie der folgende:

static Calendar getInstance()
{
  return new GregorianCalendar();
}

Im Rumpf der Erzeuger-Funktion getInstance() wird bewusst die Unterklasse GregorianCalendar ausgewählt, die Calendar erweitert. Das ist möglich, da durch Vererbung eine Ist-eine-Art-von-Beziehung gilt und GregorianCalendar ein Calendar ist. Der Aufrufer von getInstance() bekommt das nicht mit, und er empfängt wie gewünscht ein Calendar-Objekt. Mit dieser Möglichkeit kann getInstance() testen, in welchem Land die JVM läuft, und abhängig davon die passende Calendar-Implementierung auswählen.




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.

 << zurück




Copyright © Galileo Press GmbH 2005
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, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de