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.13 Innere Klassendowntop

Bisher haben wir nur Klassen kennen gelernt, die entweder in Paketen organisiert waren oder in einer Datei. Diese Form von Klassen heißen »Top-Level-Klassen«. Es gibt darüber hinaus die Möglichkeit, eine Klasse in eine andere Klasse hineinzunehmen und sie damit noch enger aneinander zu binden. Eine Klasse, die so eingebunden wird, heißt »innere Klasse«. Im Allgemeinen sieht dies wie folgt aus:

class Außen {
  class Innen {
  }
}

Die Java-Sprache definiert vier Typen von inneren Klassen, die im Folgenden beschrieben werden.


Galileo Computing

6.13.1 Statische innere Klassen und Schnittstellendowntop

Die einfachste Variante einer inneren Klasse oder Schnittstelle wird wie eine statische Eigenschaft in die Klasse eingesetzt und nennt sich statische innere Klasse. Wegen der Schachtelung wird dieser Typ im Englischen nested top-level class genannt. Die Namensgebung betont mit dem Begriff top-level, dass die Klassen das Gleiche können wie »normale« Klassen oder Schnittstellen. Insbesondere sind keine Exemplare der äußeren Klasse nötig. Sun betont in der Spachspezifikation, dass die statischen inneren Klassen keine »echten« inneren Klassen sind, aber dass soll uns im Folgenden egal sein.


Beispiel Lampe ist die äußere Klasse, und Birne ist eine innere statische Klasse, die in Lampe geschachtelt ist.

Listing 6.54 Lampe.java

public class Lampe
{
  static String s = "Huhu";
  int i = 1;
  static class Birne
  {
    void grüßGott()
    {
      System.out.println( s );
//      System.out.println( i );          // Fehler
    }
  }
}

Die Eigenschaften der statischen inneren Klasse Birne besitzen Zugriff auf alle anderen statischen Eigenschaften der äußeren Klasse Lampe. Ein Zugriff auf Objektvariablen ist aus der statischen inneren Klasse nicht möglich, da sie als extra Klasse gezählt wird, die im gleichen Paket liegt. Die innere Klasse muss einen anderen Namen als die äußere haben.

Umsetzung der inneren Klassen

Es ist eine gelungene Arbeit der Sun-Entwickler, die Einführung von inneren Klassen ohne Änderung der virtuellen Maschine über die Bühne gebracht zu haben. Der Compiler generiert aus den inneren Klassen nämlich einfach normale Klassen, die jedoch mit einigen Spezialfunktionen ausgestattet sind. Für die entschachtelten inneren Klassen generiert der Compiler neue Namen nach dem Muster: ÄußereKlasse$InnereKlasse, das heißt, ein Dollar-Zeichen trennt die Namen von äußerer und innerer Klasse.


Galileo Computing

6.13.2 Mitglieds- oder Elementklassendowntop

Eine Mitgliedsklasse (engl. member class), auch Elementklasse genannt, ist ebenfalls vergleichbar mit einem Attribut, nur ist es nicht statisch. (Statische innere Klassen lassen sich aber auch als statische Mitgliedsklassen bezeichnen.) Die innere Klasse kann zusätzlich auf alle Attribute der äußeren Klasse zugreifen. Dazu zählen auch die privaten Eigenschaften, eine Designentscheidung, die sehr umstritten ist und kontrovers diskutiert wird.


Beispiel Rahmen besitzt eine innere Mitgliedsklasse Muster.

Listing 6.55 Rahmen.java

public class Rahmen
{
  String s = "kringelich";
  class Muster
  {
    void standard()
 {
      System.out.println( s );
    }
//    static void immer()  {  }   // Fehler
  }
}

Ein Exemplar der Klasse Muster hat Zugriff auf alle Eigenschaften von Rahmen. Um innerhalb der äußeren Klasse Rahmen ein Exemplar von Muster zu erzeugen, muss ein Exemplar der äußeren Klasse existieren. Das ist eine wichtige Unterscheidung gegenüber den statischen inneren Klassen von weiter oben. Statische innere Klassen existieren auch ohne Objekt der äußeren Klasse. Eine zweite wichtige Eigenschaft ist, dass innere Mitgliedsklassen selbst keine statischen Eigenschaften definieren dürfen.


