Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


Java 2 von Friedrich Esser
Designmuster und Zertifizierungswissen
Zum Katalog
gp Kapitel 13 Reflexion
  gp 13.1 Klasse, Interface und Reflexion
  gp 13.2 Übersicht über Reflexion
  gp 13.3 Inspektion von Klassen und Interfaces
  gp 13.4 Manipulation von Klassen und Arrays
  gp 13.5 Introspection und Kommandos
  gp 13.6 Command-Pattern
  gp 13.7 Dynamisches Proxy-Pattern
  gp 13.8 Zusammenfassung

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


Galileo Computing

13.1 Klasse, Interface und Reflexion  downtop

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:

gp  Das Interface-Konzept bietet Typsicherheit. Der Compiler ist aufgrund der Typprüfung in der Lage, gravierende Fehler frühzeitig abzufangen.

Reflexion

Im Zeitalter globaler, firmenübergreifender Netze kommunizieren (B2B- bzw. B2C-)Applikationen 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

gp  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

gp  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

Icon

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

gp  Kann der erforderliche Service auch auf Basis von Interfaces realisiert werden, sind sie einem Konzept, das auf Reflexion basiert, vorzuziehen.

Galileo Computing

13.2 Übersicht über Reflexion  downtop

Bevor wir auf interessante Einsatzgebiete zu sprechen kommen, werden in den folgenden Abschnitten zuerst alle Reflexions-Techniken vorgestellt.


Galileo Computing

13.2.1 Package java.lang.reflect  downtop

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
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.


Galileo Computing

13.2.2 Object und Class  downtop

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.

gp  Zu jeder geladenen Klasse bzw. jedem Interface existiert zur Laufzeit genau eine Instanz von Class in der JVM.
gp  Die Klasse Class ist immutable und man kann keine Class-Instanzen erschaffen (No-Arg-Konstruktor ist private).
gp  Eine Referenz zur Class-Instanz erhält man bei

TYPE
getClass() .class

    gp  primitiven Typen: mit Hilfe der Konstanten TYPE der zugehörigen Wrapper-Klasse,
    gp  Objekten: mit Hilfe der Object-Methode getClass(),
    gp  Klassen: durch Anfügen von .class an den Klassennamen.

forName():
Zugriff auf eine Klasse über ihren Namen

gp  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.
gp  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>()-Methoden von Class in allen Details untersucht werden.


Galileo Computing

13.2.3 Constructor, Field und Method  downtop

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

gp  Constructor legt Instanzen an.
gp  Field greift (lesend/schreibend) auf Feldwerte zu.
gp  Method ist für die Ausführung von Methoden zuständig.

AccessibleObject

Die Superklasse AccessibleObject definiert nur drei Methoden.

Zugriffsüberprüfung und
-erlaubnis

gp  setAccessible() versucht durch das Argument true eine Zugriffserlaubnis für ein Member zu setzen, auch wenn es nicht im normalen Zugriff steht.
gp  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.

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:

gp  getDeclaringClass() liefert das Class-Object, zugehörig zur Klasse des Members.
gp  getModifiers() liefert ein Bit-Muster als int-Wert, das alle zugehörigen Modifikatoren des Members identifiziert (siehe Modifier).
gp  getName() liefert den Namen des Members.

Galileo Computing

13.2.4 Modifier  downtop

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>() kann dann getestet werden, ob der Modifikator für das Member gesetzt ist (z.B.: isAbstract())


Galileo Computing

13.2.5 Array  downtop

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

gp  static Object newInstance(Class compType, int length)
gp  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:

gp  get<primitiveType>() bzw. set<primitiveType>()

Beispiel:

public static long getLong(Object array, int index)

public static void setLong(Object array,int index,long l)


Galileo Computing

13.2.6 Proxy  downtop

Proxy: Stellvertreter mit
verschiedenen Aufgaben

Die Bezeichnung »Proxy« deutet schon auf ein gleich lautendes, grundlegendes Pattern hin, das in Variationen auftreten kann.

gp  Mit Proxy bezeichnet man ein Stellvertreter-Objekt für ein Original-Objekt, welches nicht im direkten Zugriff steht.

Ein Proxy ist ein

Remote

gp  Remote-Proxy, sofern es ein lokaler Stellvertreter für ein Objekt ist, das nur über ein Netz(protokoll) erreichbar ist.

Access

gp  Access-Proxy, sofern es die wahren Objekte kapselt, um den direkten Zugriff zu blockieren.

Broker/Façade

gp  Broker/Façade-Proxy, sofern es (je nach Methoden-Aufruf) die Objekte selektiert, die den Service wirklich ausführen.

Virtual

gp  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

gp  die übergebenen spezifizierten Interfaces implementiert,
gp  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;

Galileo Computing

13.3 Inspektion von Klassen und Interfaces  downtop

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.


Galileo Computing

13.3.1 Class-Objekt  downtop

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

Galileo Computing

13.3.2 Methoden get<X> vs. getDeclared<X>  downtop

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

gp  get<X> beziehen sich auf die gesamte Hierarchie,
gp  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


Galileo Computing

13.3.3 Package, Superklassen und Interfaces  downtop

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

gp  Sofern die Klasse aus einer JAR-Datei 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
    gp  primitiv: Z (boolean), B (byte), C (char), S (short), I (int),
J (long), F (float), D (double).
    gp  Referenz: L mit angehängtem Klassennamen.

Galileo Computing

13.3.4 Konstruktoren und Modifikatoren  downtop

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[])

