Titel   Inhalt   Suchen   Index   API  Go To Java 2, Zweite Auflage, Handbuch der Java-Programmierung
 <<    <     >    >>  Kapitel 10 - OOP IV: Verschiedenes

10.1 Lokale und anonyme Klassen



10.1.1 Grundlagen

Bis zum JDK 1.0 wurden Klassen immer auf Paketebene definiert, eine Schachtelung war nicht möglich. Das hat die Compiler-Implementierung vereinfacht und die Struktur der Klassen innerhalb eines Pakets flach und übersichtlich gemacht. Unhandlich wurde dieses Konzept immer dann, wenn eine Klasse nur lokale Bedeutung hatte oder wenn "auf die Schnelle eine kleine Klasse" gebraucht wurde. Da es in Java keine Funktionszeiger gibt, besteht die einzige Möglichkeit, kleine Codebestandteile zwischen verschiedenen Programmteilen auszutauschen, darin, ein Interface zu deklarieren und die benötigte Funktionalität in einer implementierenden Klasse zur Verfügung zu stellen. Diese Technik wurde in Abschnitt 9.4.3 vorgestellt.

Insbesondere bei den Erweiterungen für die Programmierung grafischer Oberflächen, die mit dem JDK 1.1 eingeführt wurden, entstand der Wunsch nach einem flexibleren Mechanismus. Durch das neue Ereignismodell (siehe Kapitel 28) müssen seit dem JDK 1.1 sehr viel häufiger kleine Programmteile geschrieben werden, die nur in einem eng begrenzten Kontext eine Bedeutung haben. Die Lösung für dieses Problem wurde mit der Einführung von lokalen und anonymen Klassen geschaffen (im JDK werden sie als Inner Classes bezeichnet). Dabei wird innerhalb einer bestehenden Klasse X eine neue Klasse Y definiert, die nur innerhalb von X sichtbar ist. Objektinstanzen von Y können damit auch nur innerhalb von X erzeugt werden. Anders herum kann Y auf die Membervariablen von X zugreifen.

Lokale und anonyme Klassen sind ein mächtiges - und manchmal leicht verwirrendes - Feature von Java. Wir wollen nachfolgend seine wichtigsten Anwendungen vorstellen. Darüber hinaus gibt es seltener genutzte Varianten, die hauptsächlich durch trickreiche Anwendung von Modifiern auf die lokale Klasse oder ihrer Member entstehen. Auf diese wollen wir hier nicht weiter eingehen.

10.1.2 Nicht-statische lokale Klassen

Klassen in Klassen

Die Definition einer nicht-statischen lokalen Klasse entspricht genau dem zuvor beschriebenen Grundprinzip: innerhalb des Definitionsteils einer beliebigen Klasse wird eine neue Klasse definiert. Ihre Instanzierung muß innerhalb der äußeren Klasse erfolgen, also in einer der Methoden der äußeren Klasse oder während ihrer Initialisierung. Die innere Klasse kann auf die Membervariablen der äußeren Klasse zugreifen und umgekehrt. Das folgende Listing illustriert dies an einem einfachen Beispiel:

001 /* Listing1001.java */
002 
003 class Outer
004 {
005   String name;
006   int    number;
007 
008   public void createAndPrintInner(String iname)
009   {
010     Inner inner = new Inner(); 
011     inner.name = iname;
012     System.out.println(inner.getQualifiedName());
013   }
014 
015   class Inner
016   {
017     private String name;
018 
019     private String getQualifiedName()
020     {
021       return number + ":" + Outer.this.name + "." + name;
022     }
023   }
024 }
025 
026 public class Listing1001
027 {
028   public static void main(String[] args)
029   {
030     Outer outer = new Outer();
031     outer.name = "Outer";
032     outer.number = 77;
033     outer.createAndPrintInner("Inner");
034   }
035 }
Listing1001.java
Listing 10.1: Eine nicht-statische lokale Klasse

Zunächst wird eine Klasse Outer mit den Membervariablen name und number definiert. Innerhalb von Outer wird dann eine Klasse Inner definiert. Sie besitzt eine eigene Membervariable name und eine Methode getQualifiedName. Beim Programmstart erzeugt main eine Instanz von Outer und initialisiert ihre Membervariablen. Anschließend ruft sie die Methode createAndPrintInner auf.

In createAndPrintInner wird nun eine Instanz von Inner erzeugt und mit dem als Argument übergebenen Namen initialisiert. Die Instanzierung erfolgt also im Kontext der äußeren Klasse, und diese kann auf die Membervariable der inneren Klasse zugreifen. In der Praxis wichtiger ist jedoch die Möglichkeit, die innere Klasse auf die Membervariablen der äußeren Klasse zugreifen zu lassen. Dadurch wird ihr der Status der äußeren Klasse zugänglich gemacht und sie kann Programmcode erzeugen (und durch die Kapselung in ein Objekt nötigenfalls an eine ganz andere Stelle im Programm transferieren), der Informationen aus der Umgebung der Klassendefinition verwendet. Um dies zu zeigen, ruft Outer nun die Methode getQualifiedName der inneren Klasse auf.