Beispiel Für das Beispiel Rahmen und Muster erzeugt der Compiler die Dateien Rahmen.class und Rahmen$Muster.class. Damit ohne Änderung der virtuellen Maschine die innere Klasse an die Attribute der äußeren kommt, wird in jedem Exemplar der inneren Klasse eine Referenz auf das zugehörige Objekt der äußeren Klasse gelegt. Damit kann die innere Klasse auch auf nicht statische Attribute der äußeren zugreifen. Für die innere Klasse ergibt sich folgendes Bild in Rahmen$Muster.class:
class Rahmen$Muster
{
  private Rahmen this$0;
  // ...
}

Die Variable this$0 ist eine Kopie der Referenz auf Rahmen.this. Die Konstruktoren der inneren Klasse erhalten einen zusätzlichen Parameter vom Typ Rahmen, mit dem die this$0-Variable initialisiert wird.

Innere Klassen von außen erzeugen

Innerhalb der äußeren Klassen kann einfach mit dem new-Operator ein Exemplar der inneren Klasse erzeugt werden. Kommen wir von außerhalb und wollen Exemplare der inneren Klasse erzeugen, so müssen wir bei Elementklassen sicherstellen, dass es ein Exemplar der äußeren Klasse gibt. Die Sprache schreibt eine neue Form für die Erzeugung mit new vor, die das allgemeine Format

ref.new InnereKlasse(...)

besitzt. Dabei ist ref eine Referenz der äußeren Klasse.


Beispiel Die Klasse Haus besitzt die innere Klasse Zimmer.
class Haus
{
  class Zimmer
  {
  }
}

Um von außen ein Objekt von Zimmer aufzubauen, schreiben wir

Haus h = new Haus();
Zimmer z = h.new Zimmer();

oder auch in einer Zeile

Zimmer z = new Haus().new Zimmer();

Die this-Referenz

Möchte eine innere Klasse In auf die this-Referenz der umgebenden Klasse Out zugreifen, so schreiben wir Out.this. Wenn sich Variablen überdecken, so schreiben wir Out.this.Eigenschaft, um an die Eigenschaften der äußeren Klasse zu gelangen. Das Schlüsselwort outer ist zwar im Sprachstandard reserviert, wird aber dafür nicht eingesetzt.


Beispiel Elementklassen können beliebig geschachtelt sein, und da der Name eindeutig ist, gelangen wir immer mit Klassenname.this an die jeweilige Eigenschaft.

Listing 6.56 Haus.java

class Haus
{
  String s = "Haus";
  class Zimmer
  {
    String s = "Zimmer";
    class Stuhl
    {
      String s = "Stuhl";
      void ausgabe()
      {
        System.out.println( s );               // Stuhl
        System.out.println( this.s );          // Stuhl
        System.out.println( Stuhl.this.s );    // Stuhl
        System.out.println( Zimmer.this.s );   // Zimmer
        System.out.println( Haus.this.s );     // Haus
      }
    }
  }
  public static void main( String args[] )
  {
    new Haus().new Zimmer().new Stuhl().ausgabe();
  }
}

Betrachten wir das obere Beispiel, dann lassen sich Objekte für die inneren Klassen Haus, Zimmer und Stuhl wie folgt erstellen:

Haus a = new Haus;                    // Exemplar von Haus
Haus.Zimmer b = a.new Zimmer();       // Exemplar von Zimmer in a
Haus.Zimmer.Stuhl c = b.new Stuhl();  // Exemplar von Stuhl in b
c.ausgabe();                          // Methode von Stuhl

Damit ist auch deutlich geworden, dass die Qualifizierung mit dem Punkt bei Haus.Zimmer.Stuhl nicht automatisch bedeutet, dass Haus ein Paket mit dem Unterpaket Zimmer ist, in dem die Klasse Stuhl existiert. Das macht es für die Lesbarkeit nicht gerade einfacher, und es droht eine Verwechslungsgefahr zwischen inneren Klassen und Paketen. Deshalb sollte die Namenskonvention befolgt werden: Klassennamen beginnen mit Großbuchstaben, Paketnamen mit Kleinbuchstaben.


Galileo Computing

6.13.3 Lokale Klassendowntop

Lokale Klassen sind auch innere Klassen, die jedoch nicht als Eigenschaft direkt in einer Klasse eingesetzt werden. Diese Form der inneren Klasse befindet sich in Anweisungsblöcken von Methoden oder Initialisierungsblöcken. Lokale Schnittstellen sind nicht möglich.


