Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Vorwort
1 Java ist auch eine Sprache
2 Sprachbeschreibung
3 Klassen und Objekte
4 Der Umgang mit Zeichenketten
5 Mathematisches
6 Eigene Klassen schreiben
7 Angewandte Objektorientierung
8 Exceptions
9 Die Funktionsbibliothek
10 Threads und nebenläufige Programmierung
11 Raum und Zeit
12 Datenstrukturen und Algorithmen
13 Dateien und Datenströme
14 Die eXtensible Markup Language (XML)
15 Grafische Oberflächen mit Swing
16 Grafikprogrammierung
17 Netzwerkprogrammierung
18 Verteilte Programmierung mit RMI und Web-Services
19 JavaServer Pages und Servlets
20 Applets
21 Midlets und die Java ME
22 Datenbankmanagement mit JDBC
23 Reflection und Annotationen
24 Logging und Monitoring
25 Sicherheitskonzepte
26 Java Native Interface (JNI)
27 Dienstprogramme für die Java-Umgebung
A Die Begleit-DVD
Stichwort
Download: - ZIP, ca. 12,5 MB
Buch bestellen
Ihre Meinung?
<< zurück | Galileo Computing / <openbook> /Java-Insel | vor >>
Java ist auch eine Insel von Christian Ullenboom Programmieren mit der Java Standard Edition Version 6
Java ist auch eine Insel 7., aktualisierte Auflage geb., mit DVD (November 2007) 1.492 S., 49,90 Euro Galileo Computing ISBN 978-3-8362-1146-8 |
| 6 Eigene Klassen schreiben
|  | 6.1 Eigene Klassen mit Eigenschaften deklarieren
|  |  | 6.1.1 Attribute deklarieren
|  |  | 6.1.2 Methoden deklarieren
|  |  | 6.1.3 Die this-Referenz
|  | 6.2 Privatsphäre und Sichtbarkeit
|  |  | 6.2.1 Für die Öffentlichkeit: public
|  |  | 6.2.2 Paketsichtbar
|  |  | 6.2.3 Kein Public Viewing – Passwörter sind privat
|  |  | 6.2.4 Wieso nicht freie Methoden und Variablen für alle?
|  |  | 6.2.5 Privat ist nicht ganz privat: Es kommt darauf an, wer’s sieht
|  |  | 6.2.6 Zugriffsmethoden für Attribute deklarieren
|  |  | 6.2.7 Setter und Getter nach der JavaBeans-Spezifikation
|  | 6.3 Statische Methoden und statische Attribute
|  |  | 6.3.1 Warum statische Eigenschaften sinnvoll sind
|  |  | 6.3.2 Statische Eigenschaften mit static
|  |  | 6.3.3 Statische Eigenschaften über Referenzen nutzen?
|  |  | 6.3.4 Warum die Groß- und Kleinschreibung wichtig ist
|  |  | 6.3.5 Statische Eigenschaften und Objekteigenschaften
|  |  | 6.3.6 Statische Variablen zum Datenaustausch
|  |  | 6.3.7 Statische Blöcke als Klasseninitialisierer
|  | 6.4 Konstanten und Aufzählungen
|  |  | 6.4.1 Konstanten über öffentliche statische finale Variablen
|  |  | 6.4.2 Eincompilierte Belegungen der Klassenvariablen
|  |  | 6.4.3 Typ(un)sicherere Aufzählungen
|  |  | 6.4.4 Aufzählungen mit enum
|  | 6.5 Objekte anlegen und zerstören
|  |  | 6.5.1 Konstruktoren schreiben
|  |  | 6.5.2 Der Standard-Konstruktor
|  |  | 6.5.3 Parametrisierte und überladene Konstruktoren
|  |  | 6.5.4 Konstruktor nimmt ein Objekt vom eigenen Typ an (Copy-Konstruktor)
|  |  | 6.5.5 Einen anderen Konstruktor der gleichen Klasse aufrufen
|  |  | 6.5.6 Initialisierung der Objekt- und Klassenvariablen
|  |  | 6.5.7 Finale Werte im Konstruktor und in statischen Blöcken setzen
|  |  | 6.5.8 Exemplarinitialisierer (Instanzinitialisierer)
|  |  | 6.5.9 Ihr fehlt uns nicht – der Garbage-Collector
|  |  | 6.5.10 Implizit erzeugte String-Objekte
|  |  | 6.5.11 Private Konstruktoren, Utility-Klassen, Singleton, Fabriken
|  | 6.6 Assoziationen zwischen Objekten
|  |  | 6.6.1 Unidirektionale 1:1-Beziehung
|  |  | 6.6.2 Bidirektionale 1:1-Beziehungen
|  |  | 6.6.3 Unidirektionale 1:n-Beziehung
|  | 6.7 Vererbung
|  |  | 6.7.1 Vererbung in Java
|  |  | 6.7.2 Spielobjekte modelliert
|  |  | 6.7.3 Einfach- und Mehrfachvererbung
|  |  | 6.7.4 Sichtbarkeit protected
|  |  | 6.7.5 Konstruktoren in der Vererbung und super
|  |  | 6.7.6 Automatische und explizite Typanpassung
|  |  | 6.7.7 Das Substitutionsprinzip
|  |  | 6.7.8 Typen mit dem binären Operator instanceof testen
|  |  | 6.7.9 Methoden überschreiben
|  |  | 6.7.10 Mit super an die Eltern
|  |  | 6.7.11 Kovariante Rückgabetypen
|  |  | 6.7.12 Array-Typen und Kovarianz
|  |  | 6.7.13 Zusammenfassung zur Sichtbarkeit
|  | 6.8 Dynamisches Binden
|  |  | 6.8.1 Unpolymorph bei privaten, statischen und finalen Methoden
|  |  | 6.8.2 Polymorphie bei Konstruktoraufrufen
|  |  | 6.8.3 Finale Klassen
|  |  | 6.8.4 Nicht überschreibbare (finale) Methoden
|  | 6.9 Abstrakte Klassen und abstrakte Methoden
|  |  | 6.9.1 Abstrakte Klassen
|  |  | 6.9.2 Abstrakte Methoden
|  | 6.10 Schnittstellen
|  |  | 6.10.1 Deklarieren von Schnittstellen
|  |  | 6.10.2 Implementieren von Schnittstellen
|  |  | 6.10.3 Markierungsschnittstellen
|  |  | 6.10.4 Ein Polymorphie-Beispiel mit Schnittstellen
|  |  | 6.10.5 Die Mehrfachvererbung bei Schnittstellen
|  |  | 6.10.6 Keine Kollisionsgefahr bei Mehrfachvererbung
|  |  | 6.10.7 Erweitern von Interfaces – Subinterfaces
|  |  | 6.10.8 Vererbte Konstanten bei Schnittstellen
|  |  | 6.10.9 Schnittstellenmethoden, die nicht implementiert werden müssen
|  |  | 6.10.10 Abstrakte Klassen und Schnittstellen im Vergleich
|  | 6.11 Geschachtelte (innere) Klassen, Schnittstellen, Aufzählungen
|  |  | 6.11.1 Statische innere Klassen und Schnittstellen
|  |  | 6.11.2 Mitglieds- oder Elementklassen
|  |  | 6.11.3 Lokale Klassen
|  |  | 6.11.4 Anonyme innere Klassen
|  |  | 6.11.5 this und Vererbung
|  | 6.12 Generische Datentypen
|  |  | 6.12.1 Einfache Klassenschablonen
|  |  | 6.12.2 Einfache Methodenschablonen
|  |  | 6.12.3 Umsetzen der Generics, Typlöschung und Raw-Types
|  |  | 6.12.4 Einschränken der Typen
|  |  | 6.12.5 Generics und Vererbung, Invarianz
|  |  | 6.12.6 Wildcards
|  | 6.13 Die Spezial-Oberklasse Enum
|  |  | 6.13.1 Methoden auf Enum-Objekten
|  |  | 6.13.2 enum mit eigenen Konstruktoren und Methoden
|  | 6.14 Dokumentationskommentare mit JavaDoc
|  |  | 6.14.1 Einen Dokumentationskommentar setzen
|  |  | 6.14.2 Mit javadoc eine Dokumentation erstellen
|  |  | 6.14.3 HTML-Tags in Dokumentationskommentaren
|  |  | 6.14.4 Generierte Dateien
|  |  | 6.14.5 Dokumentationskommentare im Überblick
|  |  | 6.14.6 JavaDoc und Doclets
|  |  | 6.14.7 Veraltete (deprecated) Klassen, Konstruktoren und Methoden
## 6.5 Objekte anlegen und zerstören 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. ### 6.5.1 Konstruktoren schreiben Wenn der new-Operator ein Objekt angelegt, wird ein Konstruktor der Klasse automatisch aufgerufen. 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.
    class Player
    {

      Player()
      {
      }
    }
