Kapitel 8 Innere Klassen
Innere Klassen – ihrem Namen entsprechend in anderen Klasse enthalten – lösen gewisse Probleme einfach und elegant. Abhängig von der Intention bzw. Verwendung können innere Klassen auf vier verschiedene Arten deklariert werden.
Neben Klassen gibt es auch innere Interfaces, die allerdings nur als statischer Typ sinvoll sind.
Bedingt durch die Flexibilität, die sich aufgrund der Kombinationsmöglichkeiten von inneren Klassen und Modifikatoren ergeben, sind die Syntax-Regeln zu inneren Klassen nicht unbedingt trivial.
Innere Klassen (inner classes) wurden erst nachträglich in Java ab JDK 1.1 eingeführt. Überraschenderweise waren nur kleine Syntaxerweiterungen, aber keine Änderung an der JVM notwendig.
Modifikatoren zu inneren Klassen
Bedingt durch ihre Deklaration innerhalb einer anderen Klasse, sind neben den Zugriffs-Modifikatoren noch die Modifikatoren abstract, final oder static erlaubt.
Top-Level- vs. innere Klasse
Neben den rein formalen bzw. syntaktischen Unterschieden gegenüber den regulären Top-Level-Klassen, die direkt auf Package-Ebene deklariert sind, werden abschließend Einsatzmöglichkeiten von inneren Klassen besprochen.
Interfaces sind immer statisch
Da innere Interfaces ausschließlich in statischer Form auftreten, werden sie nur kurz im folgenden Abschnitt behandelt.
8.1 Arten von inneren Klassen ![down](common/down.gif) ![top](common/15x15leer.gif)
Zur Deklaration von inneren Klassen bzw. Interfaces wird – im Gegensatz zur Verwendung – keine neue Syntax benötigt.
Dadurch, dass sie zu einer äußeren Klasse gehören, unterliegen sie allerdings denselben Zugriffsrestriktionen wie alle anderen Member der äußeren Klasse.
Innere Klassen lassen sich in zwei grundlegend verschiedene Typen einteilen: statische und nicht statische (siehe Abb. 8.1).
Typisierung von inneren Klassen
Abbildung 8.1 Übersicht über innere Klassen bzw. Interfaces
Nested-Top-Level-Klasse
Die Variante »statische innere Klassen« wird häufig auch als nested top-level class bezeichnet.
Ein grundlegender semantischer Unterschied besteht nur zwischen den statischen und den nicht statischen Typen.
Lokale bzw. anonyme Klassen sind entweder vom statischen oder aber nicht statischen Typ (siehe Abb. 8.1).
Obwohl selten genutzt, gilt:
Schachtelung von inneren Klassen
|
Klassen können beliebig tief ineinander geschachtelt werden, wobei aber nicht jede Kombination der vier Typen erlaubt ist.1 |
![Icon](common/R.gif)
Neben der normalen Regel, dass Felder innerhalb einer Klasse unterschiedliche Namen haben müssen, gilt für alle inneren Klassen bzw. Interfaces noch folgende zusätzliche Namens-Regel:
Namens-Regel für innere Klassen
|
Der Name einer inneren Klasse bzw. eines Interfaces darf nicht identisch mit dem einer äußeren Klasse sein (d.h., mit dem Namen der Klasse, die sie bzw. es enthält). |
Nachfolgend werden zuerst Syntax und Semantik der verschiedenen Typen besprochen.
8.2 Statische innere Klassen und Interfaces ![down](common/down.gif) ![top](common/top.gif)
Äußere Klasse:
Sub-Package für statische innere Klassen
Für statische innere Klassen und Interfaces bildet eine Top-Level-Klasse eine Art Sub-Package, d.h. einen weiteren Namens-Raum unterhalb des Packages.
Abbildung 8.2 Top-Level-Klasse als Sub-Package
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
[extends SuperClass] [implements Interface1, ...]
{...}
[icModifiers] [static] interface
InnerInterface
[extends
Interface1,...]
{...}
//...
}
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
|
Die Anlage einer Instanz einer statischen inneren Klasse ist völlig unabhängig von den Instanzen der äußeren Klasse. |
Insbesondere gibt es also keine besonderen wechselseitigen Zugriffsmöglichkeiten auf Instanz-Variablen.
![Icon](common/R.gif)
Für Nested-Top-Level-Klassen bzw. Interfaces gelten folgende zusätzliche Regeln:
Beziehungen der statischen inneren Klasse zur äußeren
1. |
Statische innere Klassen und Interfaces können ineinander geschachtelt werden. |
2. |
Alle statischen inneren Klassen sowie die äußere Klasse können untereinander auf alle statischen Member zugreifen. |
3. |
Statische Klassen können Subklassen von äußeren sein.2 |
4. |
Statische und äußere Klassen können ein inneres Interface implementieren. |
Beispiele
Inneres 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
abstract class Outer2 implements InnerI,
InnerC1.InnerI {
interface InnerI { void f1(); }
// Vermeidung in umgekehrter Richtung
abstract static class InnerC1 implements Outer.InnerI,
InnerI {
Namensregel gilt für geschachtelte innere Klassen
// weiteres Interface mit gleichem
Namen wie oben
static interface InnerI { void f2(); }
// weitere innere Klasse mit unterschiedlichem Namen
static class InnerC2 {}
}
// nicht abstract, da Implementation von
f1() und f2()
static final class InnerC3 extends Outer {
void f1() {};
void 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 ![down](common/down.gif) ![top](common/top.gif)
2D-Klassen:
äußere abstrakte Klasse mit inneren statischen
Subklassen
Das Konstrukt »Erweiterung einer abstrakten äußeren Klasse durch eine statische innere Klasse« wird u.a. im neuen Package java.awt.geom für 2D-Klassen eingesetzt.
Hier ein Deklarations-Schnipsel:
public abstract class Point2D implements
Cloneable {
public static class Float extends Point2D {
//...
}
public static class Double extends
Point2D {
//...
}
}
Die konkreten Punkte haben dann entweder float- oder double-Koordinaten, können aber unabhängig davon alle als Typ Point2D angesprochen werden.
Template-Prinzip für 2D-Objekte
Nach dem Template-Prinzip »Programmiere gegen ein Interface« (siehe 6.6) werden – wenn möglich – die Instanzen der konkreten statischen inneren 2D-Klassen über Referenzen von Interfaces wie z.B. Shape angesprochen:
Shape circle= new Ellipse2D.Double(10.0,10.0,10.0,10.0);
Das Interface Shape steht dabei für ein generisches 2D-Objekt, welches offen oder geschlossen sein kann, wobei natürlich die fundamentalen Shape-Operationen wie z.B. »Ausfüllen« oder »Transformieren« von allen Shape-Objekten implementiert sind.
8.3 Nicht statische innere Klassen ![down](common/down.gif) ![top](common/top.gif)
Alle nicht statischen inneren Klassen haben unabhängig von ihrer Ausprägung als Member-, lokale oder anonyme Klasse Gemeinsamkeiten in der Instanz-Bindung und den Regeln.
Ihre Instanzen verhalten sich entweder wie Felder von Instanzen der äußeren Klasse oder wie lokale Variablen in Blöcken und Methoden. Benötigt man nur eine lokale Variable, reicht eine lokale Klasse ohne Namen.
Von allen drei Subtypen bilden zweifellos die Member-Klassen in Bezug auf Syntax und Semantik die schwierigste Untergruppe.
8.4 Member-Klassen ![down](common/down.gif) ![top](common/top.gif)
![Icon](common/R.gif)
Instanzen von Member-Klassen haben Ähnlichkeit mit Feldern, sie haben allerdings besondere Regeln:
Beziehungen der Member-Klasse zur äußeren Klasse
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. |
Abbildung 8.3 Assoziation einer Member-Klasse zur äußeren Klasse
Member-Klasse vs. Feld
|
In einer äußeren Instanz können beliebig viele Instanzen einer Member-Klasse »enthalten« sein (siehe Abb. 8.3). |
![Icon](common/Kaktus.gif)
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.
Beispiel
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.
8.4.1 Anlage und Zugriff auf Instanzen einer Member-Klasse ![down](common/down.gif) ![top](common/top.gif)
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.
Beispiel
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.
Abbildung 8.4 Member-Bindung an eine Instanz der äußeren Klasse
Eine Member-Registrierung ist dagegen implizit schlecht möglich, da die Anzahl der Member-Objekte beliebig groß ist.
Tiefe Schachtelung von
Member-Klassen ![Icon](common/Kaktus.gif)
Beispiel
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).
Abbildung 8.5 Logischer Zusammenhang der Instanzen in Methode f()
Fazit: Vermeide Schachtelungen von Member-Klassen!
8.4.2 Member-Klassen und Vererbung ![down](common/down.gif) ![top](common/top.gif)
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.
![Icon](common/Kaktus.gif)
1. Eine Member-Klasse ist Subklasse der äußeren Klasse
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.
2. Eine Klasse ist Subklasse einer Member-Klasse
![Icon](common/Kaktus.gif)
Subklassen von Member-Klassen
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!
//...
}
}
3. Eine äußere Klasse ist Subklasse
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).
Abbildung 8.6 Zugriff auf Superklassen der äußeren Klasse
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. |
Beispiel (zu Abb. 8.6)
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();
8.4.3 Member-Klasse vs. Instanz-Variable ![down](common/down.gif) ![top](common/top.gif)
![Icon](common/T.gif)
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
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.
Beispiel
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.
![Icon](common/T.gif)
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.
8.5 Lokale Klassen ![down](common/down.gif) ![top](common/top.gif)
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
![Icon](common/R.gif)
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). |
Beispiele
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);
8.5.1 Gültigkeit vs. Lebensdauer ![down](common/down.gif) ![top](common/top.gif)
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«.
Scope vs. Existenz
|
In diesem Fall ist also die Lebensdauer von lokalen Objekten größer als ihre Gültigkeit. |
Beispiel
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;
}
}
Ein zugehöriger Testcode:
B b= new Outer().stillAlive("Wert"); System.out.println(b.f());
// :: Wert 10
Hinweis zur final-Regel in 8.5
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.
8.6 Anonyme Klassen ![down](common/down.gif) ![top](common/top.gif)
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:
![Icon](common/R.gif)
Deklaration anonymer Klassen
|
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. |
![Icon](common/R.gif)
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 |
Beispiele
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
}
}
![Icon](common/D.gif)
Design-Hinweise für anonyme Klassen
Einsatz von anonymen Klassen
|
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.
8.6.1 Zugriff auf interne Methoden anonymer Klassen ![down](common/down.gif) ![top](common/top.gif)
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) {}
}
}
8.6.2 Probleme mit anonymen Klassen ![down](common/down.gif) ![top](common/top.gif)
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.
Beispiel: Compiler- bzw. JVM-Test
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...
8.7 Firewall-Idiom: Quasi-Objekte von Interfaces ![down](common/down.gif) ![top](common/top.gif)
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.
Abbildung 8.7 Klassen-Schablone für Quasi-Objekte von Interfaces
![Icon](common/i.gif)
Ä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.
Beispiel
Exportieren von statischen inneren Klassen als Interface-Objekte
Die folgende Klasse FirewallFactory liefert aufgrund eines Selektors drei verschiedene Implementierungen für den IService aus:
Schutz vor unerlaubtem Cast
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(); }
Schutz vor
Reflexion
// 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); }
}
}
}
8.8 Generisches Verhalten ![down](common/down.gif) ![top](common/top.gif)
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.
8.8.1 Pluggable-Behavior-Pattern ![down](common/down.gif) ![top](common/top.gif)
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).
![Icon](common/P.gif)
Abbildung 8.8 Variante des Pluggable-Behavior-Pattern
Pluggable-Behavior-Pattern
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.
Vorteil
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.
Beispiel
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).
Abbildung 8.9 Generische Methode sort() mit Plug-in compare()
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.
8.9 Zusammenfassung ![down](common/down.gif) ![top](common/top.gif)
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.
8.10 Testfragen ![top](common/15x15leer.gif) ![top](common/top.gif)
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{ /*...*/ } } Æ
}
Welche Aussagen sind richtig?
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();
}
}
Welche Aussagen sind richtig?
A: Die Ausgabe ist: 113
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();
}
}
Welche Aussagen sind richtig?
A: Die Ausgabe ist: C2C1C1
B: Die Ausgabe ist: C1C2
C: Die Ausgabe ist: C1C1C2
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.
|