Kapitel 13 Reflexion
Reflexion erlaubt eine neue Art der Programmierung, auch Meta-Programmierung genannt.
Zur Compile-Zeit noch nicht bekannte Klassen können zur Laufzeit geladen und auf ihre Fähigkeiten untersucht werden. Diese können dann mit Hilfe des Reflexions-Mechanismus wie a priori bekannte Klassen benutzt werden.
Das innovative Konzept weicht vom traditionellen Programm-Design typgebundener Sprachen radikal ab.
OO-Evolution:
Klasse ‡ Interface ‡ Reflexion
13.1 Klasse, Interface und Reflexion
Betrachten wir kurz die evolutionären Schritte von der Klasse über das Interface hin zur Reflexion.
Klasse
In der traditionellen OO-Programmierung schreibt man Code auf Basis von Klassen(-Frameworks). Der Service ist dann selbstverständlich nur auf Objekte der Hierarchie beschränkt, Clients müssen explizit diese Hierarchie verwenden.
Interface
Durch die Einführung des Interface-Patterns ist es möglich, Code nur auf Basis von Interface-Frameworks zu schreiben. Somit kann dieser Code von Clients genutzt werden, ohne überhaupt eine konkrete Klassen-Implementation bzw. -Hierarchie zu kennen.
Interface:
Flexibilität verbunden mit Typsicherheit
Wie an den meisten Pattern, Beispielen und dem Collection-Framework zu sehen, ist man mit Hilfe dieses evolutionären Schritts in der Lage, sehr generellen Code bzw. Templates zu schreiben, wobei ein entscheidender Vorteil der Klassen erhalten bleibt:
|
Das Interface-Konzept bietet Typsicherheit. Der Compiler ist aufgrund der Typprüfung in der Lage, gravierende Fehler frühzeitig abzufangen.1 |
Reflexion
Im Zeitalter globaler, firmenübergreifender Netze kommunizieren (B2B- bzw. B2C-)Applikationen2 auf Basis von Applets, Komponenten- oder Agenten-Systemen, deren Fähigkeiten häufig nicht durch Interfaces zu Compile-Zeit festzulegen sind.
Die Objekte können erst zur Laufzeit auf ihre Fähigkeiten hin untersucht werden, um sie dann zu nutzen.
Reflexion:
Service- Bestimmung zur Laufzeit
|
Reflexion bietet den Vorteil, den Service von Klassen zu nutzen, wenn es zur Compile-Zeit nicht möglich ist, diese Klassen auf die Einhaltung gewisser Schnittstellen zu überprüfen. |
Der Vorteil der Reflexion ist gleichzeitig ihr Nachteil:
Reflexion:
Verlust der Typsicherheit
|
Reflexion hat den inhärenten Nachteil, die Typsicherheit zu unterlaufen, sodass viele Fehler erst zur Laufzeit auftreten und der Compiler keine Chance mehr hat, Fehler frühzeitig abzufangen. |
Interfaces vs. Reflexion
Das Einsatzgebiet von Reflexion deckt sich stark mit dem der Interfaces. Hat man (als Designer) die Wahl zwischen beiden Techniken, lautet die Heuristik (für die Schnittmenge):
Interfaces:
1. Wahl bei Design- Entscheidungen
|
Kann der erforderliche Service auch auf Basis von Interfaces realisiert werden, sind sie einem Konzept, das auf Reflexion basiert, vorzuziehen. |
13.2 Übersicht über Reflexion
Bevor wir auf interessante Einsatzgebiete zu sprechen kommen, werden in den folgenden Abschnitten zuerst alle Reflexions-Techniken vorgestellt.
13.2.1 Package java.lang.reflect
Mit Ausnahme der Klassen Object, Class und Package befinden sich alle Klassen für den hauptsächlichen Teil der Reflexions-Mechanismen im Subpackage java.lang.reflect (siehe Abb. 13.1).
Die beiden Ausnahmen InvocationTargetException und UndeclaredThrowableException sowie die Permission-Subklasse ReflectPermission für den Sicherheitsmanager wurden im Diagramm weggelassen.
Klassen mit Reflexionsaufgaben
Abbildung 13.1 Am Reflexions-Mechanismus beteiligte Klassen
Die Zugehörigkeiten der Klassen zu ihren Reflexionsaufgaben sind bereits an den Namen zu erkennen. Es folgt ein kurzer Überblick.
13.2.2 Object und Class
Class: Basisklasse der Reflexion
Ausgangspunkt des Reflexions-Mechanismus ist Class, die für alle Objekte mittels getClass() von Object geholt werden kann.
Da Class sowie Object bereits in Kapitel 10, Package java.lang, besprochen wurden, werden hier nur noch einmal die wesentlichen Charakteristiken zusammengestellt.
|
Zu jeder geladenen Klasse bzw. jedem Interface existiert zur Laufzeit genau eine Instanz von Class in der JVM. |
|
Die Klasse Class ist immutable und man kann keine Class-Instanzen erschaffen (No-Arg-Konstruktor ist private). |
|
Eine Referenz zur Class-Instanz erhält man bei |
TYPE
getClass() .class
|
|
primitiven Typen: mit Hilfe der Konstanten TYPE der zugehörigen Wrapper-Klasse, |
|
|
Objekten: mit Hilfe der Object-Methode getClass(), |
|
|
Klassen: durch Anfügen von .class an den Klassennamen. |
forName():
Zugriff auf eine Klasse über ihren Namen
|
Die statische Class-Methode forName() kann – sofern notwendig – zur Laufzeit eine Klasse über ihren vollen Namen laden und liefert eine Class-Instanz, über die man auf die Klasse zugreifen kann. |
|
Die Methode newInstance() eines Class-Objekts erschafft Instanzen der Klasse, sofern der No-Arg-Konstruktor im Zugriff steht. |
Eine unbekannte Klasse bzw. unbekanntes Interface kann mit Hilfe der get<X>()- und is<X>()-Methoden3 von Class in allen Details untersucht werden.
13.2.3 Constructor, Field und Method
Spezialisierte Klassen
Die Instanzen der Klassen Constructor, Field oder Method werden von namensgleichen Getter-Methoden der Klasse Class geliefert. Auch ihre Aufgabe wird vom Namen reflektiert:
Constructor
Field Method
|
Constructor legt Instanzen an. |
|
Field greift (lesend/schreibend) auf Feldwerte zu. |
|
Method ist für die Ausführung von Methoden zuständig. |
AccessibleObject
Die Superklasse AccessibleObject definiert nur drei Methoden.
Zugriffsüberprüfung und
-erlaubnis
|
setAccessible() versucht durch das Argument true eine Zugriffserlaubnis für ein Member zu setzen, auch wenn es nicht im normalen Zugriff steht. |
|
setAccessible() in der statischen Version versucht, die Zugriffserlaubnis für ein Array von Membern vom Typ AccessibleObject zu setzen. |
Ist ein Sicherheitsmanager installiert, prüft er die Zulässigkeit und löst andernfalls eine SecurityException aus.4
Eine SecurityException wird z.B. dann ausgelöst, wenn man auf den Konstruktor der Klasse Class zugreifen will.
Hat man aber eine entsprechende Zulassung, kann diese mit isAccessible() geprüft werden.
Member
Interface Member
Die drei o.a. Klassen implementieren alle das Interface Member, das die folgenden gemeinsamen Methoden definiert:
|
getDeclaringClass() liefert das Class-Object, zugehörig zur Klasse des Members. |
|
getModifiers() liefert ein Bit-Muster als int-Wert, das alle zugehörigen Modifikatoren des Members identifiziert (siehe Modifier). |
|
getName() liefert den Namen des Members. |
13.2.4 Modifier
Bit-Muster identifiziert Modifier
Um das Bit-Muster, das die Methode getModifiers() liefert, ohne Bit-Operationen auswerten zu können, existiert die Klasse Modifier, die nur statische Konstante und Methoden enthält.
Die Konstanten tragen die Namen der Modifikatoren und identifizieren diese durch ein Bit (z.B.: public static final int ABSTRACT= 1024).
Mit is<Modifier>()5 kann dann getestet werden, ob der Modifikator für das Member gesetzt ist (z.B.: isAbstract())
13.2.5 Array
Array: statische Methoden für Anlage und Elementzugriff
Die Klasse Array manipuliert Arrays nur mit Hilfe von statischen Methoden und hat eine Sonderstellung, da es keine Methoden- und Konstruktoren-Aufrufe gibt. Mit
|
static Object newInstance(Class compType, int length) |
|
static Object newInstance(Class compType, int[] dim) |
können Arrays von beliebigem Typ und beliebiger Länge bzw. Dimension angelegt werden.
Die übrigen Methoden sind Getter- und Setter-Methoden für jeden der primitiven Typen. Sie haben folgende generelle Form:
|
get<primitiveType>() bzw. set<primitiveType>() |
Beispiel:
public static long getLong(Object array, int index)
public static void setLong(Object array,int index,long l)
13.2.6 Proxy
Proxy: Stellvertreter mit
verschiedenen Aufgaben
Die Bezeichnung »Proxy« deutet schon auf ein gleich lautendes, grundlegendes Pattern hin, das in Variationen auftreten kann.
|
Mit Proxy bezeichnet man ein Stellvertreter-Objekt für ein Original-Objekt, welches nicht im direkten Zugriff steht. |
Ein Proxy ist ein
Remote
|
Remote-Proxy, sofern es ein lokaler Stellvertreter für ein Objekt ist, das nur über ein Netz(protokoll) erreichbar ist. |
Access
|
Access-Proxy, sofern es die wahren Objekte kapselt, um den direkten Zugriff zu blockieren. |
Broker/Façade
|
Broker/Façade-Proxy, sofern es (je nach Methoden-Aufruf) die Objekte selektiert, die den Service wirklich ausführen.6 |
Virtual
|
Virtual-Proxy, sofern es die Objekte on-demand lädt, d.h. erst, wenn sie wirklich gebraucht werden. |
Klasse Proxy
Erzeugung von Proxy-Objekten
Die Proxy-Klasse kann einen Access-, Broker- oder Virtual-Proxy realisieren, je nachdem, wie sie verwendet wird. Mit der statischen Methode
public static Object newProxyInstance(ClassLoader loader,
Class[] interfaces, InvocationHandler h)
throws IllegalArgumentException;
wird ein Class-Objekt erschaffen und zurückgegeben, das
|
die übergebenen spezifizierten Interfaces implementiert, |
|
an das übergebene InvocationHandler-Objekt alle Client-Aufrufe von Methoden weiterleitet. |
Interface InvocationHandler
InvocationHandler:
Mittler zwischen Proxy und Objekt
Das Interface InvocationHandler besteht nur aus einer Methode, mit deren Hilfe die Aufrufe an die tatsächlichen Objekte weitergeleitet werden:
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable;
13.3 Inspektion von Klassen und Interfaces
Nach dem kurzen Überblick werden hier die Standard-Mechanismen zur Klassen-Inspektion vorgestellt.
Die einzelnen Methoden sind meist selbsterklärend. Ihre Verwendung wird anhand von Code-Fragmenten demonstriert und – falls opportun – mit kurzen Kommentaren versehen.
13.3.1 Class-Objekt
Beispiele:
Finden und Laden von Class
Von einer zu inspizierenden Klasse kann man sich das zugehörige Class-Objekt auf verschiedene Arten holen.
Class-Objekt zu Klassen
Class oc= object.getClass();
Class oc= ClassType.class;
Class oc= Class.forName("QualifiedClassname");
Class-Object zu primitiven Typen oder void
Class pc= PrimitiveWrapperClass.TYPE;
Class pc= primitiveType.class;
Beispiel
Zu String, double und void sollen auf verschiedene Arten Referenzen zum Class-Objekt geholt werden:
Class sc1= String.class;
Class sc2= new String().getClass();
Class sc3;
try {
sc3= Class.forName("java.lang.String");
} catch (ClassNotFoundException e) { sc3= null; }
// alle drei Referenzen zeigen auf nur eine Class-Instanz
System.out.println(sc1==sc2 && sc1==sc3); // :: true
System.out.println(sc1.getName()); // :: java.lang.String
Class dc1= Double.TYPE;
Class dc2= double.class;
System.out.println(dc2==dc1); // :: true
System.out.println(dc2); // ::
double
System.out.println(void.class); // ::
void
13.3.2 Methoden get<X> vs. getDeclared<X>
Die Zugriffs-Methoden mit der Bezeichnung get<X> liefern nur public deklarierte Methoden oder Objekte als Ergebnis, die gleichnamigen Methoden mit dem Zusatz »Declared« – kurz Declared-Variante – liefern die entsprechenden Ergebnisse für alle Zugriffs-Modifikatoren.
get<X> bzw.
getDeclared<X>
Die Methoden
|
get<X> beziehen sich auf die gesamte Hierarchie, |
|
getDeclared<X> beziehen sich nur auf die inspizierte Klasse, ohne Superklassen bzw. Super-Interfaces zu berücksichtigen. |
Beispiel
getClasses() bzw.
getDeclaredClasses()
Die Methode mit dem missglückten Namen getClasses() liefert alle public deklarierten inneren Klassen und Interfaces einer inspizierten Klasse einschließlich der geerbten. Die Declared-Variante liefert dann zusätzlich alle nicht public deklarierten, schließt aber die geerbten aus:
package kap13;
class TestMemberBase {
public class M {}
}
class TestMember extends TestMemberBase {
public interface I1 {}7
public static class S {}
public class M1 {}
class M2 {}
}
Beim Test liefert die Inspektion von TestMember bei getClasses() sowie getDeclaredClasses() jeweils vier Class-Objekte.
Class[] c= TestMember.class.getClasses();
¨
//Class[] c= TestMember.class.getDeclaredClasses(); ¦
for (int i=0; i < c.length; i++) System.out.println(c[i]);
Tabelle 13.1 Ausgabe zum Test: Zeile ¨ links, Alternative ¦ rechts
class kap13.TestMember$M1
class kap13.TestMember$S
interface kap13.TestMember$I1
class kap13.TestMemberBase$M
|
class kap13.TestMember$M2
class kap13.TestMember$M1
class kap13.TestMember$S
interface kap13.TestMember$I1
|
13.3.3 Package, Superklassen und Interfaces
Informationen zu:
Package und Hierarchie der Klassen/Interfaces
Mit Hilfe des Class-Objekts können Informationen zum Package, der Superklasse und den implementierten Interfaces geholt werden.
Package package= classObject.getPackage();
Class superClass= classObject.getSuperClass();
Class[] interface= classObject.getInterfaces();
Beispiele
Die Methoden der Klasse Package geben Auskunft über das zugehörige Package einer Klasse:
Class c= Integer.class;
Package p= c.getPackage();
System.out.println(
p.getName() +"\n"+ // :: java.lang
p.getImplementationTitle() +"\n"+ // :: Java Runtime Environment
p.getImplementationVendor() +"\n"+ // :: Sun Microsystems, Inc.
p.getImplementationVersion()+"\n"+ // :: 1.3
(p.isSealed()?"":"n.")+" versiegelt");// ::
n. versiegelt
Informationen zu
JAR-Dateien
|
Sofern die Klasse aus einer JAR-Datei7 stammt, können die Attribute der Manifest-Datei mit den im Beispiel aufgeführten Methoden abgefragt werden. |
Sind alle Klassen des Packages in nur einer JAR-Datei enthalten, liefert die Methode isSealed() den Wert true, andernfalls false.
Der folgende Code ermittelt alle Superklassen:
Class c= Integer.class;
while ((c= c.getSuperclass())!=null) {
String s=c.getName(); // voll
qualifizierender Name
// liefert nur die Klassennamen ohne Package-Name
System.out.println( //
:: Number
s.substring(s.lastIndexOf(".")+1)); //
:: Object
}
Die Methode getInterfaces() liefert in einem Array für eine Klasse die implementierten Interfaces oder für ein Interface die zugehörigen Super-Interfaces, wie das folgende Beispiel zeigt.
package kap13;
interface I1 {}
interface I2 {}
interface I3 extends I2,I1 {}
getInterfaces()
Class[] si= I3.class.getInterfaces();
for (int i=0; i<si.length;i++)
System.out.println(si[i].getName()); // :: kap13.I2
kap13.I1
String-Darstellung für Arrays
Darstellung der
Array-Dimensionen und des Element-Typs
Gehört ein Class-Objekt zu einem Array, wird mit getName() eine recht eigenwillige String-Darstellung zurückgeliefert:
1. |
Es werden so viele Klammern [ geliefert, wie das Array Dimensionen hat. |
2. |
Der Typ der Komponenten folgt als identifizierendes Zeichen. Bei |
|
|
primitiv: Z (boolean), B (byte), C (char), S (short), I (int),
J (long), F (float), D (double). |
|
|
Referenz: L mit angehängtem Klassennamen. |
13.3.4 Konstruktoren und Modifikatoren
Informationen zu Konstruktoren
Die folgenden beiden Methoden gibt es auch in der Declared-Variante (siehe Methoden get<X> vs. getDeclared<X>). Die erste Methode liefert alle Konstruktoren, die zweite nur den Konstruktor, dessen Signatur zu den Class-Argumenten passt:
getConstructors()
getConstructor()
public Constructor[] getConstructors()
throws SecurityException;
public Constructor getConstructor(Class[] pTypes)
throws NoSuchMethodException,SecurityException;
Wird die Signatur nicht getroffen, wird eine NoSuchMethodException ausgelöst.
Beispiele
Die Klasse ReflectUtil soll neben den Zugriffen auf Konstruktoren nur noch einige Modifier-Methoden demonstrieren.
Die statische Methode printConstructors() ist nur eine alternative Kurzform der Instanz-Methode Constructor.toString().
Abfrage der Modifier
class ReflectUtil {
public static String stripPrefix(String s) {
return s.substring(s.lastIndexOf(".")+1);
}
public static String getAccModifier(int i) {
if (Modifier.isPrivate(i)) return "private ";
else if (Modifier.isProtected(i)) return "protected ";
else if (Modifier.isPublic(i)) return "public ";
else return "";
}
Untersuchung der Konstruktoren
public static void printConstructors(Class c,
boolean publicOnly) {
if (c.isInterface()) return;
Constructor[] constr= (publicOnly? c.getConstructors():
c.getDeclaredConstructors());
for (int i= 0; i< constr.length; i++) {
System.out.print(
getAccModifier(constr[i].getModifiers())
+ stripPrefix(constr[i].getName())+"(");
Class[] pType= constr[i].getParameterTypes();
for (int j= 0; j<pType.length; j++)
System.out.print((j==0?"":",")+
stripPrefix(pType[j].getName()));
System.out.println(")");
}
}
}
Ein kurzer Test:
ReflectUtil.printConstructors(Class.class,false);
ReflectUtil.printConstructors(CharArrayReader.class,true);
Tabelle 13.2 Ausgabe zum Test
private Class()
public CharArrayReader([C,int,int)
public CharArrayReader([C)
|
Im Folgenden wird ein spezieller Konstruktor der String-Klasse ausgewählt.
Informationen
zu einem String-Konstruktor
Constructor co;
try {
co= String.class.getConstructor(new
Class[] {byte[].class});
} catch(NoSuchMethodException e) { co= null; };
System.out.println(co); // :: public java.lang.String(byte[])
13.3.5 Felder
Feld-Informationen:
getFields() getField()
Analog zu den Konstruktoren gibt es vier Methoden, wobei die Declared-Varianten nicht aufgeführt werden:
public Field[] getFields() throws SecurityException;
public Field getField(String name) throws SecurityException,
NoSuchFieldException;
Ein spezielles Feld wird mit Hilfe seines einfachen Namens selektiert.
Beispiel
class B { public int ib; private char
cb; }
class A extends B { public int ia; private char ca; }
Field[] fld= A.class.getFields();
¨
// Field[] fld= A.class.getDeclaredFields(); ¦
for (int i= 0; i< fld.length; i++)
System.out.println(fld[i]);
// Versuch der Feldauswahl löst NoSuchFieldException
aus
try {
System.out.println(A.class.getField("ca"));
} catch(NoSuchFieldException e) {};
Zu ¨ und ¦: In Zeile ¨ werden die Felder ia und ib als Ergebnis geliefert, bei der Declared-Variante in Zeile ¦ die Felder ia und ca.
13.3.6 Methoden
Methoden-
Informationen: getMethods() getMethod()
Auch hier gibt es die üblichen Zugriffs-Methoden, wobei die Declared-Varianten wieder nicht aufgeführt werden:
public Method[] getMethods() throws SecurityException;
public Method getMethod(String name,Class[] parameterTypes)
throws NoSuchMethodException,SecurityException;
Die spezielle Auswahl einer Methode erfolgt über ihren einfachen Namen und ihre Signatur (analog zu den Konstruktoren).
Der folgende Code selektiert StringBuffer.append(char[],int,int):
Informationen
zu StringBuffer.append()
Method m;
try {
m= StringBuffer.class.getMethod("append",
new Class[] {char[].class,int.class,int.class });
} catch(NoSuchMethodException e) { m= null; }
13.3.7 Array
elementType[].class
Mit type[].class kann die Class-Instanz eines Arrays ermittelt werden.
Array-Prüfung
isArray()
Eine Class-Instanz cArray
gehört zu einem Array genau dann, wenn:
cArray.isArray()==true
Array-Komponenten
Die Class-Instanz der Komponente eines Arrays liefert die Methode:
Multidimensional: eindimensionales Array von Array-Komponenten
cArray.getComponentType();
Da es keine echten multidimensionalen Arrays gibt, gilt:
|
Die Komponenten eines (so genannten) mehrdimensionalen Arrays sind wieder Arrays, d.h., getComponentType() liefert eine Class-Instanz, für die isArray() den Wert true liefert. |
Beispiel
Bestimmen des
Element-Typs
class ArrayUtil {
public static Class getElementType(Class
cArray) {
int n= 0;
while (cArray.isArray()) {
cArray= cArray.getComponentType(); n++;
}
if (n>0) return cArray;
throw new IllegalArgumentException("Kein Array!");
}
Bestimmen der Dimension
public static int getDimension(Class
cArray) {
int n= 0;
while (cArray.isArray()) {
cArray= cArray.getComponentType(); n++;
}
return n;
}
}
13.4 Manipulation von Klassen und Arrays
Nach Inspektion einer Klasse können Konstruktoren, Felder und Methoden prinzipiell genauso genutzt werden wie diejenigen von Klassen, die zur Übersetzungszeit bekannt sind.
13.4.1 Anlage von Objekten
Die Klasse Constructor hat eine zur Class gleich lautende Methode:
Object-Erzeugung mit newInstance()
Object newInstance(Object[] initargs)
throws InstantiationException,IllegalAccessException,
IllegalArgumentException, InvocationTargetException;
Im Gegensatz zu Class kann mit newInstance() jeder im Zugriff stehende Konstruktor ausgeführt werden, nicht nur der No-Arg-Konstruktor.
Beispiel
Ein String-Objekt wird mit Hilfe des Konstruktors
String(char value[], int offset, int count)
angelegt:
Erzeugen eines Strings
mit newInstance()
char[] carr= new char[] {'a','b','c'};
String s= null;
try {
Constructor co= String.class.getConstructor(
new Class[] {char[].class,int.class, int.class} );
s= (String) co.newInstance(
new Object[]{carr,new Integer(0),new Integer(2)});
} catch (Exception e) { System.out.println(e); }
System.out.println(s); // :: ab
Argumente müssen immer als Object-Array übergeben werden. Dies impliziert:
|
Argumente von primitiven Typen müssen mit Hilfe von Wrapper-Instanzen übergeben werden. |
13.4.2 Lesen und Schreiben von Feldern
Zugriff auf
Feldwerte: get(), get<primitiveT>() set(), set<primitiveT>()
Der Zugriff auf Feldwerte erfolgt mit Getter und Setter der Klasse Field:
Object get(Object o);
<primitiveType> get<primitiveType>(Object
o);
void set(Object o, Object value);
void set<primitiveType>(Object
o,<primitiveType> v);
Field-Getter/Setter-Konventionen
Für das erste Object-Argument gilt bei
|
Instanz-Methoden: Es wird die Instanz übergeben, für die die Feld-Operation ausgeführt werden soll. |
|
statischen Methoden: Das Argument wird ignoriert, es sollte deshalb null sein. |
Zusätzlich gilt für
|
Getter-Methoden: Das Resultat ist entweder ein Objekt oder bei den Varianten direkt der Wert des primitiven Typs. |
|
Setter-Methoden: Mit dem zweiten Argument wird die neue Feldreferenz übergeben. Für primitive Typen hat man die Alternative, den Wert in einem Wrapper zu übergeben oder die passende Variante zu benutzen. |
Beispiel
class TestField {
static public double d= 1.0;
String s= "instanz";
}
Auf die beiden Felder in TestField wird mit Field-Getter bzw. -Setter zugegriffen:
TestField tf= new TestField();
Class c= TestField.class;
try {
Field fd= c.getField("d");
Field fs= c.getDeclaredField("s"); // Declared notwendig ¨
setDouble()
fd.setDouble(null,-1.0); // primitive
Variante
fd.set(null, new Double(2.0));
// Wrapper-Variante
fs.set(tf, "Instanz");
// nur so!
getDouble()
System.out.println(fd.getDouble(null)); // :: 2.0
System.out.println(fd.get(null));
// :: 2.0
System.out.println(fs.get(tf));
// :: Instanz
} catch (Exception e) {System.out.println(e);}
Zu ¨: Auf das Feld s kann nur im selben Package zugegriffen werden. Der Field-Zugriff mit getField() würde zu einer Ausnahme führen.
Für statische Felder ist das erste Argument besser immer null, einfach um Klarheit zu schaffen.
13.4.3 Ausführung von Methoden
Ausführung von Methoden mit
invoke()
Die Ausführung von Methoden erfolgt mit der Method-Methode:
Object invoke(Object obj, Object[] args)
throws IllegalAccessException,IllegalArgumentException, InvocationTargetException;
Wie bei den Feldern muss als erstes Argument die Instanz übergeben werden, auf der die Methode ausgeführt wird. Dieser Wert wird wieder ignoriert (sollte also null sein), wenn die Methode statisch ist.
Die Argument-Übergabe, insbesondere für primitive Typen, ist vom Konzept her gleich der der Konstruktoren (siehe Anlage von Objekten). Sollte die Methode ein Resultat liefern, wird es von invoke() zurückgeliefert (bei primitiven Typen wieder als Wrapper-Objekt).
Bei fehlerhafter Benutzung bzw. Ausführung wird eine der o.a. Ausnahmen ausgelöst.
Beispiel
Ausführen von StringBuffer-Methoden
Es werden die Operationen append(), setCharAt() und capacity() auf der Instanz sb der Klasse StringBuffer ausgeführt:
StringBuffer sb= new StringBuffer("Invoke");
Class c= StringBuffer.class;
try {
Method append= c.getMethod("append",
new Class[] {char[].class});
Method setCharAt= c.getMethod("setCharAt",
new Class[] {int.class, char.class});
setCharAt.invoke(sb,
¨
new Object[] {new Integer(0), new Character('i')});
sb= (StringBuffer) append.invoke(sb,
new Object[] {new char[] {'(',')'}});
System.out.println(sb); // ::
invoke()
System.out.println( // ::
22
c.getMethod("capacity",null).invoke(sb,
null)); ¦
} catch (Exception e) {System.out.println(e);}
Zu ¨ und ¦: Das invoke() in Zeile ¬ liefert als Resultat null. Primitive Typen müssen als Wrapper-Objekte übergeben werden. Da es bei capacity() keine Argumente gibt, kann in Zeile - zweimal null übergeben werden.
13.4.4 InvocationTargetException
Bei Fehlern im Reflexions-Mechanismus, die mit Ausnahmen bestraft werden, muss man zwei Arten unterscheiden.
Die erste ist bekannt. Man hat z.B. die Methode get() oder set() der Klasse Field oder getMethod() von Method mit falschen Argumenten aufgerufen bzw. man hat keine Zugriffserlaubnis. Die Antwort ist dann z.B. eine IllegalArgumentException oder IllegalAccessException.
Eine andere Art von Ausnahme ist aber weniger angenehm. Bei einem Konstruktor- bzw. Methoden-Aufruf kann eine Ausnahme von beliebiger Art (Error oder Exception) bei der Ausführung selbst auftreten. D.h., nicht die Methoden newInstance() bzw. invoke() der Klassen Constructor bzw. Method erzeugen die Ausnahme, sondern ihre Clients.
Invocation
TargetException: Wrapper für die wirkliche Ausnahme
|
Die InvocationTargetException ist ein spezieller Wrapper, der die Ausnahme, die in der Client-Methode auftritt, weitergibt. |
Die Client-Ausnahme erhält man dann mit der Methode
getTargetException()
Throwable getTargetException();
Beispiel
class TestInvokeTargetExc {
public static int foo(int i) {
if (i<0) throw new IllegalArgumentException("i<0!");
return (int) Math.sqrt(i);
}
}
Ein Test ergibt:
try {
System.out.println(
TestInvokeTargetExc.class.getMethods()[0].
invoke(null,new Object[] {new Integer(-1)}));
} catch (InvocationTargetException e) {
System.out.println(e.getTargetException());
} catch (Exception e) { System.out.println(e); }
Tabelle 13.3 Ausgabe zum Test-Code
java.lang.IllegalArgumentException: i<0!
|
13.4.5 Anlage von Arrays
Anlage von Arrays:
Array.newInstance()
In der Klasse Array existieren zwei Methoden zur Anlage von Arrays:
static Object newInstance(Class componentType, int[] dim)
throws IllegalArgumentException,
NegativeArraySizeException;
Die Methode ist für die Anlage mehrdimensionaler Arrays geeignet:
|
Ist das erste Argument componentType kein Array, gibt die Länge des int-Arrays die Anzahl der Dimensionen an, wobei die int-Elemente die Länge jeder Array-Dimension angegeben. |
|
Ist das erste Argument componentType selbst ein Array, so setzt sich das neu erschaffene Array aus den Dimensionen beider Argumente zusammen. |
static Object newInstance(Class componentType,
int length)
throws NegativeArraySizeException
Diese vereinfachte Version der ersten Methode wird zur Anlage eindimensionaler Arrays benutzt, bei denen componentType den Typ der Elemente repräsentiert, also selbst kein Array ist.
Beispiel
Anlage von ein- und mehrdimensionalen Arrays
Anlage der Arrays: String[5], double[1][2][3] und int[5][1..5]
try {
String[] sarr=(String[]) Array.newInstance(String.class,5);
System.out.println(sarr);
double[][][] d3= (double[][][]) Array.newInstance(
double.class, new int[] {1,2,3});
System.out.print(ArrayUtil.getDimension(d3.getClass())+"-dim");
System.out.println(ArrayUtil.getElementType(d3.getClass()));
// Anlage einer unteren Dreiecks-Matrix int[5][1..5]
int[][] i2dim= (int[][]) Array.newInstance(int[].class,
5);
for (int i= 0; i<i2dim.length;)
i2dim[i]= (int[]) Array.newInstance(int.class,++i);
for (int i=0;i<i2dim.length;i++) {
for (int j=0;j<i2dim[i].length;j++)
System.out.print(i2dim[i][j]+" ");
System.out.println();
}
} catch (Exception e) { System.out.println(e); }
Tabelle 13.4 Ausgabe zum Beispiel
[Ljava.lang.String;@273d3c
3-dim double
0
0 0
0 0 0
0 0 0 0
0 0 0 0 0
|
Mit [L wird nach der String-Darstellung für Arrays in 13.3.3 ein eindimensionales Array von Referenzen identifiziert.
Mit den beiden Methoden von ArrayUtil (aus Beispiel Array) werden die Anzahl der Dimensionen und der Element-Typ in der zweiten Zeile angezeigt. Es folgen alle int-Elemente der Dreiecksmatrix.
13.4.6 Lesen und Schreiben von Array-Komponenten
Es gibt zwei Arten von Getter-Methoden, eine allgemeine und eine spezielle für jeden primitiven Typ:8
Array.get()
static Object get(Object array, int index)
throws IllegalArgumentException,
ArrayIndexOutOfBoundsException;
|
Die Methode liefert als Ergebnis die Komponenten mit dem angegebenen Index und muss auch für mehrdimensionale Arrays verwendet werden, bei denen das Ergebnis wieder ein Array ist. |
Primitive Typen werden in Wrapper-Instanzen zurückgeliefert.
Array.get
<primitiveT>()
static <primitiveType> get<primitiveType>(Object
arr,int index);
|
Die Methode liefert als Ergebnis ein Element vom primitiven Typ mit dem angegebenen Index, d.h. ist nur für eindimensionale Arrays geeignet und führt – falls notwendig – eine widening Conversion aus (siehe 1.5.1 und Beispiel). |
Die Setter-Methoden sind analog:
Array.set()
Array.set <primitiveT>()
static void set(Object array, int index, Object value);
static void set<primitiveType> (Object array, int index,
<primitiveType> value);
Beispiel
Zuerst wird das Element sarr[1] eines String-Arrays gesetzt und gelesen, anschließend iarr[1][2] eines zweidimensionalen int-Arrays:
String[] sarr= {"s1",null,"s3"};
Array.set(sarr,1,"s2");
System.out.println(Array.get(sarr,1)); // ::
s2
int[][] iarr= {{1,2},{3,4,-5}};
Array.setInt(Array.get(iarr,1),2,5);
System.out.println(
Array.getInt(Array.get(iarr,1),2));
// :: 5
// Array.getDouble(Array.get(iarr,1),2));
// :: 5.0
Die letzte auskommentierte Zeile zeigt die automatische Konvertierung.
13.4.7 Unterstützende Array-Methoden
Array-Länge
Die Bestimmung der Länge eines Arrays mit array.length setzt voraus, dass der Array-Typ dem Compiler bekannt ist. Deshalb besitzt Array die Methode
Array.getLength()
static int getLength(Object array);
die dasselbe Ergebnis wie array.length liefert, wobei erst zur Laufzeit die Objekt-Referenz array ein Array-Objekt referenzieren muss.
Class-Instanz zur Array-Komponente
Array.
getComponentType()
Die Klasse Class enthält die Instanz-Methode
Class getComponentType();
die zu einem Array die Class-Instanz der Komponente zurückgibt.
Beispiel: Vergrößern eines Arrays
Da die Länge eines Arrays nach der Anlage fix ist, besteht eine Standardaufgabe darin, ein Array zu vergrößern, wobei natürlich die Werte aller Elemente erhalten bleiben müssen.
Prinzipiell muss dazu ein neues, größeres Array vom gleichen Typ erschaffen und die Werte der Elemente des alten in die des neuen Arrays kopiert werden.
Fügen wir diesen Service der Klasse ArrayUtil aus Array hinzu:
class ArrayUtil { // übernommen aus Beispiel
in Array
public static Class getElementType(Class cArray) {/*...*/}
public static int getDimension (Class cArray) {/*...*/}
Vergrößern eines Arrays
// factor gibt an, um wieviel das Array vergrößert
wird
// Kontrakt: factor-Wert liegt im Intervall (1.0;100.0]
public static Object expandArray(Object
array,double factor) {
if (!array.getClass().isArray() || factor <= 1.0)
throw new IllegalArgumentException(
"kein Array || factor<=1"); // besonders deskriptiv!
Object narray= Array.newInstance(
array.getClass().getComponentType(),
(int)(Array.getLength(array)*Math.min(factor,100.0)));
System.arraycopy()
System.arraycopy(array,0,narray,0,Array.getLength(array));
return narray;
}
}
Zur Implementation wurden die drei hervorgehobenen Methoden aus Class, System und Array verwendet.
13.5 Introspection und Kommandos
Klassifizierung von Methoden
Nach dem Klassen-Design-Prinzip besteht die öffentliche Schnittstelle einer Klassen nur aus Methoden. Man kann folgende Kategorien unterscheiden:
|
Getter- und Setter-Operationen, die – nach Bean-Konvention – auf die Properties (Eigenschaften) einer Klasse zugreifen, |
|
Operationen zu Events (Ereignissen), |
|
Operationen, spezifisch für den Service der Klasse. |
Natürlich enthält nicht jede Klasse Methoden aller drei Kategorien. Besitzt sie nur Getter/Setter, stellt sie eine geschützte Datenstruktur dar.9 10
Metainformationen
zu Methoden
|
Essenziell für Clients sind Metainformationen10 , die die Identifizierung, semantische Zuordnung und korrekte Ausführung von Methoden eines Servers sicherstellen. |
|
13.5.1 Inspektion mit Hilfe der Klasse Introspector
Klassen-Analyse mit dem Bean-Introspector
Bei der Identifizierung – und rudimentär vielleicht auch bei der semantischen (logischen) Bedeutung von Methoden – hilft die Klasse Introspector zusammen mit dem Interface BeanInfo aus dem Package java.beans.
|
Introspector enthält nur statische Methoden, kann nicht instanziiert werden und übernimmt die Analyse von Klassen, auch wenn diese keine Beans11 sind. |
getBeanInfo()
Die zentrale Methode – genauer drei ihrer Varianten – der Introspector-Klasse ist getBeanInfo(). Sie liefert als Ergebnis eine Referenz auf ein BeanInfo-Interface, welches auf Informationen zu allen public deklarierten Methoden bzw. Properties oder Events abgefragt werden kann.12
Beispiel
class MyInt {
private int i;
public int geti() { return i; }
public void seti(int i) { this.i=i; }
public int add(MyInt j) { return i+j.i; }
}
Die Klasse MyInt wird mit Hilfe von Introspector untersucht:
getMethodDescriptors()
try {
BeanInfo cinfo= Introspector.getBeanInfo(MyInt.class,
Object.class); ¨
MethodDescriptor[] md= cinfo.getMethodDescriptors();
for (int i= 0; i<md.length; i++) // setzt md!=null voraus!
System.out.println(md[i].getMethod());
getPropertyDescriptors()
getPropertyType() getReadMethod() getWriteMethod()
PropertyDescriptor[] pd= cinfo.getPropertyDescriptors();
for (int i= 0; i<pd.length; i++) { // setzt pd!=null voraus!
System.out.println(pd[i].getPropertyType() +"\n"+
pd[i].getReadMethod() +"\n"+
pd[i].getWriteMethod());
} catch (IntrospectionException e) { System.out.println(e); }
Zu ¨: Die Variante von getBeanInfo() gibt nur Informationen zur Klasse MyInt selbst, d.h., die Superklasse Object wird als »Stop«-Klasse ausgeschlossen.
Tabelle 13.5 Ausgabe zum Beispiel
public void kap13.MyInt.seti(int)
public int kap13.MyInt.geti()
public int kap13.MyInt.add(kap13.MyInt)
int
public int kap13.MyInt.geti()
public void kap13.MyInt.seti(int)
|
13.5.2 Kommandos
Die unter dem dritten Punkt der Klassifizierung als Service apostrophierten Methoden lassen sich weiter differenzieren.
Methoden-
Kategorie: Kommandos
Denn häufig bieten Klassen sehr generelle Operationen – als Kommandos bezeichnet – an, um ihren Dienst zu starten.
Kommandos
|
sind einfache, verständliche Dienste, in der Regel ohne oder mit einem Parameter. |
|
haben eine klassenübergreifende generelle Semantik, d.h. assoziieren eine klassenunabhängige einheitliche Bedeutung. |
Die folgenden Kommandos tauchen z.B. in vielen Klassen auf und werden etwa mit den dahinter stehenden Bedeutungen verwendet:
main() - Applikations-Start
exec() - Start einer Applikation von einer anderen
start() - Start eines Thread, eines Applets
run() - Start eines beliebigen Dienstes
execute() - Ausführung interaktiver (Menü-)Befehle
redo/undo() - (Rück-)Rücknahme interaktiver Befehle
Klassenunabhängigkeit
Die Klassen zu Kommandos
sind opak
Kommandos sind folglich unabhängig von Klassen bzw. Klassen-Hierarchien. Es können prinzipiell keine weiteren Annahmen über die Klassen gemacht werden, zu denen sie gehören.
13.6 Command-Pattern
Command-Pattern
Die Klassenunabhängigkeit (Kommandos) stellt gerade dann eine Herausforderung an ein klares Design dar, wenn folgende Situation vorliegt:
1. |
Die Anzahl der verschiedenen Kommando-Klassen bzw. -Dienste ist groß und unterliegt Änderungen bzw. Erweiterungen. |
2. |
Die Kommando-Objekte werden von vielen Client-Objekten benutzt. |
3. |
Die Kommados werden über einheitliche Namen identifiziert. |
Diese Situation tritt insbesondere bei interaktiven Applikationen auf, bei denen Kommandos an Menü-Aktionen bzw. GUI-Ereignisse gebunden sind. Besonders erschwert wird die Situation noch durch diese Forderung:
4. |
Mittels Undo- bzw. Redo-Kommandos können vorherige (beliebige) Kommandos rückgängig gemacht bzw. erneut ausgeführt werden. |
Bis zum dritten Punkt hilft das Command-Pattern, beim vierten muss es dann vom Memento-Pattern13 unterstützt werden.
13.6.1 Realisierung mittels Interfaces oder Reflexion
Realisierung: Interface vs.
Reflexion
Das Command-Pattern kann verschieden realisiert werden, unter anderem mit Hilfe von Interfaces oder auf Basis von Reflexion. Unabhängig vom konkreten Design werden jedoch folgende Ziele avisiert:
Design-Prinzipien zum Command-Pattern
|
Die Proliferation der verschiedenen konkreten Kommando-Klassen muss minimiert werden. |
Im Idealfall lädt also nur eine Factory die konkreten Klassen auf Anweisung der Clients und reicht dann opake Kommando-Objekte heraus.
|
Die Ausführung (Invokation) der auftretenden Kommandos wird an einen Kommando-Manager (CommandManager) delegiert. |
Dieses Design gewährleistet eine koordinierte Bearbeitung der Kommandos und ermöglicht erst eine Kommando-Historie, was eine notwendige Voraussetzung für die Implementation der vierten Forderung ist.
Im gesamten Client-/Manager-System werden also nur Objekte referenziert, die Kommandos ausführen können (Abb. 13.2).14
Umsetzung der Design-Prinzpien zum Command-Pattern
Abbildung 13.2 Command-Pattern (Basis: Interface oder Reflexion)
Die Klassen Client, Invoker und CommandManager benutzen nur Command-Objekte, die von einer CommandFactory erzeugt werden und von denen Kommandos, hier execute(), ausgeführt werden (Abb. 13.2).
Vergleichen wir zwei Arten der Command-Pattern-Implementation.
Command-Pattern:
interface-basiert
Command-Pattern, rein interface-basiert
Die CommandFactory reicht nur Objekte vom Typ ICommand heraus, von denen das restliche System Kommandos – hier execute() – ausführt.
Command-Pattern:
reflexions-basiert
Command-Pattern, reflexions-basiert
Die CommandFactory reicht nur Objekte vom Typ Object heraus. Das restliche System findet Kommandos – hier execute() – per Reflexion und führt sie dynamisch aus.
Fazit
Sicherheit vs.
Flexibilität
Im Interface-Fall prüft der Compiler, dass nur ICommand-»Objekte« verwendet werden, bei Reflexion kann dagegen ein Kommando zur Laufzeit noch untersucht werden, um es dann »passend« zu verwenden.
|
Stehen die Kommandos in der Signatur fest, ist die Interface-Lösung wesentlich besser, da typsicher und schneller.15 |
Beispiel
Code-Umsetzung der Design-Prinzpien zum Command-Pattern
»Schöne« Anwendungen des Command-Patterns tendieren zur Länge. Deshalb setzt das folgende Beispiel einfach nur das in Abb. 13.2 gezeigte Klassen-Muster für beide Implementationen um.
Dadurch, dass im CmdManager die Interface-Lösung direkt vor der Reflexion steht, wird das o.a. Fazit – die Vor- und Nachteile beider Techniken – klarer.
Die CmdFactory muss dazu das Kommando-Objekt als Typ Object zurückgeben, damit es gleichermaßen als ICommand oder per Reflexion benutzt werden kann.
Command-
Interface
interface ICommand
{ void execute(); }
Command-Klassen
class Cmd1 implements
ICommand {
public void execute() { System.out.println("Cmd1"); }
}
class Cmd2 implements
ICommand {
public void execute() { System.out.println("Cmd2"); }
}
// Cmd3 wird mittels Reflexion genutzt, es existieren
drei
// execute-Methoden mit unterschiedlicher Signatur
class Cmd3 {
public void execute() { System.out.println("Cmd3"); }
public void execute(String s) {
System.out.println("Cmd3: "+s);
}
public void execute(Integer i) {
System.out.println("Cmd3: "+i.intValue()*i.intValue());
}
}
Command-Factory
class CmdFactory {
public static Object create (String cmd) {
if (cmd.equals("Cmd1")) return new Cmd1();
else if (cmd.equals("Cmd2")) return new Cmd2();
else if (cmd.equals("Cmd3")) return new Cmd3();
else throw new IllegalArgumentException();
}
}
Command-Manager
class CmdManager {
public void invoke (Object cmd, Object arg) throws Exception {
// Objekte vom Typ ICommand werden direkt ausgeführt
if (cmd instanceof ICommand) ((ICommand)cmd).execute();
// ansonsten per Reflexion etwas Passendes suchen!
else {
if (arg==null)
cmd.getClass().getMethod("execute",null).invoke(cmd,null);
else {
Method[] marr= cmd.getClass().getMethods();
int i= 0;
do {
if (marr[i].getName().equals("execute")) {
Class[] carr= marr[i].getParameterTypes();
if (carr.length==1 &&
carr[0].isAssignableFrom(arg.getClass())){
marr[i].invoke(cmd,new Object[] {arg} );
return;
}
}
i++;
} while (i< marr.length);
throw new IllegalArgumentException("falsche Signatur");
}
}
}
}
Command-
Client/Invoker
class ClientInvoker
{
public static void main(String[] args) {
try {
new CmdManager().invoke(CmdFactory.create("Cmd1"),null);
new CmdManager().invoke(CmdFactory.create("Cmd2"),null);
new CmdManager().invoke(CmdFactory.create("Cmd3"),null);
new CmdManager().invoke(CmdFactory.create("Cmd3"),"StringArg");
new CmdManager().invoke(CmdFactory.create("Cmd3"),new Integer(2));
new CmdManager().invoke(CmdFactory.create("Cmd3"),new Double(2.));
} catch (Exception e) { System.out.println(e); }
}
}
13.7 Dynamisches Proxy-Pattern
13.7.1 Allgemeine Eigenschaften
Proxy-Pattern
Das Proxy-Pattern ist in seiner statischen Version wohl bekannt und ist für das Design vieler Programme unentbehrlich. In Abschnitt Proxy wurden verschiedene Arten von Aufgaben vorgestellt, bei denen Proxies eingesetzt werden.
Das Proxy-Pattern selbst ist vom Front-End-Design her recht einfach.
Alle Clients führen nicht direkt auf den Objekten ihre Operationen aus, sondern nur indirekt über ein Ersatzobjekt, dem Proxy.
In der normalen statischen Version implementiert das Proxy dazu ein fest definiertes Service-Interface, sodass der Compiler prüfen kann, ob die Clients das Proxy mit den korrekten Operationen aufrufen.
Das Proxy führt dann zur Laufzeit die Methoden derjenigen Objekte aus, für die es die Ersatzfunktion wahrnimmt, d.h., die Methoden-Aufrufe der Clients werden an die Original-Objekte weitergereicht und eventuelle Ergebnisse wieder an die Clients zurückgegeben.
Da die Clients ein Proxy immer über die Methoden eines Interfaces nutzen, ist das Objektgebilde hinter dem Proxy nicht sichtbar und an sich irrelevant für die Clients.
Genau dies macht das Proxy-Pattern so machtvoll und vielfältig im Einsatz.
13.7.2 Dynamische Proxy-Variante
Dynamisches
Proxy-Pattern
Von der statischen Version kann man noch einen kleinen Schritt weitergehen, indem man das Service-Interface nicht vorher angibt, sodass es der Compiler prüfen kann, sondern erst zur Laufzeit.
Damit verbindet man die Typsicherheit, die ein Interface bietet mit dem extremen Vorteil, erst zur Laufzeit dynamisch den Service festlegen oder wechseln zu können.
Die Auswirkungen dieses dynamischen Proxy-Patterns auf das traditionelle Design (von Proxies) sind aufgrund dieser eigentlich kleinen Änderung gewaltig.16 Im Folgenden soll die Implementation dieses Patterns in der Plattform besprochen werden.
13.7.3 Dynamische Proxy-Implementation
Wie bereits in Proxy dargelegt, wird ein Proxy-Objekt mit Hilfe der statischen Methode Proxy.newProxyInstance() angelegt.
Die Methode lässt die Wahl eines ClassLoaders offen und kann praktisch beliebig viele Interfaces akzeptieren, die das neu erschaffene Proxy-Objekt dann implementiert (und akzeptiert).
Nachfolgend wird eine vereinfachte Variante benutzt, die als ClassLoader immer eines der Interfaces benutzt, für das das Proxy-Objekt steht:
newProxyInstance()
(ProxyInterfaceX) proxyObj=
(ProxyInterfaceX) newProxyInstance(
ProxyInterfaceX.class.getClassLoader(),
new Class[] {ProxyInterface1,...,ProxyInterfaceN},
invocationHandler);
Das Interface AnyProxyInterfaceX steht dabei für irgendeines der Interfaces ProxyInterface1,...,ProxyInterfaceN.
Wird nun von einem Client des neu erschaffenen Proxy-Objekts eine Methode m() des Interfaces ProxyInterfaceX aufgerufen, wird diese an das invocationHandler-Objekt über die Methode invoke() weitergereicht.
invocationHandler
Erst der invocationHandler entscheidet, wie er die Methode m() mit seinen Argumenten an die Original-Objekte weiterreicht und welche Methode dann ausgeführt werden soll.
Der invocationHandler ist also eine weitere Indirektionsstufe, um das Proxy nicht direkt an die Objekte binden zu müssen, die es vertritt.
Aus dieser Konstruktion leiten sich ein paar einfache Regeln ab:
Proxy-Regeln
|
Es sind nur Interfaces erlaubt. |
|
Alle Interfaces dürfen nur genau einmal aufgeführt werden. |
|
Alle Interfaces müssen vom ClassLoader geladen werden können. |
|
Nicht public deklarierte Interfaces müssen im Package der Proxy-Klasse liegen. |
|
Die Interfaces dürfen keine Methoden mit gleicher Signatur, aber unterschiedlichem Rückgabe-Typ enthalten. |
Abschließend soll ein Beispiel für ein Broker-Proxy gegeben werden.
13.7.4 Beispiel Broker- bzw. Façade-Proxy
Beispiel:
Broker- bzw. Façade-Proxy für Teile
Das Proxy-Pattern soll anhand einer Teile-Hierarchie, die nur auf Interfaces beruht, demonstriert werden. Der Hauptgrund für das Beispiel liegt in der Teile-Problematik und einem zugehörigen Factory-Beispiel, das bereits in 6.11.1 vorgestellt wurde.
Die folgende Proxy-Variante zeigt die radikalen Auswirkungen auf das Design.
Aufgabe
Nur Interfaces für die Klienten
1. |
Die Klienten sehen die Teile nur als Interfaces in einer Hierarchie, die Mehrfachvererbung enthält. |
Teile-Klassen nur lose gekoppelt
2. |
Die Hierarchie der Teile-Klassen nach völlig anderen Gesichtspunkten aufgebaut. Die Teile-Klassen können z.B. eine andere oder überhaupt keine Hierarchie bilden (um relationale Tabellen ein RDBMS zu kapseln). |
Vierstufige
Architektur
3. |
Die Architektur ist vierstufig: |
|
|
Klienten: Sie kennen nur die Teile-Interfaces, den Broker und haben keine Möglichkeit, die eigentlichen Teile-Klassen zu benutzen (Façade-Pattern). |
|
|
Broker: Er kennt nur den Invocation-Handler für den Teile-Service, d.h. reicht alles an ihn weiter. Er hält für die Klienten noch eine Kollektion aller Teile bereit (natürlich über ein entsprechendes Interface). |
|
|
Invocation-Handler: Er kennt die Klassen und muss letztendlich den Service der Interface-Hierarchie auf real existenten Teile-Klassen verteilen. Somit schlagen Änderungen in der Klassen-Struktur auf den Handler durch (aber nicht weiter!). |
|
|
Teile-Hierarchie: Sie leisten letztendlich den Service. |
Zur Realisierung: minimalistisches Prinzip
Realisierung, Implementation
Damit die Wirkung des Proxies nicht im Code untergeht, beschränkt sich das Design auf das absolut notwendige:
|
Zur Datenhaltung werden nur transiente Kollektionen aus java.util verwendet (siehe hierzu nächstes Kapitel). |
|
Es wird keine Fehler- oder Ausnahmebehandlung implementiert. |
|
Da selbst bei einem schlichten Design der Überblick verloren gehen kann, wird der Code von UML-Diagrammen bzw. Abbildungen unterstützt. |
Interface-Hierarchie zu Teile
Zuerst wird die (minimalistische) Interface-Hierarchie für die Klienten vorgestellt:
ITeile: Container
// --- Container für alle Teile ---
interface ITeile {
ITeil getTeil(String id);
void setTeil(ITeil teil);
Collection getTeile(); // immutable,
nur zum Iterieren
void dumpTeile(); // einfache Konsol-Darstellung
}
ITeil:
generelles Teil
// --- minimales allgemeines Interfaces aller Teile
---
interface ITeil {
String getIdent();
ITeile getOberTeile();
void setOberTeil(ITeil teil);
}
IBaugruppe: besteht aus weiteren Teilen
// --- Baugruppen bestehen immer aus Teilen ---
interface IBaugruppe extends ITeil {
// Alle Unterteile einer Baugruppe(BG). Nicht
rekursiv!
// Nur Teile, die direkt in der BG enthalten sind
ITeile getUnterTeile();
void setUnterTeil(ITeil teil);
}
IVKTeil:
Verkaufsteil
// --- nur Verkaufsteile haben einen VK-Preis ---
interface IVKTeil extends ITeil {
double getPreis();
void setPreis(double preis);
}
IErzeugnis:
Endprodukt
// --- Erzeugnis sind spezielle Baugruppen mit Verkaufspreis
---
interface IErzeugnis extends IVKTeil,IBaugruppe
{
String getProductInfo();
void setProductInfo(String s);
}
Wer das Decorator-Pattern verinnerlicht hat, erkennt das folgende Hierarchieproblem. Sofern gewissen Spezialisierungen unabhängig voneinander sind, führen sie zu einer kombinatorischen Explosion der Klassen.
Im UML-Diagramm (Abb. 13.3) fällt der Nachteil des Designs besonders auf, denn:
|
Zu jeder Spezialisierung eines Teils (ob Erzeugnisse, Baugruppen, Variantteile, Einzelteile, Material, ...) kann es wieder eine Spezialisierung Verkaufsteil geben.17 |
Es kann z.B. durchaus Baugruppen geben, die verkauft werden müssen, was aber in der Hierarchie nicht vorgesehen ist.
Interface-Hierarchie und zugehörige Klassen
Abbildung 13.3 Interface- und Klassen-Hierarchie zu Teile
Wie zu erkennen, ist die Klassen-Hierarchie fast keine und implementiert auch das Interface IErzeugnis überhaupt nicht.
Aufgabe des Broker-Proxies und Invocation-Handler
Da die Interface- und Klassen-Hierarchie nicht mehr zurückgenommen werden kann (schon ausgeliefert!), ist es Aufgabe des
|
Broker-Proxies, Invocation-Handler und die Klassen vor den Klienten zu verbergen |
|
Broker-Proxies in Zusammenarbeit mit dem Invocation-Handler, den Klienten auch Baugruppen, die Verkaufsteile sind, nachträglich anzubieten und natürlich Erzeugnisse zu emulieren. |
Der erste Punkt wird durch private deklarierte statische innere Klassen gelöst.18
Klassen zur
Implementation
Abbildung 13.4 Implementations-Details
Details zum Code
Zu den Implementations-Details (Abb. 13.4):
1. |
BrokerProxy enhält nur zwei statische Methoden neuTeil() und getTeile(). Die Methode neuTeil() liefert für die Klienten Objekte, die sich |
|
|
wie Instanzen der vier Interfaces ITeil,...,IErzeugnis verhalten |
|
|
wie eine Instanz von IBaugruppe und IVKTeil verhält |
2. |
BrokerProxy übergibt die entsprechenden Class-Instanzen der Interfaces an Proxy und den InvocationHandler bzw. THandler. |
3. |
Instanzen von THandler kapseln ein Objekt von Teil bzw. VKTeil, nehmen die Methodenaufrufe mittels invoke() entgegen und leiten sie um oder im Fall von get/setProductInfo() installieren in selbst.19 |
4. |
BrokerProxy und Teil benutzen die Klasse Teile nur als Factory über die Methode newInstance(). ITeile wird dann als Container für alle Teile bzw. Ober- und Unterteile von Baugruppen verwendet. |
BrokerProxy
class BrokerProxy {
// der innere Teile-Container
private static ITeile tMap= Teile.newInstance();
// --- Teil ---
Teil
private static class Teil
implements ITeil, IBaugruppe {
private String id;
private ITeile oMap= Teile.newInstance();
public Teil (String id) { this.id=
id; }
public String getIdent() { return id; }
public ITeile getOberTeile() { return oMap; }
public void setOberTeil(ITeil teil) {
if (teil!=null) oMap.setTeil(teil);
}
public ITeile getUnterTeile() {
ITeile u= Teile.newInstance();
ITeil t;
for (Iterator i= tMap.getTeile().iterator();
i.hasNext();) {
t= (ITeil)i.next();
if (t.getOberTeile().getTeil(this.id)!=null)
u.setTeil(t);
}
return u;
}
public void setUnterTeil(ITeil teil) {
if (teil!=null) teil.setOberTeil(this);
}
}
// --- Verkaufsteil ---
VKTeil
private static class VKTeil
extends Teil implements IVKTeil {
private double preis;
public VKTeil (String id, double preis) {
super(id); setPreis(preis);
}
public double getPreis() { return preis; }
public void setPreis(double preis) { this.preis= preis; }
}
// --- Teile-Container ---
Teile
private static class Teile implements ITeile {
private SortedMap tMap= new TreeMap();
private Teile() {};
static ITeile newInstance() {return new Teile();
}
public void dumpTeile() {
System.out.println(tMap.keySet());
}
public ITeil getTeil(String id) {
return (ITeil)tMap.get(id);
}
public void setTeil(ITeil teil) {
tMap.put(teil.getIdent(),teil);
}
public Collection getTeile() {
// immutable Wrapper, siehe hierzu auch 14.6.2
return Collections.unmodifiableCollection(tMap.values());¨
}
}
// --- Invocation-Handler ---
THandler
private static class THandler implements InvocationHandler
{
private Teil t;
private String info;
public THandler(String id, Class[] c) {
if (c[0]==ITeil.class || c[0]== IBaugruppe.class)
t= new Teil(id);
else t= new VKTeil(id, Double.NaN);
//kein gültiger Preis
}
public Object invoke(Object proxy, Method m,
Object[] args) throws Throwable {
if (m.getName().equals("setProductInfo")) { ¦
info= (String) args[0]; return null;
}
else if (m.getName().equals("getProductInfo")) Æ
return info;
else return m.invoke(t,args);
Ø
}
}
// --- BrokerProxy ---
BrokerProxy:
getTeile()
public static ITeile getTeile() {
return tMap;
}
BrokerProxy:
neuTeil()
public static Object neuTeil(String art, String
id) {
Class[] c;
if (art.equals("E"))
c= new Class[] {IErzeugnis.class};
else if (art.equals("V"))
c= new Class[] {IVKTeil.class};
else if (art.equals("B"))
c= new Class[] {IBaugruppe.class};
// abweichend von der Interface-Hierarchie wird
// ein Objekt erschaffen, dass sich wie eine
// Instanz von von IBaugruppe und IVKTeil verhält
else if (art.equals("BV"))
c= new Class[] {IVKTeil.class, IBaugruppe.class};
else
c= new Class[] {ITeil.class};
newProxyInstance()
ITeil t= (ITeil) Proxy.newProxyInstance(
c[0].getClassLoader(),
c, new THandler(id,c));
tMap.setTeil(t);
return t;
}
}
Erklärung zum Code
zu ¨: Eine Kollektion auszuliefern, die man mühsam aufgebaut hat und die dann von der Klienten zerstört wird, ist nicht besonders schön. Hier bietet sich ein Decorator bzw. Wrapper an, der alles immutable hält.
zu ¦,Æ: Ohne Fehlerbehandlung, nur auf ein Minimum reduziert, wird hier der Service der »imaginären« Erzeugnis-Klasse im THandler selbst implementiert.
zu Ø: Ansonsten wird einfach alles per Reflexion weitergeleitet.
Damit ist die Implementation beendet. Allerdings ist die Benutzung durch die Klienten nicht uninteressant.
Im folgenden heisst der Klient natürlich Test und baut eine kleine Erzeugnisstruktur auf, deren Gozinto-Graph in Abb. 13.5 dargestellt ist.
Beispiel:
Zwei Erzeugnisse
Abbildung 13.5 Gozinto-Graph zu zwei Erzeugnissen
Test: der Klient
public class Test {
public static void main(String[] args) {
// holt Teile-Container vom Proxy
ITeile teile= BrokerProxy.getTeile();
// im folgenden wird nur die Erzeugnisstruktur
aufgebaut
E: Erzeugnis
((IErzeugnis)BrokerProxy.neuTeil("E","E1")).setPreis(1000.00);
B: Baugruppe
((IBaugruppe)teile.getTeil("E1")).setUnterTeil(
(ITeil) BrokerProxy.neuTeil("B","B1"));
T: Teil
((ITeil)BrokerProxy.neuTeil("T","T1")).
setOberTeil(teile.getTeil("B1"));
V: Verkaufsteil
((ITeil) BrokerProxy.neuTeil("V","T2")).
setOberTeil(teile.getTeil("B1"));
((IVKTeil) teile.getTeil("T2")).setPreis(100.00);
((IErzeugnis)BrokerProxy.neuTeil("E","E2")).setPreis(2000.00);
((IBaugruppe)teile.getTeil("E2")).setUnterTeil(
(ITeil) BrokerProxy.neuTeil("BV","B2"));
BV: Baugruppe +
Verkaufsteil
((IBaugruppe)teile.getTeil("E2")).
setUnterTeil(teile.getTeil("B1"));
teile.getTeil("T1").setOberTeil(teile.getTeil("B2"));
// alle Teile auf der Konsole ausgeben
System.out.print("Alle Teile: "); teile.dumpTeile();
// diverse Unter- bzw. Oberteile auf der Konsole
ausgeben
System.out.print("Unterteile zu " +
teile.getTeil("E2").getIdent() + ": ");
((IBaugruppe)teile.getTeil("E2")).getUnterTeile().dumpTeile();
System.out.print("Unterteile zu " +
teile.getTeil("B1").getIdent() +": ");
((IBaugruppe)teile.getTeil("B1")).getUnterTeile().dumpTeile();
System.out.print("Oberteile zu "+
teile.getTeil("T1").getIdent() +": ");
teile.getTeil("T1").getOberTeile().dumpTeile();
// die Summe aller Verkaufspreise ermitteln
immutable
Kollektion
double sum= 0.0;
Object t;
for (Iterator i= teile.getTeile().iterator(); i.hasNext(); )
if ((t= i.next()) instanceof IVKTeil) {
if (!Double.isNaN(((IVKTeil)t).getPreis())) // Vorsicht!
sum+= ((IVKTeil)t).getPreis();
//i.remove(); geht nicht, ist immutable
}
System.out.println(sum);
// Erzeugnis-Info setzen und wieder lesen
((IErzeugnis)teile.getTeil("E1")).setProductInfo("Info
zu E1");
System.out.println(((IErzeugnis)teile.getTeil("E1")).
getProductInfo());
}
}
Konsolausgabe zum Test
Tabelle 13.6 Ausgabe zu Test
Alle Teile: [B1, B2, E1, E2, T1, T2]
Unterteile zu E2: [B1, B2]
Unterteile zu B1: [T1, T2]
Oberteile zu T1: [B1, B2]
3100.0
Info zu E1
|
Fazit
Typsicherheit + Dynamik
(minus Laufzeitkosten)
Dieses Proxy-Pattern kombiniert die Stärken der Typsicherheit von Interfaces mit der Dynamik der Reflexion zu dem Preis. Der Preis, den man dafür zahlt, heisst Laufzeitkosten.
Bei Zugriffen auf eine DBMS im Applikationsserver kann das toleriert werden.
13.8 Zusammenfassung
Reflexion stellt einen neuen, evolutionären Schritt nach den Interfaces in der OO-Entwicklung dar.
Reflexion ist in Verbindung mit der Analyse unbekannter Objekte und dem dynamischen Laden von Klassen, die erst zur Laufzeit bekannt sind, ein unentbehrlicher Mechanismus.
Außer Object und Class, den Ausgangspunkten der Reflexion, sind alle relevanten Klassen im Package java.lang.reflect zusammengefasst. Für jede Aufgabe der Manipulation eines unbekannten Objekts gibt es spezialisierte Klassen.
Zur Member-Manipulation stehen Constructor, Field und Method zur Verfügung, für Arrays die Klasse Array. Modifier können mit Hilfe der Klasse Modifier identifiziert werden.
Anhand vieler Code-Fragmente werden alle Bereiche der Manipulation behandelt. Abschließend wird das Command-Pattern als Alternative Interface vs. Reflexion und das dynamische Proxy-Pattern als Symbiose von Interface- und Reflexions-Strategie vorgestellt.
1 Die Betonung liegt auf »gravierend«, da er ja nur Syntax-Prüfungen vornimmt und nicht etwa notwendige Constrains zu den Interface-Methoden prüft (siehe auch Kapitel 14, Collection-Framework).
2 B2B: Business to Business, B2C: Business to Customer.
3 <X> steht für Name, Package, Field etc.
4 Mit securityManager.checkPermission(ACCESS_PERMISSION), wobei ACCESS_
PERMISSION eine Instanz von ReflectPermission ist (Details siehe Klasse
AccessibleObject).
5 <Modifier> wird durch den Namen des Modifikators ersetzt.
6 Siehe hierzu auch das Broker-Pattern in 12.4.4.
7 Eine ZIP-Datei, mit einem Eintrag manifest.mf und den kompilierten
Klassen *.class.
8 Die Ausnahmen werden nur bei der ersten Methode explizit angegeben, sie sind für alle anderen gleich.
9 record in Pascal, struct in C++ oder row in einer RDBMS.
10 Korrekt im Sinn von Einhaltung aller Constrains.
11 Klassen werden durch Einhaltung gewisser Design-Richtlinien ihrer Schnittstellen zu Beans. Gerade durch die Einhaltung dieser Bean-Konventionen können sie als Komponenten eingesetzt werden.
12 Diese Unterscheidungen beruhen auf Bean-Konventionen.
13 Das Memento-Pattern erfasst den Zustand eines Objekts, bevor die Kommando-Operation eine Änderung verursacht und kapselt ihn so in ein Objekt, dass dieses später zur Undo-Operation verwenden kann. Es wird hier nicht weiter besprochen.
14 Der Einfachheit halber beschränken wir uns in der Darstellung auf ein Kommando mit Namen execute().
15 Schneller deshalb, weil Reflexions-Mechanismen Zeit kosten.
16 Sie sind leider im Rahmen dieses Kapitels nicht darstellbar.
17 Spezialisierungen sind halt häufig kombinierbar, da orthogonal. Deshalb gibt es ja auch das Decorator-Pattern.
18 An sich reichte auch Default-Zugriff nur auf Package-Ebene, aber sicher ist sicher.
19 Für persistente Datenhaltung nicht unbedingt zu empfehlen, aber als Demonstration ganz gut.
|