Konstruktordeklarationen sehen ähnlich wie Methodendeklarationen aus – so gibt es auch Sichtbarkeiten und Überladung –, doch bestehen zwei deutliche Unterschiede: Konstruktoren tragen immer denselben Namen wie die Klasse. Konstruktorendeklarationen besitzen keinen Rückgabetyp, also noch nicht einmal void. Dass der Konstruktor während der Initialisierung und damit vor einem äußeren Methodenaufruf aufgerufen wird, soll ein kleines Beispiel die Aufrufreihenfolge zeigen. Listing 6.27 Dungeon.java
    class Dungeon
    {
      Dungeon()
      {
        play();
        System.out.println( "constructor" );
        play();
      }

      void play()
      {
        System.out.println( "play()" );
      }

      public static void main( String[] args )
      {
        System.out.println( "Before constructor" ); // Before constructor
        Dungeon d = new Dungeon();                  // play()
                                                    // constructor
                                                    // play()
        System.out.println( "After constructor" );  // After constructor
        d.play();                                   // play()
      }
    }
Die Ausgabe auf dem Bildschirm ist in den Kommentaren angegeben. ### 6.5.2 Der Standard-Konstruktor Wenn wir in unseren Klassen keinen Konstruktor angeben, legt der Compiler automatisch einen Standard-Konstruktor (auch Default-Konstruktor genannt) an. Schreiben wir nur
    class Player
    {
    }