Beispiel Die main()-Methode besitzt eine innere Klasse mit einem Konstruktor, der auf die finale Variable j zugreift.

Listing 6.57 DrinnenMachtSpass.java

public class DrinnenMachtSpass
{
  public static void main( String args[] )
  {
    int i = 2;
    final int j = 3;
    class In
    {
      In() {
        System.out.println( j );
//        System.out.println( i );    // Fehler
      }
    }
    new In();
  }
}

Die Definition der inneren Klasse In ist wie eine Anweisung eingesetzt. Jede lokale Klasse kann auf Methoden der äußeren Klasse zugreifen und zusätzlich auf die lokalen Variablen und Parameter, die mit dem Modifizierer final als unveränderlich ausgezeichnet sind. Liegt die innere Klasse in einer statischen Methode, kann sie jedoch keine Objektmethode aufrufen. Eine weitere Einschränkung im Vergleich zu den Elementklassen ist, dass die Modifizierer public, protected, private und static nicht erlaubt sind.


Galileo Computing

6.13.4 Anonyme innere Klassendowntop

Anonyme Klassen gehen noch einen Schritt weiter als lokale Klassen. Sie haben keinen Namen und erzeugen immer automatisch ein Objekt. Klassendefinition und Objekterzeugung sind zu einem Sprachkonstrukt verbunden. Die allgemeine Notation ist Folgende:

new KlasseOderSchnitstelle() { /* Eigenschaften der inneren Klasse */ }

In dem Block geschweifter Klammern lassen sich nun Methoden und Attribute definieren. Hinter new steht der Name einer Klasse oder Schnittstelle.

gp Wenn hinter new der Klassenname A steht, dann ist die anonyme Klasse eine Unterklasse von A.
gp Wenn hinter new der Name einer Schnittstelle S steht, dann erbt die anonyme Klasse von Object und implementiert die Schnittstelle S. Implementiert sie nicht die Operationen der Schnittstelle, haben wir nichts davon, denn dann hätten wir eine abstrakte innere Klasse, von der kein Objekt erzeugt werden könnte.

Für anonyme innere Klassen gilt die Einschränkung, dass keine zusätzlichen extends- oder implements-Angaben möglich sind.


Beispiel Wir wollen eine innere Klasse schreiben, die Unterklasse von java.awt.Point ist. Sie soll die toString()-Methode überschreiben.

Listing 6.58 InnerToStringPoint.java

import java.awt.Point;
public class InnerToStringPoint
{
  public static void main( String args[] )
  {
    Point p = new Point( 10, 12 ) {
      public String toString() {
        return "(" + x + "," + y + ")";
      }
    };
    
    System.out.println( p );    // (10,12)
  }
}

Da sofort eine Unterklasse von Point definiert wird, fehlt der Name der inneren Klasse. Das einzige Exemplar dieser anonymen Klasse lässt sich über die Variable p weiterverwenden.


Hinweis Es lässt sich leicht ausmalen, dass eine innere Klasse zwar nützliche Methoden der Oberklasse überschreiben oder Schnittstellen implementieren kann. Neue Eigenschaften anzubieten wäre zwar legal, aber nicht sinnvoll. Denn von außen wären diese Methoden und Attribute unbekannt, da die zugänglichen Eigenschaften der Oberklasse den Zugriff festlegen. Deshalb sind auch anonyme Unterklassen von Object (ohne weitere implementierte Schnittstellen) nur selten nützlich.

Umsetzung innerer anonymer Klassen

Auch für innere anonyme Klassen erzeugt der Compiler eine normale Klassendatei. Wir haben gesehen, dass im Fall einer »normalen« inneren Klasse die Notation ÄußereKlasse$InnereKlasse gewählt wird. Das klappt bei anonymen inneren Klassen natürlich nicht mehr, da uns der Name der inneren Klasse fehlt. Der Compiler wählt daher folgende Notation für Klassennamen: InnerToStringDate$1. Falls es mehr als eine innere Klasse gibt, wird 1 zu 2, 2 zu 3 und so weiter.

Nutzung innerer Klassen für Threads

Sehen wir uns ein weiteres Beispiel für die Implementierung von Schnittstellen an. Um nebenläufige Programme zu implementieren, gibt es die Klasse Thread oder die Schnittstelle Runnable. Programmbausteine, die parallel ausgeführt werden sollen, werden bei einer Klasse in eine Methode run() gesetzt, und dann wird ein Exemplar im Konstruktor eines Thread-Objekts übergeben. Der Thread wird mit start() angekurbelt. Das geht gut mit einer inneren Klasse, die die Schnittstelle Runnable implementiert.

