Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 3. Auflage |
<< | < | > | >> | API | Kapitel 10 - OOP IV: Verschiedenes |
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.
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 |
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 name 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 030 erzeugte Instanz outer 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. |
|
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 |
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 |
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. |
|
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 statischer lokaler 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 |
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. |
|
Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 3. Auflage, Addison Wesley, Version 3.0.1 |
<< | < | > | >> | API | © 1998-2003 Guido Krüger, http://www.javabuch.de |