macht der Compiler daraus immer automatisch:
    class Player
    {
      Player() { }
    }
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. ### 6.5.3 Parametrisierte und überladene Konstruktoren Konstruktoren können wie Methoden überladen, also mit unterschiedlichen Parameterlisten deklariert sein. Dies soll auch für den Spieler gelten: public Player( String name ) public Player( String name, String item ) Der Player soll sich mit einem Namen und alternativ auch mit einem Gegenstand initialisieren lassen. Listing 6.28 com/tutego/insel/game/v6/Player.java, Player
    class Player
    {
      String name;
      String item;

      public Player( String name )
      {
        this.name = name;
      }

      public Player( String name, String item )
      {
        this.name = name;
        this.item = item;
      }
    }
Das kann so aussehen: Listing 6.29 com/tutego/insel/game/v6/Playground.java, main()
    Player spuderman = new Player( "Spuderman" ) ;
    System.out.println( spuderman.name ); // Spuderman
    System.out.println( spuderman.item ); // null
    Player holk      = new Player( "Holk", "green color" );
    System.out.println( holk.name );      // Holk
    System.out.println( holk.item );      // green color
#### Wann der Compiler keinen Standard-Konstruktor einfügt Wenn es mindestens einen ausprogrammierten Konstruktor gibt, legt der Compiler einen Standard-Konstruktor nicht mehr automatisch an. Der Versuch, bei unserer Spieler-Klasse ein Objekt einfach mit dem Standard-Konstruktor über new Player() zu erzeugen, führt zu einem Übersetzungsfehler, da es keinen Compiler-generierten Konstruktor gibt.
    Player p = new Player();        // The constructor Player() is undefined
Dass der Standard-Konstruktor nicht angelegt wird, hat seinen guten Grund: Es ließe sich sonst ein Objekt anlegen, ohne das vielleicht wichtige Variablen initialisiert worden wären. So ist das bei unserem Spieler. Die parametrisierte Konstruktoren erzwingen, dass beim Erzeugen ein Spielername angegeben werden muss, sodass nach dem Aufbau auf jeden Fall ein Spielername vorhanden ist. Wenn wir ermöglichen wollen, dass Entwickler neben den parametrisierten Konstruktoren auch einen parameterlosen Standard-Konstruktor nutzen können, müssten wir diesen per Hand hinzufügen. #### 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. Wenn ein Objekt Attribute besitzt, die nicht über setXXX()-Funktionen modifiziert werden können, diese Werte aber bei der Objekterzeugung wichtig sind, so bleibt nichts anderes übrig, als die Werte im Konstruktor einzufügen. (Eine setXXX()-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 und Font. #### Finale Werte aus dem Konstruktor belegen Eine finale Variable darf nur einmal belegt werden. Das bedeutet nicht zwingend, dass sie am Deklarationsort mit einem Wert belegt werden muss, sondern es ist möglich, das auch später vorzunehmen. Der Konstruktor darf zum Beispiel finale Objektvariablen beschreiben. Das Paar finale Variable und initialisierender Konstruktor ist ein häufig genutztes Idiom, wenn Variablenwerte später nicht mehr geändert werden sollen. So ist im Folgenden die Variable pattern final, da sie nur einmalig über den Konstruktor gesetzt und anschließend nur noch gelesen wird. Listing 6.30 Pattern.java
    public class Pattern
    {
      private final String pattern;

      public Pattern( String pattern )
      {
        this.pattern = pattern;
      }

      public String getPattern()
      {
        return pattern;
      }
    }