In getQualifiedName wird auf drei unterschiedliche Arten auf Membervariablen zugegriffen. Bei der Verwendung von unqualifizierten Namen (also solchen ohne Klassennamen-Präfix) werden lexikalische Sichtbarkeitsregeln angewandt. Der Compiler prüft also zunächst, ob es eine lokale Variable dieses Namens gibt. Ist das nicht der Fall, sucht er nach einer gleichnamige Membervariable in der aktuellen Klasse. Ist auch diese nicht vorhanden, erweitert er seine Suche sukzessive von innen nach außen auf alle umschließenden Klassen. Im Beispiel bezeichnet name also die gleichnamige Membervariable von Inner und number diejenige von Outer. Wird die Membervariable einer äußeren Klasse durch eine gleichnamige Membervariable der inneren Klasse verdeckt, kann über den Präfix "Klassenname.this." auf die äußere Variable zugegriffen werden. Im Beispielprogramm wird das für die Variable number so gemacht.

Wird der Ausdruck "Klassenname.this" alleine verwendet, bezeichnet er das Objekt der äußeren Klasse, in der die aktuelle Instanz der inneren Klasse erzeugt wurde. In getQualifiedName würde Outer.this also die in Zeile 010 erzeugte Instanz inner bezeichnen.

Die Implementierung von lokalen Klassen konnte im JDK 1.1 ohne größere Änderungen der virtuellen Maschine vorgenommen werden. Lokale Klassen sind zwar zum Compilezeitpunkt bekannt, werden aber zur Laufzeit behandelt wie normale Klassen. Insbesondere wird vom Compiler zu jeder lokalen Klasse eine eigene .class-Datei erzeugt. Um Überschneidungen zu vermeiden, besteht ihr Name aus dem Namen der äußeren Klasse, gefolgt von einem Dollarzeichen und dem Namen der inneren Klasse. Bei den später behandelten anonymen Klassen wird statt des Namens der inneren Klasse eine vom Compiler vergebene fortlaufende Nummer verwendet. Beim Übersetzen des vorigen Beispiels würden also die Klassendateien Outer.class, Outer$Inner.class und Listing1001.class generiert.

 Hinweis 

Klassen in Methoden

Innere Klassen können nicht nur auf der äußersten Ebene einer anderen Klasse definiert werden, sondern auch innerhalb ihrer Methoden und sogar innerhalb eines beliebigen Blocks. In diesem Fall können sie auch auf die lokalen Variablen der umgebenden Methode oder des umgebenden Blocks zugreifen. Bedingung ist allerdings, daß diese mit Hilfe des Schlüsselworts final als konstant deklariert wurden.

Diese Art, lokale Klassen zu definieren, ist nicht sehr gebräuchlich, taucht aber mitunter in fremdem Programmcode auf. In der Praxis werden an ihrer Stelle meist anonyme Klassen verwendet, wie sie im folgenden Abschnitt besprochen werden. Wir wollen uns dennoch ein einfaches Beispiel ansehen:

001 /* Listing1002.java */
002 
003 class Outer2
004 {
005   public void print()
006   {
007     final int value = 10;
008 
009     class Inner2
010     {
011       public void print()
012       {
013         System.out.println("value = " + value);
014       }
015     }
016 
017     Inner2 inner = new Inner2();
018     inner.print();
019   }
020 }
021 
022 public class Listing1002
023 {
024   public static void main(String[] args)
025   {
026     Outer2 outer = new Outer2();
027     outer.print();
028   }
029 }
Listing1002.java
Listing 10.2: Definition einer lokalen Klasse in einer Methode

10.1.3 Anonyme Klassen

Die häufigste Anwendung lokaler Klassen innerhalb von Methoden besteht darin, diese anonym zu definieren. Dabei erhält die Klasse keinen eigenen Namen, sondern Definition und Instanzierung erfolgen in einer kombinierten Anweisung. Eine anonyme Klasse ist also eine Einwegklasse, die nur einmal instanziert werden kann. Anonyme Klassen werden normalerweise aus anderen Klassen abgeleitet oder erweitern existierende Interfaces. Ihre wichtigste Anwendung finden sie bei der Definition von Listenern für graphische Oberflächen, auf die wir in Kapitel 28 noch ausführlich eingehen werden.

Als einfaches Anwendungsbeispiel wollen wir das in Listing 9.13 definierte Interface DoubleMethod noch einmal verwenden und zeigen, wie man beim Aufruf von printTable eine anonyme Klasse erzeugt und als Argument weitergibt:

