Titel | Inhalt | Suchen | Index | API | Go To Java 2, Zweite Auflage, Handbuch der Java-Programmierung |
<< | < | > | >> | Kapitel 43 - Reflection |
Wir wollen uns ein erstes Beispiel für die Anwendung des Reflection-APIs ansehen. In der Praxis stellt sich immer wieder das Problem, wohin bei neu entwickelten Klassen der Code zum Testen der Klasse geschrieben werden soll. Ein Weg ist der, an das Ende der Klasse eine Methode public static void main zu hängen und den Testcode dort zu plazieren:
001 public class Queue 002 { 003 //... 004 //Implementierung der Queue 005 //... 006 007 //---Testcode------------------------------ 008 public static void main(String[] args) 009 { 010 Queue q = new Queue(); 011 //... 012 //Code zum Testen der Queue 013 //... 014 } 015 } |
Auf diese Weise läßt sich der Testcode einer Dienstleistungsklasse - wie beispielsweise einer Queue - ganz einfach mit dem Java-Interpreter aufrufen und reproduzierbar testen. Nachteilig ist natürlich, daß der eigentliche Code und der Testcode vermischt werden. Dadurch wird die Klassendatei unnötig groß, was im Hinblick auf gute Downloadzeiten nicht wünschenswert ist. Besser wäre es, wenn der Testcode in einer separaten Klasse verbleiben würde. Wir wollen dazu ein kleines Programm zum Testen von Java-Klassen schreiben. Es soll folgende Eigenschaften besitzen:
Der Schlüssel zur Implementierung der Klasse Test liegt in der Anwendung des Reflection-APIs. Das Laden der Testklasse entspricht dem vorigen Beispiel, zum Aufzählen aller Methoden bedienen wir uns der Methode getMethods der Klasse Class:
public Method[] getMethods() throws SecurityException |
java.lang.Class |
getMethods liefert ein Array von Objekten des Typs Method, das für jede öffentliche Methode der Klasse ein Element enthält. Um auch die nicht-öffentlichen Methoden aufzulisten, kann die Methode getDeclaredMethods verwendet werden. Die Klasse Method stellt einige Methoden zum Zugriff auf das Methodenobjekt zur Verfügung. Die wichtigsten sind:
String getName() int getModifiers() Class[] getParameterTypes() Object invoke(Object obj, Object[] args) |
java.lang.reflect.Method |
Mit getName kann der Name der Methode ermittelt werden. getModifiers liefert eine bitverknüpfte Darstellung der Methodenattribute (static, private usw.). Der Rückgabewert kann an die statischen Methoden der Klasse Modifier übergeben werden, um festzustellen, welche Attribute die Methode besitzt:
static boolean isAbstract(int mod) static boolean isExplicit(int mod) static boolean isFinal(int mod) static boolean isInterface(int mod) static boolean isNative(int mod) static boolean isPrivate(int mod) static boolean isProtected(int mod) static boolean isPublic(int mod) static boolean isStatic(int mod) static boolean isStrict(int mod) static boolean isSynchronized(int mod) static boolean isTransient(int mod) static boolean isVolatile(int mod) |
java.lang.reflect.Modifier |
getParameterTypes liefert ein Array mit Objekten des Typs Class, das dazu verwendet werden kann, die Anzahl und Typen der formalen Argumente der Methode festzustellen. Jedes Arrayelement repräsentiert dabei die Klasse des korrespondierenden formalen Arguments. Hat das Array die Länge 0, so ist die Methode parameterlos. Gibt es beispielsweise zwei Elemente mit den Typen String und Double, so besitzt die Methode zwei Parameter, die vom Typ String und Double sind.
Um auch primitive Typen auf diese Weise darstellen zu können, gibt es in den Wrapper-Klassen der primitiven Typen (siehe Abschnitt 10.2) jeweils ein statisches Class-Objekt mit der Bezeichnung TYPE, das den zugehörigen primitiven Datentyp bezeichnet. Ein int-Argument wird also beispielsweise dadurch angezeigt, daß der Rückgabewert von getParameterTypes an der entsprechenden Stelle ein Objekt des Typs Integer.TYPE enthält. Insgesamt gibt es neun derartige Klassenobjekte, und zwar für die acht primitiven Typen und für den "leeren" Rückgabewert void:
Klassenobjekt | Typ |
Boolean.TYPE | boolean |
Character.TYPE | char |
Byte.TYPE | byte |
Short.TYPE | short |
Integer.TYPE | int |
Long.TYPE | long |
Float.TYPE | float |
Double.TYPE | double |
Void.TYPE | void |
Tabelle 43.1: Klassenobjekte für die primitiven Typen
Alternativ zur .TYPE-Notation kann auch die in Abschnitt 43.3.2 vorgestellte .class-Notation verwendet werden, um Klassenobjekte für primitive Typen zu erzeugen. Dazu ist einfach der Name des gewünschten Typs um ".class" zu ergänzen, also z.B. boolean.class oder void.type zu schreiben. Der Compiler erzeugt dann ein passendes Klassenobjekt für den primitiven Typ. |
|
Die Methode invoke der Klasse Method dient dazu, die durch dieses Methodenobjekt repräsentierte Methode tatsächlich aufzurufen. Das erste Argument obj gibt dabei das Objekt an, auf dem die Methode ausgeführt werden soll. Es muß natürlich zu einem Objekt der Klasse gehören, auf der getMethods aufgerufen wurde. Das zweite Argument übergibt die aktuellen Parameter an die Methode. Ähnlich wie bei getParameterTypes wird auch hier ein Array angegeben, dessen Elemente den korrespondierenden aktuellen Argumenten entsprechen. Bei Objektparametern ist einfach ein Objekt des passenden Typs an der gewünschten Stelle zu plazieren.
Besitzt die Methode auch primitive Argumente, wird eine automatische Konvertierung vorgenommen (unwrapping), indem das entsprechende Arrayelement in den passenden primitiven Datentyp konvertiert wird. Erwartet die Methode beispielsweise ein int, so ist ein Integer-Objekt zu übergeben, das dann beim Aufruf automatisch "ausgepackt" wird. |
|
Die Implementierung der Klasse Test sieht so aus:
001 /* Test.java */ 002 003 import java.lang.reflect.*; 004 005 public class Test 006 { 007 public static Object createTestObject(String name) 008 { 009 //Klassennamen zusammenbauen 010 int pos = name.lastIndexOf('.'); 011 if (pos == -1) { 012 name = "Test" + name; 013 } else { 014 name = name.substring(0, pos + 1) + "Test" + 015 name.substring(pos + 1); 016 } 017 //Klasse laden 018 Object ret = null; 019 try { 020 Class testclass = Class.forName(name); 021 //Testobjekt instanzieren 022 System.out.println("=============================="); 023 System.out.println("Instanzieren von: " + name); 024 System.out.println("--"); 025 ret = testclass.newInstance(); 026 } catch (ClassNotFoundException e) { 027 System.err.println("Kann Klasse nicht laden: " + name); 028 } catch (InstantiationException e) { 029 System.err.println("Fehler beim Instanzieren: " + name); 030 } catch (IllegalAccessException e) { 031 System.err.println("Unerlaubter Zugriff auf: " + name); 032 } 033 return ret; 034 } 035 036 public static void runTests(Object tester) 037 { 038 Class clazz = tester.getClass(); 039 Method[] methods = clazz.getMethods(); 040 int cnt = 0; 041 for (int i = 0; i < methods.length; ++i) { 042 //Methodenname muß mit "test" anfangen 043 String name = methods[i].getName(); 044 if (!name.startsWith("test")) { 045 continue; 046 } 047 //Methode muß parameterlos sein 048 Class[] paras = methods[i].getParameterTypes(); 049 if (paras.length > 0) { 050 continue; 051 } 052 //Methode darf nicht static sein 053 int modifiers = methods[i].getModifiers(); 054 if (Modifier.isStatic(modifiers)) { 055 continue; 056 } 057 //Nun kann die Methode aufgerufen werden 058 ++cnt; 059 System.out.println("=============================="); 060 System.out.println("Aufgerufen wird: " + name); 061 System.out.println("--"); 062 try { 063 methods[i].invoke(tester, new Object[0]); 064 } catch (Exception e) { 065 System.err.println(e.toString()); 066 } 067 } 068 if (cnt <= 0) { 069 System.out.println("Keine Testmethoden gefunden"); 070 } 071 } 072 073 public static void main(String[] args) 074 { 075 if (args.length <= 0) { 076 System.err.println("Aufruf: java Test <KlassenName>"); 077 System.exit(1); 078 } 079 Object tester = createTestObject(args[0]); 080 runTests(tester); 081 } 082 } |
Test.java |
Das Hauptprogramm ruft zunächst die Methode createTestObject auf, um ein Objekt der Testklasse zu generieren. Falls als Argument also beispielsweise Queue übergeben wurde, wird ein Objekt des Typs TestQueue erzeugt. Ist es nicht vorhanden oder kann nicht instanziert werden, liefert die Methode null als Rückgabewert.
Anschließend wird das Testobjekt an die Methode runTests übergeben. Diese besorgt sich das Klassenobjekt und ruft getMethods auf. Das zurückgegebene Array repräsentiert die Liste aller öffentlichen Methoden und wird elementweise durchlaufen. Zunächst wird überprüft, ob der Methodenname mit test anfängt. Ist das der Fall, wird geprüft, ob die Methode parameterlos ist und nicht das static-Attribut besitzt. Sind auch diese Bedingungen erfüllt, kann die Methode mit invoke aufgerufen werden. Als erstes Argument wird das Testobjekt übergeben. Als zweites folgt ein leeres Array des Typs Object, um anzuzeigen, daß keine Parameter zu übergeben sind.
Beachten Sie, daß am Anfang des Programms das Paket java.lang.reflect eingebunden wurde. Während die Klassen Class und Object aus historischen Gründen in java.lang liegen (und deshalb automatisch importiert werden), liegen sie für die übrigen Bestandteile des Reflection-APIs in java.lang.reflect und müssen deshalb explizit importiert werden. |
|
Eine beispielhafte Implementierung der Klasse TestQueue könnte etwa so aussehen:
001 public class TestQueue 002 { 003 public TestQueue() 004 { 005 //Intialisierungen, z.B. Erzeugen eines zu 006 //testenden Queue-Objekts 007 } 008 009 public void test1() 010 { 011 //Erste Testmethode 012 } 013 014 public void test2() 015 { 016 //Zweite Testmethode 017 } 018 019 //... 020 } |
Ein Aufruf von
java Test Queue
würde nun ein neues Objekt des Typs TestQueue instanzieren und nacheinander die Methoden test1, test2 usw. aufrufen.
In diesem Abschnitt wollen wir uns den Aufruf parametrisierter Methoden ansehen, was nach den Ausführungen des vorigen Abschnitts nicht mehr schwierig ist. Als Beispiel soll ein Programm geschrieben werden, das die in Java nicht vorhandenen Funktionszeiger simuliert. Es soll eine Methode enthalten, die eine Wertetabelle für eine mathematische Funktion erzeugt, deren Name als String übergeben wurde. Nach den Überlegungen des vorigen Abschnitts können wir uns gleich das Listing ansehen:
001 /* FloatTables.java */ 002 003 import java.lang.reflect.*; 004 005 public class FloatTables 006 { 007 public static double times2(double value) 008 { 009 return 2 * value; 010 } 011 012 public static double sqr(double value) 013 { 014 return value * value; 015 } 016 017 public static void printTable(String methname) 018 { 019 try { 020 System.out.println("Wertetabelle fuer " + methname); 021 int pos = methname.lastIndexOf('.'); 022 Class clazz; 023 if (pos == -1) { 024 clazz = FloatTables.class; 025 } else { 026 clazz = Class.forName(methname.substring(0, pos)); 027 methname = methname.substring(pos + 1); 028 } 029 Class[] formparas = new Class[1]; 030 formparas[0] = Double.TYPE; 031 Method meth = clazz.getMethod(methname, formparas); 032 if (!Modifier.isStatic(meth.getModifiers())) { 033 throw new Exception(methname + " ist nicht static"); 034 } 035 Object[] actargs = new Object[1]; 036 for (double x = 0.0; x <= 5.0; x += 1) { 037 actargs[0] = new Double(x); 038 Double ret = (Double)meth.invoke(null, actargs); 039 double result = ret.doubleValue(); 040 System.out.println(" " + x + " -> " + result); 041 } 042 } catch (Exception e) { 043 System.err.println(e.toString()); 044 } 045 } 046 047 public static void main(String[] args) 048 { 049 printTable("times2"); 050 printTable("java.lang.Math.exp"); 051 printTable("sqr"); 052 printTable("java.lang.Math.sqrt"); 053 } 054 } |
FloatTables.java |
Das Hauptprogramm ruft die Methode printTable viermal auf, um die Wertetabellen zu den statischen Funktionen times2, java.lang.Math.exp, sqr und java.lang.Math.sqrt zu erzeugen. Sie kann sowohl mit lokal definierten Methoden umgehen als auch mit solchen, die in einer anderen Klasse liegen (in diesem Fall sogar aus einem anderen Paket).
In Zeile 021 wird zunächst der am weitesten rechts stehende Punkt im Methodennamen gesucht. Ist ein Punkt vorhanden, wird der String an dieser Stelle aufgeteilt. Der links davon stehende Teil wird als Klassenname angesehen, der rechts davon stehende als Methodenname. Gibt es keinen Punkt, wird als Klassenname der Name der eigenen Klasse verwendet. Anschließend wird das zugehörige Klassenobjekt geladen.
Ist der Klassenname zur Compilezeit bekannt, kann anstelle des Aufrufs von forName die abkürzende Schreibweise .class verwendet werden. Das hat den Vorteil, daß bereits der Compiler überprüfen kann, ob die genannte Klasse vorhanden ist. |
|
Anders als im vorigen Abschnitt generiert das Programm nun nicht eine Liste aller Methoden, sondern sucht mit getMethod ganz konkret nach einer bestimmten:
public Method getMethod(String name, Class[] parameterTypes) |
java.lang.Class |
Dazu müssen der Name der Methode und eine Beschreibung ihrer formalen Argumente an getMethod übergeben werden. Auch hier werden die Argumente durch ein Array mit korrespondierenden Klassenobjekten repräsentiert. Da die Methoden, die in diesem Fall aufgerufen werden sollen, nur ein einziges Argument vom Typ double haben sollen, hat unsere Parameterspezifikation formParas lediglich ein einziges Element Double.TYPE. Wurde keine solche Methode gefunden oder besitzt sie nicht das static-Attribut, löst getMethods eine Ausnahme des Typs NoSuchMethodException aus.
Würde die Methode anstelle des primitiven Typs double
ein Argument des Referenztyps Double
erwarten, hätten wir ein Klassenobjekt der Klasse Double
übergeben müssen. Dafür gibt es verschiedene Möglichkeiten,
beispielsweise die beiden folgenden:
oder
|
|
Auch eine parametrisierte Methode kann mit invoke aufgerufen werden. Im Unterschied zur parameterlosen muß nun allerdings ein nicht-leeres Object-Array mit den aktuellen Argumenten übergeben werden. Hier zeigt sich ein vermeintliches Problem, denn in einem Array vom Object[] können keine primitiven Typen abgelegt werden. Um Methoden mit primitiven Parametern aufrufen zu können, werden diese einfach in die passende Wrapper-Klasse verpackt (siehe Abschnitt 10.2). Beim Aufruf von invoke werden sie dann automatisch »ausgepackt« und dem primitiven Argument zugewiesen.
Wir verpacken also den zu übergebenden Wert in ein Double-Objekt und stellen dieses in Zeile 037 in das Array mit den aktuellen Argumenten. Beim Methodenaufruf in der nächsten Zeile wird es dann automatisch ausgepackt und steht innerhalb der Methode als double-Wert zur Verfügung. Das Programm ruft die Methode für jeden der Werte 0.0, 1.0, 2.0, 3.0, 4.0 und 5.0 auf und erzeugt so eine einfache Wertetabelle.
Anders als in Listing 43.3 wird in diesem Beispiel als erstes Argument von invoke nicht das Objekt übergeben, an dem die Methode aufgerufen werden soll, sondern der Wert null. Das liegt daran, daß wir eine statische Methode aufrufen, die keiner Objektinstanz, sondern dem Klassenobjekt zugeordnet ist. Bei nicht-statischen Methoden ist die Übergabe von null natürlich nicht erlaubt und würde zu einer NullPointerException führen. |
|
Die Ausgabe des Programms ist:
Wertetabelle fuer times2
0.0 -> 0.0
1.0 -> 2.0
2.0 -> 4.0
3.0 -> 6.0
4.0 -> 8.0
5.0 -> 10.0
Wertetabelle fuer java.lang.Math.exp
0.0 -> 1.0
1.0 -> 2.7182818284590455
2.0 -> 7.38905609893065
3.0 -> 20.085536923187668
4.0 -> 54.598150033144236
5.0 -> 148.4131591025766
Wertetabelle fuer sqr
0.0 -> 0.0
1.0 -> 1.0
2.0 -> 4.0
3.0 -> 9.0
4.0 -> 16.0
5.0 -> 25.0
Wertetabelle fuer java.lang.Math.sqrt
0.0 -> 0.0
1.0 -> 1.0
2.0 -> 1.4142135623730951
3.0 -> 1.7320508075688772
4.0 -> 2.0
5.0 -> 2.23606797749979
Der in Listing 43.5 vorgestellte Code ist eigentlich nicht zur Nachahmung empfohlen, sondern soll nur als Beispiel für den Aufruf parametrisierter Methoden mit Hilfe des Reflection-APIs dienen. Ein zum hier vorgestellten Code äquivalentes Beispiel auf der Basis von Interfaces wurde in Abschnitt 9.4.3 vorgestellt. |
|
Die Methode newInstance der Klasse Class ruft immer den parameterlosen Konstruktor auf, um ein Objekt zu instanzieren. Mit Reflection ist es aber auch möglich, parametrisierte Konstruktoren zur dynamischen Instanzierung zu verwenden. Dazu besitzt Class zwei Methoden getConstructors und getConstructor, die dazu verwendet werden, Konstruktorenobjekte zu beschaffen. Anders als getMethods und getMethod liefern sie allerdings kein Objekt des Typs Method, sondern eines des Typs Constructor zurück.
Auch dieses besitzt die oben beschriebenen Methoden getModifiers, getName und getParameterTypes. Der Aufruf einer Methode erfolgt allerdings nicht mit invoke, sondern mit newInstance:
Object newInstance(Object[] initargs) |
java.lang.reflect.Constructor |
newInstance erwartet ebenfalls ein Array von Argumenten des Typs Object. Diese werden gegebenenfalls in der zuvor beschriebenen Weise auf primitive Typen abgebildet und rufen schließlich den passenden Konstruktor auf. Als Rückgabewert von newInstance wird das neu instanzierte Objekt geliefert.
Das folgende Listing zeigt die Verwendung parametrisierter Konstruktoren mit dem Reflection-API:
001 /* Listing4306.java */ 002 003 import java.lang.reflect.*; 004 005 public class Listing4306 006 { 007 public static void main(String[] args) 008 { 009 Class clazz = TestConstructors.class; 010 //Formale Parameter definieren 011 Class[] formparas = new Class[2]; 012 formparas[0] = String.class; 013 formparas[1] = String.class; 014 try { 015 Constructor cons = clazz.getConstructor(formparas); 016 //Aktuelle Argumente definieren 017 Object[] actargs = new Object[] {"eins", "zwei"}; 018 Object obj = cons.newInstance(actargs); 019 ((TestConstructors)obj).print(); 020 } catch (Exception e) { 021 System.err.println(e.toString()); 022 System.exit(1); 023 } 024 } 025 } 026 027 class TestConstructors 028 { 029 private String arg1; 030 private String arg2; 031 032 public TestConstructors() 033 { 034 arg1 = "leer"; 035 arg2 = "leer"; 036 } 037 038 public TestConstructors(String arg1) 039 { 040 this(); 041 this.arg1 = arg1; 042 } 043 044 public TestConstructors(String arg1, String arg2) 045 { 046 this(); 047 this.arg1 = arg1; 048 this.arg2 = arg2; 049 } 050 051 public void print() 052 { 053 System.out.println("arg1 = " + arg1); 054 System.out.println("arg2 = " + arg2); 055 } 056 } |
Listing4306.java |
Das Programm erzeugt zunächst ein Klassenobjekt zu der Klasse
TestConstructors. Anschließend
wird ein Array mit zwei Klassenobjekten der Klasse String
erzeugt und als Spezifikation der formalen Parameter an getConstructor
übergeben. Das zurückgegebene Constructor-Objekt
wird dann mit zwei aktuellen Argumenten "eins" und "zwei" vom Typ
String
ausgestattet, die an seine Methode newInstance
übergeben werden. Diese instanziert das Objekt, castet es auf
die Klasse TestConstructors
und ruft deren Methode print
auf. Die Ausgabe des Programms ist:
arg1 = eins
arg2 = zwei
Titel | Inhalt | Suchen | Index | API | Go To Java 2, Zweite Auflage, Addison Wesley, Version 2.0 |
<< | < | > | >> | © 2000 Guido Krüger, http://www.gkrueger.com |