Java-Stil Immer dann, wenn sich bis auf die direkte Initialisierupng vor Ort oder im Konstruktor die Belegung nicht mehr ändert, sollten Entwickler finale Variablen verwenden.
### 6.5.4 Konstruktor nimmt ein Objekt vom eigenen Typ an (Copy-Konstruktor) Ein Konstruktor ist außerordentlich praktisch, wenn er ein typgleiches Objekt über seinen Parameter entgegennimmt, und aus diesem Objekt die Startwerte für seinen eigenen Zustand nimmt. Ein solcher Konstruktor heißt Copy-Konstruktor. Dazu ein Beispiel: Die Klasse Player bekommt einen Konstruktor, der einen anderen Spieler als Parameter entgegennimmt. Auf diese Weise lässt sich ein schon initialisierter Spieler als Vorlage für die Attributwerte nutzen. Alle Eigenschaften des existierenden Spielers können so auf den neuen Spieler übertragen werden. Die Implementierung kann so aussehen: Hier klicken, um das Bild zu vergrößern Listing 6.31 com/tutego/insel/game/v7/Player.java, Player
    class Player
    {
      String name;
      String item;

      Player()
      {
      }

      Player( Player player )
      {
        name = player.name;
        item = player.item;
      }
    }
Die main()-Funktion soll jetzt einen neuen Spieler patric erzeugen und anschließend wiederum einen neuen Spieler tryk mit den Werten von patric initialisieren: Listing 6.32 com/tutego/insel/game/v7/Playground.java, main()
    Player patric = new Player();
    patric.name = "Patric Circle";
    patric.item = "knot";

    Player tryk = new Player( patric );
    System.out.println( tryk.name );  // Patric Circle
    System.out.println( tryk.item );  // knot
Hinweis Wenn die Klasse Player neben dem parametrisierten Konstruktor Player(Player) einen zweiten, Player(Object) deklarieren würde, käme es bei einem Nutzen durch new Player(patric) auf den ersten Blick zu einem Konflikt, denn beide Konstruktoren würden passen. Der Java-Compiler löst das so, dass er immer den spezifischsten Konstruktor aufruft, also Player(Player) und nicht Player(Object). Das gilt auch für new Player(null) – auch hier wird der Konstruktor Player(Player) bemüht. Während die Frage für den Alltag nicht so bedeutend ist, müssen sich Kandidaten der Java-Zertifizierung »Sun Certified Java Programmer« auf eine solche Frage einstellen.
### 6.5.5 Einen anderen Konstruktor der gleichen Klasse aufrufen 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 einen Spieler als Vorlage über einen Parameter nimmt, aber auch einen anderen Konstruktor, der den Namen und den Gegenstand direkt entgegennimmt: Listing 6.33 com/tutego/insel/game/v8/Player.java, Player
    class Player
    {
      String name;
      String item;

      Player( Player player )
      {
        name = player.name;
        item = player.item;
      }

      Player( String name, String item )
      {
        this.name = name;
        this.item = item;
      }
    }
Zu erkennen ist, dass beide Konstruktoren die Objektvariablen initialisieren und letztlich das Gleiche machen. Schlauer ist es, wenn der Konstruktor Player(Player) den Konstruktor Player(String, String) der eigenen Klasse aufruft. Dann muss nicht gleicher Programmcode für die Initialisierung mehrfach ausprogrammiert werden. Java lässt eine solche Konstruktor-Verkettung mit dem Schlüsselwort this zu. Listing 6.34 com/tutego/insel/game/v9/Player.java, Player
    class Player
    {
      String name;
      String item;

      Player()
      {
        this( "", "" );
      }

      Player( Player player )
      {
        this( player.name, player.item );
      }

      Player( String name, String item )
      {
        this.name = name;
        this.item = item;
      }
    }