new Thread( new Runnable() {
  public void run() {
    for ( int i = 0; i < 10; i++ )
      System.out.println( i );
  };
}).start();
for ( int i = 0; i < 10; i++ )
  System.out.println( i );

In der Ausgabe wird zum Beispiel Folgendes erscheinen (hier komprimiert):

0 1 2 3 4 5 6 7 8 0 1 2 3 4 5 6 7 8 9 9

Der erste Thread beginnt recht schnell seine Zahlen auszugeben. Aber an der doppelten Zahl 9 sehen wir, dass die beiden Teile wirklich parallel arbeiten. Ausführliche Informationen finden sich im Thread-Kapitel.

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

(Strg)+[_____) nach der geschweiften Klammer listet eine Reihe von Methoden auf, die wir uns von Eclipse implementieren lassen können. Da entscheiden wir uns doch für run() ...

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

Konstruktoren innerer anonymer Klassen

Da anonyme Klassen keinen Namen haben, muss für Konstruktoren ein anderer Weg gefunden werden. Dazu dienen die so genannten Exemplarinitialisierungsblöcke. Das sind Blöcke in geschweiften Klammen direkt innerhalb einer Klasse.


Beispiel Die anonyme Klasse ist eine Unterklasse von Point und initialisiert im Konstruktor einen Punkt mit den Koordinaten -1, -1. Aus diesem speziellen Punkt-Objekt lesen wir dann die Koordinaten wieder aus.

Listing 6.59 AnonymUndInnen.java

import java.awt.*;
public class AnonymUndInnen
{
  public static void main( String args[] )
  {
    System.out.println( new Point() {
          { x = -1; y = -1; }
        }.getLocation() );
    System.out.println( new Point(-1,0) {
          { y = -1; }
        }.getLocation() );
  }
}

Diesen Effekt können wir natürlich einfacher ohne anonyme Klasse haben:

new Point(-1, -1).getLocation()

super()

Innerhalb eines Konstruktors kann kein super() verwendet werden, um den Konstruktor der Oberklasse aufzurufen. Dies liegt daran, dass automatisch ein super() in den Initialisierungsblock eingesetzt wird. Die Parameter für die gewünschte Variante des (überladenen) Oberklassen-Konstruktors werden am Anfang der Definition der anonymen Klasse angegeben. Dies zeigt das zweite Beispiel im Programm AnonymUndInnen.java:

System.out.println( new Point(-1,0) { { y = -1; } }.getLocation() );

Beispiel Wir initialisieren ein Objekt BigDecimal, welches beliebig große Ganzzahlen aufnehmen kann. Im Konstruktor der anonymen Unterklasse geben wir anschließend den Wert mit der geerbten toString()-Methode aus.
new java.math.BigDecimal( "12345678901234567890" ) {
  { System.out.println( toString() ); }
};


Galileo Computing

6.13.5 Eine Sich-Selbst-Implementierungdowntop

Eine Klasse kann entweder von einer Klasse erben oder eine (beziehungsweise mehrere) Schnittstellen implementieren. Es ergibt sich ein Sonderfall, wenn wir eine Schnittstelle implementieren, die innerhalb derjenigen Klasse definiert ist, die die Schnittstelle implementiert. Das sieht etwa so aus:

class Outer implements Outer.InterfaceInner
{
  interface InterfaceInner
  {
    void knexDirDeineWelt();
  }
  public void knexDirDeineWelt()
  {
  }
}

Prinzipiell spricht erst einmal nichts gegen diese Implementierung. Innere Klassen, wie InterfaceInner, werden auf eine extra Klassendatei abgebildet, da es innere Klassen beziehungsweise Schnittstellen für die Laufzeitumgebung sowieso nicht gibt. In unserem Fall könnte der Compiler die Datei Outer$InterfaceInner erzeugen. Im nächsten Schritt würde dann Outer diese Schnittstelle erweitern und wie im Beispiel eine Methode überschreiben.

So schön dies auch aussieht: Es funktioniert nicht! Frühere Compiler erlaubten diese Konstellation, aber sie führt zu zirkulären Abhängigkeiten:

cyclic inheritance involving Outer

