Titel   Inhalt   Suchen   Index   DOC  Handbuch der Java-Programmierung, 5. Auflage
 <<    <     >    >>   API  Kapitel 43 - Reflection

43.3 Methoden- und Konstruktorenaufrufe



43.3.1 Parameterlose Methoden

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 platzieren:

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 }
Listing 43.2: Testcode in der main-Methode

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, dass 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 Array-Element 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, dass 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.class zu schreiben. Der Compiler erzeugt dann ein passendes Klassenobjekt für den primitiven Typ.

 Tipp 

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

Besitzt die Methode auch primitive Argumente, wird eine automatische Konvertierung vorgenommen (unwrapping), indem das entsprechende Array-Element 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.

 Tipp 

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 muss mit "test" anfangen
043       String name = methods[i].getName();
044       if (!name.startsWith("test")) {
045         continue;
046       }
047       //Methode muss 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
Listing 43.3: Die Klasse Test

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, dass keine Parameter zu übergeben sind.

Beachten Sie, dass 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.

 Hinweis 

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 }
Listing 43.4: Die Klasse TestQueue

Ein Aufruf von

java Test Queue

würde nun ein neues Objekt des Typs TestQueue instanzieren und nacheinander die Methoden test1, test2 usw. aufrufen.

43.3.2 Parametrisierte Methoden

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
Listing 43.5: Funktionszeiger mit Reflection nachbilden

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, dass bereits der Compiler überprüfen kann, ob die genannte Klasse vorhanden ist.

 Tipp 

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:

formparas[0] = (new Double(0)).getClass();

oder

formparas[0] = Double.class;
 Tipp 

Auch eine parametrisierte Methode kann mit invoke aufgerufen werden. Im Unterschied zur parameterlosen muss 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, dass 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.

 Hinweis 

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.

 Hinweis 

43.3.3 Parametrisierte Konstruktoren

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
Listing 43.6: Parametrisierte Konstruktoren mit Reflection aufrufen

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   DOC  Handbuch der Java-Programmierung, 5. Auflage, Addison Wesley, Version 5.0.2
 <<    <     >    >>   API  © 1998, 2007 Guido Krüger & Thomas Stark, http://www.javabuch.de