Der Gewinn gegenüber der vorherigen Lösung ist, dass es nur eine zentrale Stelle gibt, die im Fall von Änderungen angefasst werden müsste. 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 wiedererkennen. #### Einschränkungen von this() Beim Aufruf eines anderen Konstruktors mittels this() gibt es zwei wichtige Beschränkungen: Der Aufruf von this() muss die erste Anweisung des Konstruktors sein. 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.35 Stereo.java
    public class Stereo
    {
      static final int STANDARD = 1000;
      int standard = 1000;
      int watt;

      Stereo()
      {
        // this( standard );        // nicht zulässig wegen Zugriff auf Objektvariable
        this( STANDARD );           // das ist OK
      }

      Stereo( int watt )
      {
        this.watt = watt;
      }
    }
Da Objektvariablen bis zu einem bestimmten Punkt noch nicht initialisiert sind (was der nächste Abschnitt erklärt), 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. ### 6.5.6 Initialisierung der Objekt- und Klassenvariablen 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 deklariert 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.36 InitObjectVariable.java
    class InitObjectVariable
    {
      int j = 1;

      InitObjectVariable()
      {
        // nix Neues
      }

      InitObjectVariable( int j )
      {
        this.j = j;
      }

      InitObjectVariable( int x, int y )
      {
        // nix Neues
      }
    }
Die Variable j wird mit 1 belegt. Es ist wichtig zu wissen, an welcher Stelle Variablen ihre Werte bekommen. So erstaunlich es 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 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 NeedlessInitNull
    {
      int i = 0;
    }
Der Compiler würde nur zusätzlich in jeden Konstruktor die Initialisierung i = 0 einsetzen. [Wir wollen hier den Fall, dass der Konstruktor der Oberklasse i einen Wert ungleich 0 setzt, nicht betrachten. ] Aus diesem Grund ist auch Folgendes nicht meisterhaft:
    class NeedlessInitNull
    {
      int i = 0;
      NeedlessInitNull( int i ) { this.i = i; }
    }
Die Belegung für i wird sowieso überschrieben. #### 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;
      }
    }