Wenn InterfaceInner zuerst übersetzt würde und dann Outer, wäre es noch zu verstehen, doch Probleme verursachen zum Beispiel Definitionen in der inneren Klasse, die von der äußeren Klasse abhängig sind. Wir könnten etwa den Rückgabewert von knexDirDeineWelt() so ändern, dass es ein Outer-Objekt zurückliefert:

class Outer implements Outer.InterfaceInner
{
  interface InterfaceInner
  {
    Outer knexDirDeineWelt();
  }
  public Outer knexDirDeineWelt()
  {
  }
}

Jetzt sehen wir: Ohne InterfaceInner kein Outer, da dies knexDirDeineWelt() vorschreibt, und ohne Outer kein InterfaceInner, da sonst der Rückgabewert nicht bekannt ist. Mitunter wäre das Problem sogar lösbar, doch hier lässt der Compiler lieber die Finger davon.

Innere Klasse vor der äußeren Klasse

Dass es nicht unmöglich ist, eine innere Klasse von der äußeren abzuleiten, zeigt folgendes Beispiel:

interface I
{
  void boo();
  interface J extends I
  {
  }
   J foo();
}

Es ist möglich, dass die innere Schnittstelle die äußere erweitert. Es ist auch möglich, dass eine innere Klasse eine äußere erweitert:

interface Musiker
{
  void singe();
  public class Enrique_Iglesias implements Musiker
  {
    public void singe()
    {
      System.out.println( "Schubi dubi dab dubi dei." );
    }
  }
}

Galileo Computing

6.13.6 this und Vererbungdowntop

Wenn wir ein qualifiziertes this verwenden, dann bezeichnet C.this die äußere Klasse, also das umschließende Exemplar. Gilt jedoch die Beziehung C1.C2. ... Ci. ... Cn., dann haben wir mit Ci.this ein Problem, wenn Ci eine Oberklasse von Cn ist. Es geht also um den Fall, dass eine textuell umgebende Klasse zugleich auch Oberklasse ist. Das eigentliche Problem liegt darin, dass hier zweidimensionale Namensräume hierarchisch kombiniert werden. Die eine Dimension sind die Bezeichner beziehungsweise Methoden aus den lexikalisch umgebenden Klassen, die andere Dimension sind die ererbten Eigenschaften aus der Oberklasse. Hier sind beliebige Überlappungen und Mehrdeutigkeiten denkbar. Durch diese ungenaue Beziehung zwischen inneren Klassen und Vererbung kam es unter JDK 1.1 und 1.2 zu unterschiedlichen Ergebnissen.


Beispiel In der Klasse Schuh erweitert die innere Klasse Fuss den Schuh und überschreibt die Methode wasBinIch().

Listing 6.60 Schuh.java

public class Schuh
{
  void wasBinIch()
  {
    System.out.println( "Ich bin der Schuh des Manitu" );
  }
  class Fuss extends Schuh
  {
    void spannung()
    {
      Schuh.this.wasBinIch();
    }
    void wasBinIch()
    {
      System.out.println( "Ich bin ein Schuh.Fuss" );
    }
  }
  public static void main( String args[] )
  {
    new Schuh().new Fuss().spannung();
  }
}

Legen wir in der main()-Funktion ein Objekt der Klasse Fuss an, dann landen wir in der Klasse Fuss und nicht in Schuh. Das heißt, die Ausgabe ist:

Ich bin der Schuh des Manitu

Das bedeutet, dass in spannung() durch Schuh.this zwar das zum Fuss-Objekt gehörende Schuh-Exemplar gemeint ist, wir aber durch die Überschreibung dennoch in der Methode aus der Klasse Fuss landen. Vor 1.2 kam als Ergebnis die erste Zeichenkette heraus. Das Ergebnis unter JDK 1.2 ist analog zu ((Schuh)this).wasBinIch().


Galileo Computing

6.13.7 Implementierung einer verketteten Listedowntop

Verkettete Listen gibt es in Java seit der Java-2-Plattform als vordefinierte Klasse, so dass wir eigentlich die Implementierung nicht betrachten müssten. Da es für viele Leser jedoch noch ein Geheimnis ist, wie die dazu benötigten Pointer in Java abgebildet werden, sehen wir uns eine einfache Implementierung an. Zunächst benötigen wir eine Zelle, die Daten und eine Referenz auf das folgende Listenelement speichert. Die Zelle wird durch die Klasse Cell modelliert. Im UML-Diagramm taucht die innere Klasse dann nicht auf.

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