001 /* Listing1003.java */
002 
003 public class Listing1003
004 {
005   public static void printTable(DoubleMethod meth)
006   {
007     System.out.println("Wertetabelle " + meth.toString());
008     for (double x = 0.0; x <= 5.0; x += 1) {
009       System.out.println(" " + x + "->" + meth.compute(x));
010     }
011   }
012 
013   public static void main(String[] args)
014   {
015     printTable(
016       new DoubleMethod() 
017       {
018         public double compute(double value)
019         {
020           return Math.sqrt(value);
021         }
022       }
023     );
024   }
025 }
Listing1003.java
Listing 10.3: Anwendung anonymer Klassen

Statt eine vordefinierte Klasse zu instanzieren, wird hier in Zeile 016 eine lokale anonyme Klasse definiert und instanziert. Syntaktisch unterscheidet sie sich von der Instanzierung einer vordefinierten Klasse dadurch, daß nach dem new KlassenName(...) nicht ein Semikolon, sondern eine geschweifte Klammer steht. Anschließend folgt die Definition der Klasse. Wird als Klassenname ein Interface angegeben, implementiert die anonyme Klasse dieses Interface. Handelt es sich dagegen um den Namen einer Klasse, wird die anonyme Klasse daraus abgeleitet. In unserem Beispiel wird das Interface DoubleMethod implementiert.

Das Programm hat durch die Verwendung der anonymen Klasse nicht unbedingt an Übersichtlichkeit gewonnen. Tatsächlich sind sowohl Nutzen als auch Syntax anonymer Klassen Gegenstand vieler Diskussionen gewesen. Der große Vorteil anonymer Klassen besteht in ihrer Flexibilität. Eine anonyme Klasse kann da deklariert werden, wo sie gebraucht wird (hier beispielsweise beim Aufruf von printTable). Zudem kann sie Code weitergeben, der auf lokale Variablen und Membervariablen ihrer unmittelbaren Umgebung zugreift.

Ihre Anwendung sollte sich auf die Fälle beschränken, in denen eine Klasse mit wenigen Zeilen Code erzeugt werden muß, die nur an einer bestimmten Programmstelle bedeutsam ist. Ist die Klasse umfangreicher oder wird sie an verschiedenen Stellen benötigt, sollte eine benannte Klasse definiert und an den Aufrufstellen instanziert werden.

 Hinweis 

10.1.4 Statische lokale Klassen

Die letzte Variante innerer Klassen, die wir betrachten wollen, ist eigentlich gar keine. Hierbei wird eine Klasse innerhalb einer anderen Klasse definiert und mit dem Attribut static versehen. In diesem Fall erzeugt der Compiler Code, der genau dem einer gewöhnlichen globalen Klasse entspricht. Insbesondere ist eine statische lokale Klasse nicht nur innerhalb der Klasse sichtbar, in der sie definiert wurde, sondern kann auch von außen instanziert werden. Sie hält auch keinen Verweis auf die instanzierende Klasse und kann somit nicht auf deren Membervariablen zugreifen. Der einzige Unterschied zu einer globalen Klasse besteht darin, daß der Name der inneren Klasse als Präfix den Namen der äußeren Klasse enthält. Beide sind durch einen Punkt voneinander getrennt.

Eine Klasse könnte beispielsweise dann als statische lokale Klasse definiert werden, wenn ihre Daseinsberechtigung auf der Existenz der äußeren Klasse basiert. Typische Anwendungen sind kleinere Hilfsklassen, die ausreichend Substanz zur Deklaration einer eigenen Klasse, aber zu wenig für eine eigene Datei haben. Durch den separaten Namensraum können sie auch Allerweltsnamen wie "Entry", "Element" oder "Debug" haben.

Das folgende Listing zeigt eine einfache Anwendung lokaler statischer Klassen:

001 /* Listing1004.java */
002 
003 class Outer3
004 {
005   static class Inner3
006   {
007     public void print()
008     {
009       System.out.println("Inner3");
010     }
011   }
012 }
013 
014 public class Listing1004
015 {
016   public static void main(String[] args)
017   {
018     Outer3.Inner3 inner = new Outer3.Inner3();
019     inner.print();
020   }
021 }
Listing1004.java
Listing 10.4: Anwendung anonymer Klassen

Lokale und anonyme Klassen werden recht häufig eingesetzt. Auch in diesem Buch sind weitere Beispiele für ihre Anwendung zu finden. So wird beispielsweise in Abschnitt 28.2.2 erläutert, wie man sie zur Entwicklung von Ereignishandlern für GUI-Programme nutzen kann. Ein weiteres Beispiel für ihre Anwendung findet sich in Abschnitt 15.4, der die Implementierung eines Iterators (s.u.) erläutert.

 Hinweis 


 Titel   Inhalt   Suchen   Index   API  Go To Java 2, Zweite Auflage, Addison Wesley, Version 2.0
 <<    <     >    >>  © 2000 Guido Krüger, http://www.gkrueger.com