Galileo Computing

13.3.5 Felder  downtop

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.


Galileo Computing

13.3.6 Methoden  downtop

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; }

Galileo Computing

13.3.7 Array  downtop

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:

gp  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;
  }
}

Galileo Computing

13.4 Manipulation von Klassen und Arrays  downtop

Nach Inspektion einer Klasse können Konstruktoren, Felder und Methoden prinzipiell genauso genutzt werden wie diejenigen von Klassen, die zur Übersetzungszeit bekannt sind.


Galileo Computing

13.4.1 Anlage von Objekten  downtop

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:

gp  Argumente von primitiven Typen müssen mit Hilfe von Wrapper-Instanzen übergeben werden.

Galileo Computing

13.4.2 Lesen und Schreiben von Feldern  downtop

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

gp  Instanz-Methoden: Es wird die Instanz übergeben, für die die Feld-Operation ausgeführt werden soll.
gp  statischen Methoden: Das Argument wird ignoriert, es sollte deshalb null sein.

Zusätzlich gilt für

gp  Getter-Methoden: Das Resultat ist entweder ein Objekt oder bei den Varianten direkt der Wert des primitiven Typs.
gp  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.


Galileo Computing

13.4.3 Ausführung von Methoden  downtop

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.


Galileo Computing

13.4.4 InvocationTargetException  downtop

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

gp  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!


Galileo Computing

13.4.5 Anlage von Arrays  downtop

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:

gp  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.
gp  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.


Galileo Computing

13.4.6 Lesen und Schreiben von Array-Komponenten  downtop

Es gibt zwei Arten von Getter-Methoden, eine allgemeine und eine spezielle für jeden primitiven Typ:

Array.get()

static Object get(Object array, int index)
                     throws IllegalArgumentException, 
                            ArrayIndexOutOfBoundsException;
gp  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);
gp  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.


Galileo Computing

13.4.7 Unterstützende Array-Methoden  downtop

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.


Galileo Computing

13.5 Introspection und Kommandos  downtop

Icon
Klassifizierung von Methoden

Nach dem Klassen-Design-Prinzip besteht die öffentliche Schnittstelle einer Klassen nur aus Methoden. Man kann folgende Kategorien unterscheiden:

gp  Getter- und Setter-Operationen, die – nach Bean-Konvention – auf die Properties (Eigenschaften) einer Klasse zugreifen,
gp  Operationen zu Events (Ereignissen),
gp  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. 10 

Metainformationen
zu Methoden

gp  Essenziell für Clients sind Metainformationen10  , die die Identifizierung, semantische Zuordnung und korrekte Ausführung von Methoden eines Servers sicherstellen.


Galileo Computing

13.5.1 Inspektion mit Hilfe der Klasse Introspector  downtop

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.

gp  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)


Galileo Computing

13.5.2 Kommandos  downtop

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

gp  sind einfache, verständliche Dienste, in der Regel ohne oder mit einem Parameter.
gp  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.


Galileo Computing

13.6 Command-Pattern  downtop

Icon
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.


Galileo Computing

13.6.1 Realisierung mittels Interfaces oder Reflexion  downtop

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

gp  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.

gp  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
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.

gp  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); }
 }
}

Galileo Computing

13.7 Dynamisches Proxy-Pattern  downtop


Galileo Computing

13.7.1 Allgemeine Eigenschaften  downtop

Icon
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.


Galileo Computing

13.7.2 Dynamische Proxy-Variante  downtop

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.


Galileo Computing

13.7.3 Dynamische Proxy-Implementation  downtop

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.

Icon

Aus dieser Konstruktion leiten sich ein paar einfache Regeln ab:

Proxy-Regeln

gp  Es sind nur Interfaces erlaubt.
gp  Alle Interfaces dürfen nur genau einmal aufgeführt werden.
gp  Alle Interfaces müssen vom ClassLoader geladen werden können.
gp  Nicht public deklarierte Interfaces müssen im Package der Proxy-Klasse liegen.
gp  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.


Galileo Computing

13.7.4 Beispiel Broker- bzw. Façade-Proxy  downtop

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:
    gp  Klienten: Sie kennen nur die Teile-Interfaces, den Broker und haben keine Möglichkeit, die eigentlichen Teile-Klassen zu benutzen (Façade-Pattern).
    gp  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).
    gp  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!).
    gp  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:

gp  Zur Datenhaltung werden nur transiente Kollektionen aus java.util verwendet (siehe hierzu nächstes Kapitel).
gp  Es wird keine Fehler- oder Ausnahmebehandlung implementiert.
gp  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:

gp  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
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

gp  Broker-Proxies, Invocation-Handler und die Klassen vor den Klienten zu verbergen
gp  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
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
    gp  wie Instanzen der vier Interfaces ITeil,...,IErzeugnis verhalten
    gp  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
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.


Galileo Computing

13.8 Zusammenfassung  toptop

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.

  

Perl – Der Einstieg




Copyright © Galileo Press GmbH 2001 - 2002
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken und speichern. Ansonsten unterliegt das <openbook> denselben Bestimmungen wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.
Die Veröffentlichung der Inhalte oder Teilen davon bedarf der ausdrücklichen schriftlichen Genehmigung von Galileo Press. Falls Sie Interesse daran haben sollten, die Inhalte auf Ihrer Website oder einer CD anzubieten, melden Sie sich bitte bei: stefan.krumbiegel@galileo-press.de


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, fon: 0228.42150.0, fax 0228.42150.77, info@galileo-press.de