Abbildung 6.11 Definition einer verketteten Liste

Listing 6.61 LinkedListDemo.java

class LinkedList
{
  private class Cell
  {
    Object data;
    Cell next;
    public Cell( Object o )
    {
       data = o;
    }
  }
  private Cell head, tail;
  public void add( Object o )
  {
    Cell n = new Cell( o );
    if ( tail == null )
      head = tail = n;
    else
    {
      tail.next = n;
      tail = n;
    }
  }
  public String toString()
  {
    String s = "";
    Cell cell = head;
    while ( cell != null )
    {
      s = s + cell.data + " ";
      cell = cell.next;
    }
    return s;
  }
}

Eine Liste besteht nun aus einer Menge von Cell-Elementen. Da diese Objekte fest mit der Liste verbunden sind, ist hier der Einsatz von geschachtelten Klassen sinnvoll. Cell kann aber auch eine statische innere Klasse sein, das spart Platz und Zeit in der JVM. Die Liste selbst benötigt zum Einfügen nur einen Verweis auf den Kopf (erstes Element) und auf das Ende (letztes Element). Um nun ein Element dieser Liste hinzuzufügen, erzeugen wir zunächst eine neue Zelle n. Ist tail und head gleich null, heißt dies, dass es noch keine Elemente in der Liste gibt. Danach legen wir die Referenzen für Listenanfang und -ende auf das neue Objekt. Werden nun später Elemente eingefügt, hängen sie sich hinter tail. Wenn es schon Elemente in der Liste gibt, dann ist tail nicht gleich null, und es zeigt auf das letzte Element. Seine next-Referenz zeigt auf null und wird dann mit einem neuen Wert belegt, nämlich mit dem des neu beschafften Objekts n. Nun hängt es in der Liste, und das Ende muss noch angepasst werden. Daher legen wir die Referenz tail auch noch auf das neue Objekt.

Listing 6.62 LinkedListDemo.java

public class LinkedListDemo
{
  public static void main( String args[] )
  {
    LinkedList l = new LinkedList();
    l.add( "Hallo" );
    l.add( "Otto" );
    System.out.println( l );
  }
}

Galileo Computing

6.13.8 Funktionszeigertoptop

Das folgende Beispiel implementiert Funktionszeiger über Schnittstellen. Es beginnt mit der Markierungsschnittstelle Operator. Sie soll Basis-Schnittstelle für Operatoren sein. Von dieser Schnittstelle wollen wir BinaryOperator ableiten, eine Schnittstelle mit einer Definition.

Listing 6.63 functions/BinaryOperator.java

package functions;
public interface BinaryOperator extends Operator
{
  double calc( double a, double b );
}

Zum Test sollen die Operatoren für + und * implementiert werden:

Listing 6.64 functions/MulOperator.java

package functions;
public class MulOperator implements BinaryOperator
{
  public double calc( double a, double b )
  {
    return a * b;
  }
}

Listing 6.65 functions/AddOperator.java

package functions;
public class AddOperator implements BinaryOperator
{
  public double calc( double a, double b )
  {
    return a + b;
  }
}

Eine Sammlung von Operatoren speichert ein Operator-Manager. Bei ihm können wir dann über eine Kennung ein Berechungs-Objekt beziehen:

Listing 6.66 functions/OperatorManager.java

package functions;
public class OperatorManager
{
  public final static int ADD = 0;
  public final static int MUL = 1;
  
  private static Operator[] operators = {
    new AddOperator(),
    new MulOperator()
  };
  public static Operator getOperator( int id )
  {
    return operators[ id ];
  }
}

Wenn wir nun einen Operator wünschen, so fragen wir den OperatorManager nach dem passenden Objekt. Die Rückgabe wird ein Operator-Objekt sein. Dies definiert jedoch noch nichts, so dass wir eine Anpassung auf BinaryOperator vornehmen sollten. Dann können wir die Funktion calc() aufrufen.

BinaryOperator op = (BinaryOperator) OperatorManager.getOperator( OperatorManager.ADD );
System.out.println( op.calc( 12, 34 ) );

So verbirgt sich hinter jeder Id eine Funktion, die wie ein Funktionszeiger verwendet werden kann. Noch interessanter ist es, die Funktionen in einen Assoziativspeicher einzusetzen und dann über einen Namen zu erfragen. Diese Implementierung nutzt kein Feld, sondern eine Datenstruktur Map.





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