![]() |
|
|||||
Sematische Einheit: äußere Klassen zu statischen inneren Nested-Top-Level-Klassen machen dann Sinn, wenn diese nur innerhalb der äußeren Klasse oder nur in Verbindung mit der äußeren Klasse eingesetzt werden sollen. Obwohl möglich, ist deshalb der Import einer Nested-Top-Level-Klasse ohne ihre äußere Klasse nicht gerade sinnvoll. Stehen die statischen Klassen bzw. Interfaces von außen im Zugriff, verhalten sie sich wie andere Top-Level-Klassen bzw. Interfaces. Allerdings haben sie aufgrund ihrer Member-Eigenschaft und des Namens-Raums eine besonders enge Bindung an ihre äußere Klasse. Deklaration von statischen inneren Klassen/Interfaces Die Deklaration erfolgt wie bei normalen statischen Membern: class OuterClass { //... [icModifiers] static class StaticInnerClass [icModifiers] [static] interface InnerInterface Als icModifiers sind abstract, final sowie die Zugriffs-Modifikatoren erlaubt. Innerhalb der äußeren Klasse ist Anlage und Zugriff auf innere Klassen wie auf andere Top-Level-Klassen möglich. erweiterte Syntax für Zugriffe auf nested top-level Klassen Die Anlage und der Zugriff von außen auf Instanzen von nicht private erklärten statischen inneren Klassen, erfolgt mit einer erweiterten Punkt-Notation: sic= new OuterClass.StaticInnerClass(); // Anlage OuterClass.StaticInnerClass[.member] // Zugriff
Insbesondere gibt es also keine besonderen wechselseitigen Zugriffsmöglichkeiten auf Instanz-Variablen. Für Nested-Top-Level-Klassen bzw. Interfaces gelten folgende zusätzliche Regeln: Beziehungen der statischen inneren Klasse zur äußeren
BeispieleInneres Interface und statische Klasse class Outer implements InnerI { // 4. Regel private static int si= 0; private static char c= '1'; // InnerI implizit static, nur innerhalb von Outer im Zugriff private interface InnerI { void f1(); } static class InnerC implements InnerI { // 4. Regel private static int si= Outer.si+2; // 2. Regel public static float f= 1.23f; public void f1() { System.out.println(si+" "+c); } } public void f1() { System.out.println(InnerC.si); // 2. Regel } } Von außen können Objekte innerer Klassen mit der oben vorgestellten Syntax manipuliert werden: Outer o= new Outer(); // erzeugt kein InnerC-Objekt! Instanz-Anlage einer statischen inneren Klasse // Anlage einer Instanz einer Nested-Top-Level-Klasse InnerC Outer.InnerC oi= new Outer.InnerC(); System.out.println(Outer.InnerC.f); // :: 1.23 o.f1(); // :: 2 oi.f1(); // :: 2 1 Schachtelung von inneren statischen Klassen/Interfaces Im nächsten Beispiel werden die Modifikatoren abstract und final mit einbezogen: // Vermeidung von Namenskollisionen durch Punkt-Notation // Vermeidung in umgekehrter Richtung Namensregel gilt für geschachtelte innere Klassen // weiteres Interface mit gleichem Namen wie oben // weitere innere Klasse mit unterschiedlichem Namen // nicht abstract, da Implementation von f1() und f2() Die zwei Interfaces stehen zwar auf verschiedenen Stufen, sind aber nicht ineinander geschachtelt und können somit den gleichen Namen haben. Zweideutigkeiten müssen mit Hilfe der Punkt-Notation beseitigt werden. Fehlerhafte Vererbungsrichtung Umgekehrt kann nach der dritten Regel keine äußere Klasse Outer Subklasse einer statischen inneren Klasse Inner sein: // C-Fehler: zyklische Vererbung class Outer extends Inner { static class Inner { ... } } 8.2.1 Design-Beispiel: 2D-Klassen
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 1. | Eine Instanz einer Member-Klasse ist mit genau einer vorher erschaffenen Instanz der äußeren Klasse verbunden (siehe Abb. 8.3). |
| 2. | Member-Klassen können bis auf Konstanten (static final erklärte Felder) keine statischen Felder oder Methoden besitzen. |
| 3. | Member-Klassen haben Zugriff auf alle Felder und Methoden der äußeren Klasse, inklusive der private deklarierten. |
| 4. | Member-Klassen können ineinander geschachtelt werden. |
| 5. | Member-Klassen können Subklassen von äußeren Klassen sein. |
|
| In einer äußeren Instanz können beliebig viele Instanzen einer Member-Klasse »enthalten« sein (siehe Abb. 8.3). |
Dies unterscheidet eine Member-Klasse von einer normalen Feldreferenz, die auf höchstens ein Objekt verweist.
Member-Klassen:
Schachtelung und Subtyp-Beziehung
Die vierte und fünfte Regel sollte nicht benötigt werden. Solche Klassen-Konstrukte führen nur zu schwer durchschaubaren Inklusions-Beziehungen (siehe auch erster Fall in 8.4.2).3
Der Zugriff auf den Typ oder eine Konstante einer Member-Klasse erfolgt wie bei Feldern mit Hilfe der erweiterten Punkt-Notation.
Es wird zuerst der Unterschied zwischen Member-Klassen und statischen inneren Klassen verdeutlicht.
Vergleich:
statische innere Klasse und Member-Klasse
class Outer { static class StaticC {} class MemberC { MemberC() { System.out.println("MemberC");} }
// zur statischen Methode gibt es keine Outer-Instanz
static void test() { StaticC sc= new StaticC(); //ok, da statisch // MemberC mc= new MemberC(); C-Fehler ¨ } // innerhalb einer Instanz-Methode Anlage möglich
void f() { new MemberC(); } ¦ }
public class Test { public static void main(String[] args) { Outer.StaticC sc= new Outer.StaticC(); // ok, da statisch // Outer.MemberC mc= new Outer.MemberC(); C-Fehler Æ
new Outer().f(); // :: MemberC Ø } }
zu ¨ und Æ: C-Fehler aufgrund der ersten Regel, denn es existiert keine Instanz der äußeren Klasse.
zu ¦ und Ø: In Instanz-Methoden erzeugte Member-Objekte werden automatisch mit der zugehörigen Instanz der äußeren Klasse verbunden.
Für eine flexible Anlage von Objekten einer Member-Klasse und Eindeutigkeit bei Zugriffen ist wieder eine Syntaxerweiterung notwendig:
Syntax zur Anlage einer Member-Klassen-Instanz
| Die Anlage einer Instanz einer inneren Klasse, verbunden mit einer Instanz der (direkt übergeordneten) äußeren Klasse, erfolgt durch |
outerClassInstance.new MemberClassConstructor()
Ist die Instanz der äußeren Klasse this, so kann – wie im letzten Beispiel bei der Methode f() zu sehen – anstatt this.new nur new geschrieben werden.
Syntax für Zugriff auf Member der äußeren Klasse
| Der Zugriff auf Felder bzw. Methoden einer äußeren Klasse erfolgt bei Zweideutigkeit aus einer Member-Klasse mit: |
OuterClass.this.field bzw. OuterClass.this.method()
Eindeutigkeit bei geschachtelten Member-Klassen
Auch bei geschachtelten inneren Klassen ist der Zugriff von Member-Klassen auf Felder bzw. Methoden aller äußeren Klassen aufgrund der Namens-Regel in 8.1 immer eindeutig.
Ein Zugriff von einem Member auf die Instanz der äußeren Klasse ist immer möglich. Die Umkehrung gilt nicht.
class Outer { private int i= 1; // Zugriff auf Member-Objekte nur durch explizite Referenz
MemberC m;
class MemberC { public static final int CONST= 10; // 2. Regel 8.4 private int i; // dieser Konstruktor setzt explizit eine Member-Referenz
MemberC() { m= this; }
Zugriff auf Felder der äußeren Klasse
// dieser Konstruktor setzt keine Referenz auf Member
// impliziter Zugriff vom Member auf äußeres Objekt
MemberC(int i) { this.i= Outer.this.i + neg() + i; // 3. Regel 8.4 }
int geti() { return i; } }
private int neg() { return -i; } void showMember() { System.out.println(m); } }
Im Testcode werden zwei Member-Instanzen angelegt:
Outer o= new Outer(); Outer.MemberC mo= o.new MemberC(1); // erweiterte Syntax System.out.println(Outer.MemberC.CONST); // :: 10 o.showMember(); // :: null o.new MemberC(); o.showMember(); // :: p08.Outer$MemberC@7007db1 System.out.println(o.m.geti());// :: 0 System.out.println(mo.geti()); // :: 1
Erklärung: Die erste Member-Instanz mo hat zwar Zugriff auf das äußere Objekt o, aber nicht umgekehrt. Erst die zweite Member-Instanz wird mit Hilfe des Konstruktors explizit im Objekt o durch m referenziert.
this$0:
unsichtbare Referenz auf die äußere Klasse
Hinter dem impliziten Registrier-Mechanismus des externen Objekts steht eine unsichtbare Referenz this$0 im Member-Objekt (siehe Abb. 8.4), die bei der Anlage des Member-Objekts gesetzt wird.
|
Eine Member-Registrierung ist dagegen implizit schlecht möglich, da die Anzahl der Member-Objekte beliebig groß ist.
Tiefe Schachtelung von
Member-Klassen 
Zur Abschreckung wird die vierte Regel aus 8.4 anhand der drei ineinander geschachtelten Klassen Outer, Member1 und Member2 demonstriert. In Member2 werden die gleich lautenden int-Felder i der drei Klassen summiert:
class Outer { private int i; public class Member1 { private int i; public class Member2 { // 4. Regel 8.4 private int i; Member2(int i1, int i2, int i3) { Outer.this.i= i1; Member1.this.i= i2; i= i3; } int sum() { return Outer.this.i + Member1.this.i + i;} } }
// f() demonstriert verschiedene Anlagen von Member-Instanzen
void f() { Outer.Member1 m1= new Member1(); Outer.Member1.Member2 m2= m1.new Member2(10,20,30); System.out.println( this.new Member1().new Member2(1,2,3).sum()); // :: 6 ¨ System.out.println(m2.sum()); // :: 51 } }
Die Ausgabe in der Methode f() wird z.B. durch folgenden Testcode erzeugt:
new Outer().f();
Erklärung: Das zweite Ergebnis 51 kommt daher, dass es nur eine Outer-Instanz gibt, die durch die Zeile ¨ auf 1 gesetzt wird (siehe Abb. 8.5).
|
Fazit: Vermeide Schachtelungen von Member-Klassen!
Wie bereits bei statischen inneren Klassen gezeigt, kann eine äußere Klasse nicht von einer Member-Klasse abgeleitet werden:
Fehlerhafte Vererbungsrichtung
// C-Fehler: zyklische Vererbung
class Outer extends MemberC { class MemberC { } }
Dagegen sind drei weitere Fälle nicht ausgeschlossen, von denen allerdings nur der dritte Fall wichtig ist. Die ersten beiden werden nur kurz angeführt, aber nicht näher besprochen, da eine sinnvolle Anwendung fehlt.
Member-Klasse
ist Subklasse der äußeren
class Outer { //... class PathologicalMember extends Outer { /*...*/ } //... }
Die 5. Regel (siehe 8.4) und die entsprechende Anmerkung bzw. Fußnote sind Grund genug, dieses Konstrukt zu meiden.
Hier wird man mit dem Problem konfrontiert, dass die Member-Klasse unbedingt eine Instanz einer äußeren Klasse zur Anlage benötigt. Andererseits wird mit Anlage einer Subklasse von Member automatisch ein Objekt der Member0-Klasse erzeugt. Die Folgerung:
Es sind nur Konstruktoren der Subklasse erlaubt, die einen Parameter o der äußeren Klasse Outer haben. Als erste Anweisung müssen sie o.super() enthalten – wieder eine Syntaxerweiterung –, womit o das äußere Objekt der Member-Superklasse wird.4
class Outer { class MemberC {} }
class Pathological extends Outer.MemberC { // Pathological() {} ergibt einen C-Fehler
Pathological(Outer o) { // ok! o.super(); // notwendig! //... } }
Zugriff von Member-Klassen auf Superklassen der äußeren Klasse
Dieser Fall kann »unverschuldet« auftreten, will sagen, er kann nicht unbedingt vermieden werden (siehe Abb. 8.6).
|
Das Problem besteht darin, an verdeckte oder überschriebene Methoden der Superklassen der äußeren Klasse Outer heranzukommen.
Outer.super:
Member-Zugriff auf Superklassen von Outer
| Die Syntaxerweiterung Outer.super gestattet den Zugriff auf eine Superklasse von Outer. |
class B1 { protected int i= 1; protected float f= 1.23f; } class B2 extends B1 { protected int i= 2; }
class Outer extends B2 { private int i= 3; // verdeckt i in Superklasse private float f= 3.21f; // verdeckt f in Superklasse
class MemberC { void foo() { System.out.println(Outer.this.i +" "+Outer.this.f); // :: 3 3.21 System.out.println(Outer.super.i+" "+Outer.super.f); // :: 2 1.23 } } }
Die Ausgabe wird z.B. erzeugt durch die Anweisung:
new Outer().new MemberC().foo();
Member-Klassen konkurrieren mit Instanz-Variablen, da sie Ähnlichkeiten aufweisen. Deshalb ist eine klare Abgrenzung wichtig, die einen Hinweis auf den unterschiedlichen Einsatz gibt.
Feld: gerichtete Aggregation, Zugriff über Feld-Schnittstelle
| Instanz-Variable: Sie implementiert eine gerichtete Aggregation, wobei das äußere Objekt nur Zugriff auf das enthaltene Objekt über dessen öffentliche Schnittstelle hat. Das enthaltene Objekt hat dabei keinen Zugriff auf das äußere Objekt.5 |
Member-Klasse:
gerichtete Aggregation, voller Zugriff auf Outer
| Member-Klasse: Sie implementiert eine gerichtete Aggregation oder Komposition, wobei das enthaltene Objekt vollen Zugriff auf das äußere Objekt hat (siehe auch Abb. 8.4). Das äußere Objekt hat implizit keinen Zugriff auf das enthaltenen Objekt.6 |
Aggregation mit Member-Klassen
In Abschnitt 4.2.5 wurde bereits auf eine Komposition mit Hilfe innerer Klassen verwiesen.
Der Einsatz von Member-Klassen setzt aber voraus, das ein voller Zugriff des aggregierten Objekts auf das Aggregat-Objekt sinnvoll ist.
Da ein Aggregat Zugriff auf seine enthaltenen Objekte haben sollte, sind explizite Referenzen für eine bidirektionale Navigation erforderlich.
Aggregation bzw. Komposition:
Member-Klasse vs. Feld
Vergleich von Komposition mit Hilfe einer inneren Klasse und Aggregation mit Hilfe eines Felds (Referenz auf eine Top-Level-Klasse):
class TopLevelC { private int num; public int numGets() { return num++; } }
class AggregateC { private TopLevelC t1,t2; private MemberC m;
private class MemberC { private String s; }
public void setTopLevelC(TopLevelC t) { if (t1==null) t1=t; else t2=t; } public void createMember(String s) { (m= new MemberC()).s= s; }
public void printAggregate() { // Kontrakt: keine null-Felder System.out.println(t1.numGets()+" "+t2.numGets()+" "+m.s); } }
Im Code-Fragment werden zwei Kompositionen und eine Aggregation erzeugt:
AggregateC a= new AggregateC(); TopLevelC t= new TopLevelC(); a.setTopLevelC(t); // Aggregation! ¨ a.setTopLevelC(new TopLevelC()); // Komposition! ‚ a.createMember("member"); // Komposition! ƒ
a.printAggregate(); // :: 0 0 member System.out.println(t.numGets()); // :: 1 a.printAggregate(); // :: 2 1 member
Erklärung: Mit dem expliziten Setzen einer Komponente t in Zeile ¨ kommt nur eine Aggregation zu Stande, da t von beliebigen anderen Objekten außerhalb des Aggregats manipuliert werden kann.
Zeile ‚ hat dagegen eine Client-Konvention eingehalten:
Komposition durch Client-
Konvention
| Komposition auf Basis von Feldern wird durch Clients dadurch erzeugt, dass keine Referenzen auf die übergebenen Komponenten gehalten werden. Es existierten nur Referenzen im Aggregat. |
Private Members: Komposition per Design
Wird eine Member-Klasse private deklariert und keine Referenz nach außen geliefert, so erhält man dagegen automatisch eine Komposition. Somit erzeugt die Zeile ƒ eine Komposition.
Lokale Klassen werden wie lokale Variablen in einem Block deklariert und sind dann nur in diesem Block gültig.
Blöcke für lokale Klassen sind in der Regel Methoden, selten Instanzierer oder statische Initialisierer. Der Einfachheit halber formulieren wir die Aussagen nur für Methoden.
Lokale Klassen:
Hybride aus lokaler Variable und Klasse
Je nachdem, ob eine lokale Klasse in einer statischen oder Instanz-Methode deklariert ist, verhält sie sich wie eine lokale Variable ohne Bindung an eine Instanz oder aber wie eine Hybride aus lokaler Variable und Member-Klasse (siehe auch Abb. 8.1).
Restriktionen für lokale Klassen

Die Regeln für lokale Klassen setzen sich aus denen für lokale Variablen und denen innerer Klassen zusammen:
| 1. | Eine lokale Klasse ist nur in dem Block gültig, in dem sie deklariert ist und kann weder private, protected, public noch static sein. |
| 2. | Lokale Klassen haben Zugriff auf final deklarierte lokale Variablen oder Parameter, die in ihrem Gültigkeitsbereich liegen. |
| 3. | Lokale Klassen können bis auf Konstanten (static final) keine statischen Felder oder Methoden besitzen. |
| 4. | Ist eine lokale Klasse in einer |
| statischen Methode deklariert, hat sie Zugriff auf alle statischen Felder und Methoden der Klasse. |
| Instanz-Methode deklariert, gelten zusätzlich die fünf Regeln zu Member-Klassen in 8.4. |
Lokale Klassen sind einfach zu benutzen
Lokale Klassen sind einfacher zu verwenden als Member-Klassen, da sie außerhalb des Blocks und damit auch außerhalb der Klasse nicht direkt benutzbar sind. Dadurch entfallen viele pathologische Nebeneffekte der Member-Klassen und auch die Zugriffsnotation (8.4.1) ist überflüssig.
Einsatzgebiet von lokalen Klassen
Das Einsatzgebiet von lokalen Klassen ist weit gesteckt:
| Lokale Klassen sind ideal für Instanzen geeignet, die nur innerhalb einer Methode benötigt werden. |
| Instanzen von lokalen Klassen können »undercover« außerhalb der Methode, in der die lokale Klasse deklariert ist, verwendet werden (siehe hierzu 8.5.1). |
Es werden die beiden Varianten für lokale Klassen anhand einer Klasse Outer demonstriert. Zuerst die Anlage einer lokalen Klasse in statischer Methode:
Lokale Klassen in einer statischen Methode
class Outer { // Zugriff von Local über Outer.s
private static String s="O";
static void methodWithLocalClass (final String CS,int k) {
// Local hat keinen direkten Zugriff auf k, deshalb
final int CI= k; ¨
class Local { String s= "L"; void f(String s, int i) { // die Ausgabe ergibt sich durch Zeile ¦ System.out.println(Outer.s +this.s+s+i); // :: OLf1
// Ausgabe z.B. durch Outer.methodWithLocalClass("m",2); System.out.println(CS+CI); // :: m2 } } new Local().f("f",1); Æ } }
Erklärung zu ¨: Um auf nicht final erklärte Parameter der Methode zugreifen zu können, in der die lokale Klasse enthalten ist, muss man den Umweg über eine zusätzlich final deklarierte lokale Variable gehen.
Lokale Klasse in einer Instanz-Methode
Es folgt die fast analoge Instanz-Variante einer lokalen Klasse:
class Outer {
// Local hat Zugriff auf Instanz-Feld über Outer.this.s
private String s="O";
void methodWithLocalClass (final String CS, int k) { final int CI= k;
class Local { String s= "L"; void f(String s, int i) { System.out.println(Outer.this.s+ this.s+s+i); // :: OLf1 System.out.println(CS+CI); // :: m2 } } new Local().f("f",1); } }
Eine zugehörige Testanweisung ist dann z.B.:
new Outer().methodWithLocalClass("m",2);
Alle lokalen Variablen werden außerhalb des Blocks, in dem sie deklariert sind, ungültig, d.h. stehen nicht mehr im Zugriff.
Mit dem Verlust der Gültigkeit (scope) verlieren lokale primitive Variablen auch ihre Existenz. Im Allgemeinen stimmen also Gültigkeit und Lebensdauer überein.
Instanzen lokaler Klassen als Resul-tat der Methode
Dies gilt aber nicht unbedingt für Instanzen von lokalen Klassen, denn sie können außerhalb des Blocks als Objekte eines anderen Typs »überleben«.
| In diesem Fall ist also die Lebensdauer von lokalen Objekten größer als ihre Gültigkeit. |
In der folgenden Klasse Outer liefert die Methode stillAlive() ein lokales Objekt l als Typ B nach außen. Die lokale Klasse Local ist dabei Subklasse von B.
Die Klasse bzw. der Typ Local ist außerhalb der Methode ungültig, aber die Instanz l überlebt außerhalb als Typ B.
Lokale Klassen-Instanz überlebt als Resultat eines anderen Typs
abstract class B { abstract String f(); }
class Outer { B stillAlive (final String s) { final int i= 10;
class Local extends B { String f() { return s+" "+i; } }
Local l= new Local();
// diese lokale Instanz von Local muss durch Übergabe
// an die Referenz B weiterhin existent bleiben.
return l; } }
B b= new Outer().stillAlive("Wert"); System.out.println(b.f()); // :: Wert 10
Kopien lokaler Variablen in Instanzen lokaler Klassen
Zusammen mit einer lokalen Instanz, die nach außen geliefert wird, müssen eventuell lokale Variablen bzw. Parameter ebenfalls als Kopie überleben.
Dies ist der Grund, warum mit Hilfe von final (2. Regel in 8.5) sichergestellt werden muss, dass sich nach dem Kopiervorgang nichts mehr ändert.
Denn ansonsten müßte man Original und Kopie der Variablen bzw. Parameter im Wert immer wieder abgleichen, eine Alternative, die die Java-Sprach-Designer nicht unbedingt attraktiv fanden.
Anonyme Klassen sind lokale Klassen ohne Namen. Als so genannte One-Shot-Klassen ist ihre Definition unmittelbar mit der Anlage einer einzigen Instanz verbunden.
Einen idealen Einsatz finden sie bei der Implementierung von Interfaces mit nur einer, höchstens zwei Methoden.
Sie unterliegen denselben Regeln wie lokale Klassen, sind aber aufgrund ihrer Namenslosigkeit weiter eingeschränkt:
| Die Deklaration einer anonymen Klasse und die Anlage der einzigen zugehörigen Instanz sind untrennbar miteinander verbunden. |
| Die Deklaration ist keine Anweisung, sondern ein Ausdruck. |
| Die Deklaration erlaubt kein extends oder implements und erfolgt alternativ durch: |
| new SuperClassName ([arguments]) { ... } wobei die Superklasse einen Konstruktor haben muss, der eine zu den Argumenten passende Signatur hat. |
| new InterfaceName() { ... } wobei die anonyme Klasse das Interface vollständig implementieren und direkte Subklasse von Object sein muss. Konstruktor-Argumente sind also nicht erlaubt. |
Zusätzlich zu den Regel für lokale Klassen gelten folgende Regeln:
Restriktionen für anonyme Klassen
| 1. | Eine anonyme Klasse kann keine eigenen Konstruktoren definieren, sondern muss – sofern notwendig – einen Initialisierer verwenden. |
| 2. | Zusätzliche Methoden, die nicht in der Superklasse bzw. im Interface deklariert sind, können nicht von außen aufgerufen werden.7 |
Zu einer abstrakten Klasse Object3D soll eine Zylinder- bzw. Würfel-Instanz als anonyme Klasse erschaffen werden, deren Methode volume() anschließend in test() ausgegeben wird.
Zuerst die Definition der abstrakten Klasse:
abstract class Object3D { protected Object spec;
public Object3D (Object spec) { this.spec= spec; }
public abstract double volume(); // zu implementieren
public static void test(Object3D o3D) { System.out.println(o3D.volume()); } }
In den beiden anonymen Klassen innerhalb von Test wird die abstrakte Methode volume() implementiert:
public class Test { public static void main(String[] args) {
Anonymer Klassen-Ausdruck als Subklasse
// Anlage und Test eines anonymen Zylinders
// Zuweisung zu einer Referenz
Object3D zylinder= // Konstruktor benötigt ein Object-Argument new Object3D( new double[] {2.,1./Math.PI} ) // Start der Definition
{ public double volume() { double r= ((double[])spec)[0]; double h= ((double[])spec)[1]; return Math.PI*r*r*h; } }; // ein Ausdruck ist mit Semikolon abzuschließen
Object3D.test(zylinder); // :: 4.0
Anonymer Klassen-Ausdruck als Argument eines Methodenaufrufs
// Anlage und Test eines anonymen Würfels
// Verwendung direkt als Argument
Object3D.test( // :: 6.0 new Object3D( new double[] {1.,2.,3.}) { public double volume() { return ((double[])spec)[0]*((double[])spec)[1]* ((double[])spec)[2]; } });
} }
Erklärung: Der Konstruktor Object3D() mit einem Object-Parameter ist zwar unschön, aber für eine generische Verwendung (sub)optimal. Denn anonyme Klassen können keine zusätzlichen Methoden deklarieren, die von außen auf einfache Art benutzt werden könnten.
Um aber unterschiedliche 3D-Körper definieren zu können, ist der generische Parameter Object ein (unschöner) Kompromiss.
Fazit: Das Beispiel zeigt deutlich, dass anonyme Klassen wie Ausdrücke verwendet werden und deshalb einigermaßen überschaubar bleiben müssen.
Deklariert man statt einer abstrakten Klasse ein Interface IObject3D, muss der Konstruktor durch eine normale Methode set() ersetzt werden:
interface IObject3D { void set(Object spec); double volume(); }
Die anonyme Klasse muss nun zwei Methoden implementieren, und ihre Instanz muss ohne Konstruktor erst einer Referenz zugewiesen werden, über die dann Werte gesetzt werden und die Ausgabe erzeugt wird.
Anonyme Klasse als Implementation eines Interfaces
public class Test { public static void main(String[] args) { IObject3D zyl= new IObject3D() { private double r=1, h=1;
// dieses set() ist nicht im Interface definiert. Kann
// normalerweise nur intern benutzt werden (siehe 8.6.1)
public void set (double r, double h) { this.r= r; this.h=h; }
public void set (Object spec) { set(((double[])spec)[0],((double[])spec)[1]); }
public double volume() { return Math.PI*r*r*h; } };
// zyl.set(2.,1./Math.PI); C-Fehler, leider nicht möglich!
zyl.set(new double[] {2.,1./Math.PI}); System.out.println(zyl.volume()); // :: 4.0 } }
| Die zugehörige Superklasse bzw. das zugehörige Interface hat nur wenige abstrakte Methoden, deren Implementierung kurz ist. |
| Es wird nur eine Instanz benötigt, wobei der Name der Klasse völlig unwichtig ist. |
| Diese Instanz hat dieselbe Schnittstelle wie die Superklasse bzw. das Interface, benötigt also keine zusätzlichen Methoden. |
Die letzte Aussage basiert auf dem Fakt, dass anonyme Instanzen nach außen immer nur als Objekte der Superklasse bzw. des Interfaces angesehen werden, was eine schöne Überleitung zum nächsten Abschnitt ist.
Wie bereits in 6.6.2 angemerkt, sind Superklassen und Interfaces keine Firewalls.
Kennt man den wahren Typ des Objekts, welches sich hinter dem Interface bzw. der Superklasse verbirgt, kann man auf seine speziellen Eigenschaften bzw. Methoden zugreifen, ob der Class-Provider dies nun will oder nicht.
Nun könnte man sich zumindest für anonyme Klassen auf die zweite Regel in 8.6 verlassen, die besagt, dass zusätzliche Methoden, die nicht in der Superklasse oder im Interface definiert sind, vor äußerem Zugriff geschützt sind.
Probleme beim Export von inneren Klassen als Interface
Diese Regel eignet sich somit vorzüglich, um Klassen als Interface-Objekte aus der äußeren Klasse zu exportieren, ohne dass der Client die Klasse selbst benutzen kann (siehe hierzu 8.7).
Handeln wir nach dem Grundsatz »Vertrauen ist gut, Kontrolle ist besser«:
class Outer { // nutzloses Marker-Interface
private interface IUseless {}
public static IUseless getLocal() { // anonyme Klasse zum Interface
return new IUseless() { String unuseable() { return "ups"; } }; } }
Die anonyme Klasse bzw. Instanz, die Outer.getLocal() exportiert, ist nutzlos, da die Methode unuseable() nach der zweiten Regel in 8.6 nicht benutzt werden kann, da sie im Interface IUseless nicht erklärt ist.
Diese Auffassung suggeriert auch der Compiler, der die folgende Anweisung mit einem Fehler quittiert:
System.out.println(Outer.getLocal().unusable()); // C-Fehler
Reflexions-Mechanismus zum
Aufruf von Methoden innerer Klassen
Diese Hürde kann aber locker genommen werden. Eine kleine Reflexions-Variante (siehe Kapitel 13) ergibt dann:
public class Test { public static void main(String[] args) {
// der nachfolgende Code führt zu dem Aufruf von
// System.out.println(Outer.getLocal().unusable());
Object o= Outer.getLocal(); Method[] useable= o.getClass().getDeclaredMethods(); try { System.out.println(useable[0].invoke(o,null)); // :: ups } catch(Exception e) {} } }
Im nächsten Beispiel reagieren zwei bekannte Entwicklungsumgebungen JBuilder 3 bzw. 4 und Forte8 auf einen einfachen Testcode recht unterschiedlich.
Anonyme Klassen können zu Compiler-bzw. Laufzeit-Problemen führen
JBuilder 3 reagiert bei fehlerhaftem Code nicht immer mit einem Compiler-Fehler, sondern überlässt dies der JVM, die sich mit einer kryptischen VerifyErrorException verabschiedet. Der Forte-Compiler ist dagegen sehr konservativ und lehnt dafür auch korrekten Code ab. JBuilder 4 liefert dagegen zutreffende Ergebnisse.
Auf die Variable i in der folgenden Klasse Fubar sollte eine anonyme Klasse zugreifen können, wenn i nicht private deklariert ist.9
abstract class Fubar { private int i= 1; // 1. Version mit, 2. Version ohne private abstract int f();
static void test() { // Anlage einer anonymen Klasse
Fubar fb= new Fubar() { int f() { return i; } }; } }
Der Testcode besteht nur aus dem Aufruf:
Fubar.test();
Ausführung der 1. Version (mit private):
Vorsicht: Irreführende Ergebnisse/Meldungen bei Entwicklungsumgebungen
JBuilder 3: Exception in thread "main" java.lang.VerifyError...
Accessing value from uninitialized register 0
Forte: Compiler-Fehler Invalid use of "this" selector syntax...
Ausführung der 2. Version (ohne private):
JBuilder 3 und 4: Kein Fehler!
Forte: Compiler-Fehler Invalid use of "this" selector syntax...
Innere Klassen als Quasi-Objekte von Interfaces
Besonders interessant ist der Einsatz von inneren Klassen zur Umsetzung des Template-Prinzips beim Interface-Pattern (siehe 6.6), um konkrete Klassen-Implementierungen zu schützen.
|

Äußere Klasse als Factory und Firewall
| Die äußere Klasse spielt dabei die Rolle einer Factory-Klasse und eines Firewalls, indem sie Instanzen von inneren Klassen nur als Interface exportiert (siehe Abb. 8.7).10 |
Die Firewall muss verhindern, dass die exportierten Instanzen nicht anders als im Interface definiert verwendet werden.
Exportieren von statischen inneren Klassen als Interface-Objekte
Die folgende Klasse FirewallFactory liefert aufgrund eines Selektors drei verschiedene Implementierungen für den IService aus:
class FirewallFactory { public interface IService { // alternativ top-level void anyService(); } // dieses private schützt vor unerlaubtem Cast
private static class ProtectedStaticCService implements IService { public void anyService() { internalMethod(); }
// dieses private schützt vor Aufruf mittels Reflection
private void internalMethod() { System.out.println("static: internalMethod()"); } }
static public IService createIService(int selector) { class ProtectedLocalCService implements IService { public void anyService() { internalMethod(); } private void internalMethod() { System.out.println("static local: internalMethod()"); } } switch (selector) { case 0: return new IService() { public void anyService() { internalMethod(); } private void internalMethod() { System.out.println( "static anonym: internalMethod()"); } }; case 1: return new ProtectedStaticCService(); case 2: return new ProtectedLocalCService(); default: return null; } } }
Erklärung: Da die Klasse FirewallFactory nur zur Erzeugung des Services IService dient, kommen eigentlich nur statische innere Klassen in Frage.
Sollte die Klasse keine reine Factory darstellen, sondern Teil des Services sein, sind auch nicht statische Varianten sinnvoll.
Firewall:
Cast und Aufruf über Reflexion werden verhindert
In diesem »konstruierten« Fall werden die drei Möglichkeiten statische innere, lokale oder anonyme Klasse implementiert und wahlweise als Quasi-Objekt IService dem Client übergeben.
Der Client kann zwar weiterhin die wahre Klasse hinter IService mittels Reflection ermitteln (unsichtbar werden sie also nicht!), aber ein Cast zu dieser Klasse bzw. ein Aufruf der internen Methoden ist aufgrund der private-Modifikatoren ausgeschlossen.
Nachfolgend ein kurzer Test, bei dem der Client nicht nur die normale IService-Methode anyService() aufrufen will, sondern mittels Type-Cast das Objekt direkt benutzen oder per Reflection die (jeweils zweite) interne Methode internalMethod() ausführen will.
Dies führt dann aber entweder zu einem Compiler-Fehler oder zu einer IllegalAccessException, wie nachfolgend demonstriert.
public class Test { public static void main(String[] args) { /* Versuch eines Casts führt zu Compiler-Fehler
FirewallFactory.ProtectedStaticCService pss= (FirewallFactory.ProtectedStaticCService) FirewallFactory.createIService(1); */
for (int i=0; i<3; i++) { FirewallFactory.IService ffs= FirewallFactory.createIService(i); ffs.anyService(); // nähere Einzelheiten zu Reflection siehe 13. Kapitel
Method[] m= ffs.getClass().getDeclaredMethods(); try { // Aufruf von internalMethod() führt zu einer Ausnahme
m[1].invoke(ffs,null); } catch(Exception e) { System.out.println(e); } } } }
Generisches Verhalten durch Plug-in-Funktionen/Methoden
Jeder C++-Programmierer kennt Funktions-Zeiger (Pointer) als Parameter, um das Verhalten einer Funktion so generisch wie möglich zu gestalten. Das Verhalten ist dann von dem Funktions-Argument (plug-in function) abhängig, das zur Laufzeit »eingehängt« wird.
Da Java keine globalen Methoden – losgelöst von Objekten – kennt, ist eine direkte Nachbildung nicht möglich. Jedoch gibt es besonders mit Hilfe der anonymen Klassen ein Äquivalent, welches typsicher und elegant ist.
Interface mit Plug-in-Methode(n)
Eine Methode pugInMethod() wird in ein Objekt »eingehüllt«, welches von einer abstrakten Klasse oder – besser noch – von einem Interface IBehavior abgeleitet wird, die/das diese Methode definiert (Abb. 8.8).
|
Die Methode pluggable() einer Klasse PluggableBehavior kann nun dynamisch ihr Verhalten mit Hilfe von pugInMethod()anpassen, dadurch dass sie einen generischen IBehavior-Parameter enthält.
Nun können einzelne oder alle Instanzen der Klasse PluggableBehavior unterschiedliches Verhalten haben, abhängig davon, welches Objekt der Methode pluggable() als Argument übergeben wird.
Die Einsatzgebiete sind sehr umfangreich, was neben der Sprachunabhängigkeit den Namen Pluggable-Behavior-Pattern rechtfertigt.
Allerdings wird diese Lösung auch als spezielle Variante des fundamentalen Adapter-Patterns (siehe 9.12.3) gehandelt.
Comparator:
ein Beispiel für Pluggable Behavior
Im Folgenden wird ein Array von Personen mit Hilfe von anonymen Plug-in-Objekten sortiert. Hierzu wird das Interface Comparator benutzt, das im Collection-Framework als eine pluggable Sortier- bzw. Ordnungs-Methode verwendet wird (siehe Abb. 8.9).
|
Anonyme Klassen sind ideale Plug-in-Methoden
public class Test { public static void main(String[] args) { Person[] parr= { Person.getPerson("Maier","17.10.1971"), Person.getPerson("Schmidt","15.01.1956"), /*...*/ }; // sortiert aufsteigend nach dem Namen der Person
Arrays.sort(parr,new Comparator() { public int compare(Object o1, Object o2) { return ((Person)o1).getName().compareToIgnoreCase( ((Person)o2).getName()); } }); // sortiert aufsteigend nach dem Alter der Person
Arrays.sort(parr,new Comparator() { public int compare(Object o1, Object o2) { return -((Person)o1).getBirthday().compareTo( ((Person)o2).getBirthday()); } }); } }
Abschließend wird eine immutable Klasse Person angelegt:
Design-Studie:
immutable Person mit private Konstruktor
class Person { private static DateFormat df= DateFormat.getDateInstance(DateFormat.MEDIUM); static final Person INVALID_PERSON;
private String name; private Date birthday;
Problem bei static final Initialisierung
(JBuilder 3)
static { // eine direkte Zuweisung der final INVALID_PERSON
// in try-catch lässt der Compiler nicht zu (s.u.)
Person p= null; try { p= new Person("@",df.parse("01.01.1")); } catch (Exception e) {} finally { INVALID_PERSON= p; } }
// direkte Anlage einer Person nicht erlaubt
private Person(String s, Date d) { name=s; birthday= d; };
public static Person getPerson (String name, String birthday) { try { return new Person (name,df.parse(birthday)); } ¨ catch (ParseException e) { return INVALID_PERSON; } }
public String getName() { return name; } public Date getBirthday() { return (Date)birthday.clone();} public String getBirthdayString() { return df.format(birthday); } }
Erklärung: Leider kann innerhalb des statischen Initialisierers im try-catch die Referenz INVALID_PERSON nicht direkt gesetzt werden, der Compiler sieht dies als womöglich doppelte Initialisierung an.
Da der Konstruktor private ist, kann keine Instanz direkt, sondern nur über getPerson() angelegt werden. Damit steht die Anlage unter voller Kontrolle der Klasse.
Fehlerhafte Personen-Anlagen werden nicht mit einer Ausnahme bestraft, sondern mit einer Referenz auf INVALID_PERSON.
zu ¨: Damit die Person immutable bleibt, wird bei getBirthday() nicht die Original-Referenz, sondern ein Klon exportiert.
Innere Klassen sind für den praktischen Einsatz konzipiert. Denn Top- Level-Klassen auf Package-Ebene lassen keine enge Bindung von Klassen zu.
Innere Klassen lassen eine weitere Strukturierung und Verteilung des Services einer Hauptklasse zu, ohne dass die Hilfsklassen isoliert von der Hauptklasse im Package deklariert und sichtbar werden.
Abhängig von dem Problem hat der Entwickler erst einmal die Wahl zwischen statischen und nicht statischen inneren Klassen.
Hat die Hilfsklasse Aufgaben nur auf Klassen-Ebene zu verrichten, ist eine der statischen Varianten sinnvoll. Hat dagegen die innere Klasse Instanzen der Hauptklasse zu unterstützen, ist eine nicht statische Variante zu wählen.
Die statischen inneren Klassen sind einfacher zu verstehen. Member-Klassen benötigen eine neue Syntax und sind in der Handhabung sicherlich komplexer.
Alle inneren Klassen weisen eine nützliche neue Eigenschaft auf, da sie Zugriff auf die Interna der äußeren Klasse, d.h. auf deren private deklarierte Felder haben.
Im Gegensatz zu der normalen Aggregation von Feldern in äußeren Klassen ist die Zugriffsrichtung vertauscht.
Member-Klassen lassen Kompositionen einfacher zu, die ansonsten auf Client-Server-Konventionen beruhen.
Durch die nachträgliche Einführung in die Java-Sprache treten bei Ausnutzung aller Möglichkeiten Effekte auf, die schwierig zu verstehen sind, und zu einem unklaren Design führen. Unangenehm wird es dann, wenn – wie in einem Beispiel demonstriert – verschiedene Compiler zu unterschiedlichen Ergebnissen kommen.
Als Letztes wurden ein Firewall-Idiom und das Pluggable-Behavior-Pattern anhand kurzer Beispiele vorgestellt.
Zu jeder Frage können jeweils eine oder mehrere Antwort(en) bzw. Aussage(n) richtig sein.
1. Welche Aussagen sind richtig?
2. Es ist folgende Definition gegeben:
class Outer { private int i; static class Inner1 { /*...*/ } ¨ class Inner2 { /*...*/ } ¦ void m() { class Inner3{ /*...*/ } } Æ }
3. Es ist folgende Applikation gegeben:
interface I { int f(); } class Outer { private int i= 1;
public static class Inner1 { public int j= 2; } public class Inner2 { public void f() { System.out.println("Inner2"); } } public I m() { return new I() { public int f() { return 3; ¨ // statt 3 soll die Summe von i+j der Klasse // Outer bzw. Inner1 zurückgegeben werden } }; } }
public class Test { public static void main(String[] args) { System.out.println(new Outer().Inner1().j); ¦ System.out.println(new Outer.Inner1().j); Æ System.out.println(new Inner1().j); Ø System.out.println(Outer.new Inner1().j); × new Outer().new Inner2().f(); ± new Outer().Inner2().f(); ð } }
Welche Aussagen bzw. Anweisungen sind richtig?
4. Es ist folgende Definition gegeben:
class OuterC { private int i=1; public static class Inner1 extends OuterC {} ¨ public class Inner2 { public int f() { return i; } } public OuterC m() { return new OuterC() { ¦ private int i= 3; public int f() { return i; } }; } public int f() { return i; } public static void main() { OuterC o= new OuterC(); System.out.print(new Inner1().f()); System.out.print(o.new Inner2().f()) Æ System.out.print(o.m().f()); Ø } } public class Test { public static void main(String[] args) { OuterC.main(); } }
B: Die Zeile ¨ erzeugt einen Compiler-Fehler.
C: Die Zeile ¦ erzeugt einen Compiler-Fehler.
D: Die Zeile Æ erzeugt einen Compiler-Fehler.
E: Die Zeile Ø erzeugt einen Compiler-Fehler.
5. Es ist folgende Definition gegeben:
class C1 { class C2 { C1 c1; C2(C1 c1) { System.out.print("C2"); this.c1=c1; } } C1() { System.out.print("C1"); } void f() { new C1().new C2(this); ¨ } }
public class Test { public static void main(String[] args) { new C1().f(); } }
D: Die Zeile ¨ erzeugt einen Compiler-Fehler.
1 Schachtelung führen aber zu obskuren Klassen-Konstrukten, die nur dazu dienen, Regeln, Compiler und die Umsetzung auf das externe Dateisystem zu testen.
2 Die Umkehrung gilt nicht, da dies zu einer zyklischen Ableitung führen würde.
3 Eine gleichzeitige Member- und Subklassen-Beziehungen ist nur pathologisch zu nennen, da dann eine Instanz der Member-Klasse einerseits mit einer Instanz der
äußeren Klasse über die Member-Beziehung und andererseits mit einer anderen
Instanz über die Vererbungs-Beziehung verbunden ist.
4 Sollte dies alles nicht klar sein, ein Hinweis: Konstrukt meiden. Sollte es klar sein: Konstrukt trotzdem meiden.
5 Außer es enthält eine Referenz auf das äußere Objekt (bidirektionale Assoziation!).
6 Außer es enthält eine Referenz auf das enthaltene Member-Objekt.
7 Diese Regel kann getunnelt werden, d.h. ist eine »Hinz- und Kunz«-Regel (siehe 8.6.1).
8 Sun Forte for Java CE 1.0, Release 2.
9 Auf private i kann nicht zugegriffen werden, da test() static deklariert ist.
10 Denn es wird immer wieder Anwender/Clients geben, die das Template-Prinzip
ignorieren und so versuchen, direkt auf die konkrete Klasse zuzugreifen, mit dem Effekt, dass sie bei Änderung der Implementation recht beleidigt sind.
| << zurück |
| |||||
| |||||
| |||||
| |||||
| |||||
| |||||
| |||||
| |||||
Copyright © Galileo Press GmbH 2001 - 2002
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken und speichern. 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.
Die Veröffentlichung der Inhalte oder Teilen davon bedarf der ausdrücklichen schriftlichen Genehmigung von Galileo Press. Falls Sie Interesse daran haben sollten, die Inhalte auf Ihrer Website oder einer CD anzubieten, melden Sie sich bitte bei: stefan.krumbiegel@galileo-press.de