### 6.5.7 Finale Werte im Konstruktor und in statischen Blöcken setzen 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. #### Konstante mit Dateiinhalt initialisieren 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 [http://de.wikipedia.org/wiki/Hubble-Konstante ] : Listing 6.37 hubble-constant.txt
    77
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 und belegen gerade nicht direkt die finale statische Konstantenvariable. Die Klasse liest in einem static-Block den Wert aus der Datei und belegt die finale statische Konstante. Listing 6.38 LateConstant.java
    import java.util.Scanner;

    class LateConstant
    {
      final static int    HUBBLE; // hier steht nicht = irgendwas
      final        String ISBN;   // hier auch nicht.

      static
      {
        HUBBLE = new Scanner(
         LateConstant.class.getResourceAsStream("hubble-constant.txt")).nextInt();
      }

      LateConstant()
      {
        ISBN = "3572100100";
      }

      public static void main( String[] args )
      {
        System.out.println( HUBBLE );                      // 77

        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. ### 6.5.8 Exemplarinitialisierer (Instanzinitialisierer) 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 Klassendeklaration 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. 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 Exemplarinitialisierers in jeden Konstruktor kopiert. Exemplarinitialisierer können schnell übersehen werden. Ein Blick auf den Konstruktor verrät uns dann nicht mehr, was er alles macht da verstreute Exemplarinitialisierer Initialisierungen ändern oder hinzufügen können. Die Initialisierung trägt damit nicht zur Übersichtlichkeit bei. Ein weiteres Manko ist, dass die Initialisierung nur bei neuen Objekten, also mit new() durchgeführt wird. Wenn Objekte wiederverwendet werden sollen, ist eine private Funktion wie initialize(), die das Objekt wie frisch erzeugt initialisiert, gar nicht so schlecht. Eine Funktion lässt sich immer aufrufen, und damit sind die Objektzustände wie neu. Die API-Dokumentation führt Exemplarinitialisierer nicht auf; die Konstruktoren müssen also die Aufgabe erklären. #### Mehrere Exemplarinitialisierer In einer Klasse können mehrere Exemplarinitialisierer auftauchen. Sie werden der Reihe nach durchlaufen, und zwar vor dem eigentlichen Konstruktor. Der Grund liegt in der Realisierung der Umsetzung: Der Programmcode der Exemplarinitialisierer wird an den Anfang aller Konstruktoren gesetzt. Objektvariablen wurden schon initialisiert. Ein Programmcode wie der folgende ... Listing 6.39 WhoIsAustin.java
    public class WhoIsAustin
    {
      String austinPowers = "Mike Myers";

      {
        System.out.println( "1 " + austinPowers );
      }

      WhoIsAustin()
      {
        System.out.println( "2 " + austinPowers );
      }
    }
... wird vom Compiler also umgebaut zu:
    public class WhoIsAustin
    {
      String austinPowers;

      WhoIsAustin()
      {
        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 Exemplar-initialisierer diese auch deklariert sein muss. So führt Folgendes zu einem Fehler:
    class WhoIsDrEvil
    {
      {
        System.out.println( drEvil );      // Ein Compilerfehler
      }

      String drEvil = "Mike Myers";
    }
### 6.5.9 Ihr fehlt uns nicht – der Garbage-Collector 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 [Lange Tradition hat der Garbage-Collector unter LISP und unter Smalltalk, aber auch Visual Basic benutzt einen GC. ] 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: Ein Objekt kann gelöscht werden, aber die Referenz existiert noch (engl. dangling pointer). Kein Zeiger verweist auf ein bestimmtes Objekt, dieses existiert aber noch im Speicher (engl. memory leaks). #### Prinzipielle Arbeitsweise des Müllaufsammlers 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 Thread-Gruppen-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. #### Die manuelle Nullung und Speicherlecks Bei folgendem Szenario wird der GC das nicht mehr benötigte Objekt hinter der Referenzvariablen ref entfernen können, wenn die Laufzeitumgebung den inneren Block verlässt.
    {
      {
        StringBuffer ref = new StringBuffer();
      }
      // StringBuffer ref ist frei für den GC
    }
In fremden Programmen sind mitunter Anweisungen wie die folgende zu lesen:
    ref = null;
Oftmals sind sie unnötig, denn wie im Fall unseres Blockes weiß der GC, wann der letzte Verweis vom Objekt genommen wurde. Anders sieht das aus, wenn die Lebensdauer der Variablen größer ist, etwa bei einer Objekt- oder sogar statischen Variable oder sie in einem Feld referenziert wird. Wenn dann das referenzierte Objekt nicht mehr benötigt wird, sollte die Variable (oder der Feldeintrag) mit null belegt werden, da andernfalls der GC das Objekt aufgrund der starken Referenzierung nicht wegräumen würde. Zwar findet der GC jedes nicht mehr referenzierte Objekt, aber die Fähigkeit zur Devination, Speicherlecks durch unbenutzte, aber referenzierte Objekte aufzuspüren, hat er nicht. ### 6.5.10 Implizit erzeugte String-Objekte 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";
Der Plus-Operator zur Konkatenation von nicht konstanten Zeichenketten (konstante Zeichenketten werden vom Compiler zusammengefügt) erzeugt einen StringBuffer (bis Java 5 StringBuffer, dann StringBuilder), 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. ### 6.5.11 Private Konstruktoren, Utility-Klassen, Singleton, Fabriken 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.40 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.41 LoggerUser.java
    public class LoggerUser
    {
      public static void main( String[] args )
      {
        Logger.getInstance().log( "Log mich!" );
      }
    }
#### Fabrikmethoden Eine Fabrikmethode 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 alte Objekte aus einem Cache wiedergeben kann, den Erzeugungsprozess auf Unterklassen verschieben kann und 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 Fabrikmethoden. 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 Fabrikmethode vom java.util.Calendar: Calendar.getInstance() Calendar.getInstance( java.util.Locale ) 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 Erzeugerfunktion getInstance() wird bewusst die Unterklasse Gregorian-Calendar 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. ### Ihr Kommentar Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.
Name |
E-Mail |
Ihr Kommentar |
<< zurück | <top> | vor >>

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