Dai uno sguardo approfondito all'API Java Reflection

In "Java In-Depth" del mese scorso, ho parlato dell'introspezione e dei modi in cui una classe Java con accesso ai dati grezzi della classe potrebbe guardare "all'interno" di una classe e capire come è stata costruita la classe. Inoltre, ho mostrato che con l'aggiunta di un caricatore di classi, quelle classi potevano essere caricate nell'ambiente in esecuzione ed eseguite. Quell'esempio è una forma di introspezione statica . Questo mese darò un'occhiata all'API Java Reflection, che offre alle classi Java la capacità di eseguire un'introspezione dinamica : la capacità di guardare all'interno delle classi che sono già caricate.

L'utilità dell'introspezione

Uno dei punti di forza di Java è che è stato progettato partendo dal presupposto che l'ambiente in cui era in esecuzione cambierebbe dinamicamente. Le classi vengono caricate dinamicamente, l'associazione viene eseguita dinamicamente e le istanze degli oggetti vengono create dinamicamente al volo quando sono necessarie. Ciò che storicamente non è stato molto dinamico è la capacità di manipolare classi "anonime". In questo contesto, una classe anonima è quella che viene caricata o presentata a una classe Java in fase di esecuzione e il cui tipo era precedentemente sconosciuto al programma Java.

Classi anonime

Supportare classi anonime è difficile da spiegare e ancora più difficile da progettare in un programma. La sfida di supportare una classe anonima può essere espressa in questo modo: "Scrivete un programma che, quando viene fornito un oggetto Java, possa incorporare quell'oggetto nella sua operazione continua". La soluzione generale è piuttosto difficile, ma vincolando il problema è possibile creare alcune soluzioni specializzate. Ci sono due esempi di soluzioni specializzate a questa classe di problemi nella versione 1.0 di Java: applet Java e la versione a riga di comando dell'interprete Java.

Le applet Java sono classi Java che vengono caricate da una macchina virtuale Java in esecuzione nel contesto di un browser Web e richiamate. Queste classi Java sono anonime perché il runtime non conosce in anticipo le informazioni necessarie per richiamare ogni singola classe. Tuttavia, il problema di invocare una particolare classe viene risolto utilizzando la classe Java java.applet.Applet.

Le superclassi comuni, come Applet, e le interfacce Java, come AppletContext, risolvono il problema delle classi anonime creando un contratto concordato in precedenza. In particolare, un fornitore di ambiente di runtime annuncia di poter utilizzare qualsiasi oggetto conforme a un'interfaccia specificata e il consumatore dell'ambiente di runtime utilizza l'interfaccia specificata in qualsiasi oggetto che intende fornire al runtime. Nel caso degli applet, esiste un'interfaccia ben specificata sotto forma di una superclasse comune.

Lo svantaggio di una soluzione di superclasse comune, specialmente in assenza di ereditarietà multipla, è che gli oggetti costruiti per funzionare nell'ambiente non possono essere utilizzati anche in qualche altro sistema a meno che quel sistema non implementa l'intero contratto. Nel caso delle Appletinterfacce, l'ambiente di hosting deve essere implementato AppletContext. Ciò che questo significa per la soluzione applet è che la soluzione funziona solo quando si caricano applet. Se si inserisce un'istanza di un Hashtableoggetto nella pagina Web e si punta su di essa il browser, non verrà caricato perché il sistema dell'applet non può funzionare al di fuori del suo intervallo limitato.

Oltre all'esempio dell'applet, l'introspezione aiuta a risolvere un problema che ho citato il mese scorso: capire come avviare l'esecuzione in una classe che la versione a riga di comando della Java virtual machine ha appena caricato. In questo esempio, la macchina virtuale deve invocare un metodo statico nella classe caricata. Per convenzione, quel metodo è denominato maine accetta un singolo argomento: un array di Stringoggetti.

La motivazione per una soluzione più dinamica

La sfida con l'architettura Java 1.0 esistente è che ci sono problemi che potrebbero essere risolti da un ambiente di introspezione più dinamico, come componenti dell'interfaccia utente caricabili, driver di dispositivo caricabili in un sistema operativo basato su Java e ambienti di modifica configurabili dinamicamente. L '"app killer", o il problema che ha causato la creazione dell'API Java Reflection, era lo sviluppo di un modello di componenti a oggetti per Java. Quel modello è ora noto come JavaBeans.

I componenti dell'interfaccia utente sono un punto di progettazione ideale per un sistema di introspezione perché hanno due utenti molto diversi. Da un lato, gli oggetti componenti sono collegati insieme per formare un'interfaccia utente come parte di alcune applicazioni. In alternativa, deve esserci un'interfaccia per strumenti che manipolano i componenti dell'utente senza dover sapere quali sono i componenti o, cosa più importante, senza accesso al codice sorgente dei componenti.

L'API Java Reflection è nata dalle esigenze dell'API del componente dell'interfaccia utente JavaBeans.

Cos'è la riflessione?

Fondamentalmente, l'API di Reflection è costituita da due componenti: oggetti che rappresentano le varie parti di un file di classe e un mezzo per estrarre tali oggetti in modo sicuro e protetto. Quest'ultimo è molto importante, poiché Java fornisce molte garanzie di sicurezza e non avrebbe senso fornire un insieme di classi che invalidano tali garanzie.

Il primo componente dell'API di Reflection è il meccanismo utilizzato per recuperare le informazioni su una classe. Questo meccanismo è integrato nella classe denominata Class. La classe speciale Classè il tipo universale per le meta informazioni che descrivono gli oggetti all'interno del sistema Java. I programmi di caricamento classi nel sistema Java restituiscono oggetti di tipo Class. Fino ad ora i tre metodi più interessanti in questa classe erano:

  • forName, che caricherà una classe con un determinato nome, utilizzando il programma di caricamento classi corrente

  • getName, che restituisce il nome della classe come Stringoggetto, utile per identificare i riferimenti agli oggetti in base al nome della classe

  • newInstance, che richiama il costruttore null sulla classe (se esiste) e restituisce un'istanza di oggetto di quella classe di oggetto

A questi tre metodi utili l'API di Reflection aggiunge alcuni metodi aggiuntivi alla classe Class. Questi sono i seguenti:

  • getConstructor, getConstructors,getDeclaredConstructor
  • getMethod, getMethods,getDeclaredMethods
  • getField, getFields,getDeclaredFields
  • getSuperclass
  • getInterfaces
  • getDeclaredClasses

In addition to these methods, many new classes were added to represent the objects that these methods would return. The new classes mostly are part of the java.lang.reflect package, but some of the new basic type classes (Void, Byte, and so on) are in the java.lang package. The decision was made to put the new classes where they are by putting classes that represented meta-data in the reflection package and classes that represented types in the language package.

Thus, the Reflection API represents a number of changes to class Class that let you ask questions about the internals of the class, and a bunch of classes that represent the answers that these new methods give you.

How do I use the Reflection API?

The question "How do I use the API?" is perhaps the more interesting question than "What is reflection?"

The Reflection API is symmetric, which means that if you are holding a Class object, you can ask about its internals, and if you have one of the internals, you can ask it which class declared it. Thus you can move back and forth from class to method to parameter to class to method, and so on. One interesting use of this technology is to find out most of the interdependencies between a given class and the rest of the system.

A working example

On a more practical level, however, you can use the Reflection API to dump out a class, much as my dumpclass class did in last month's column.

To demonstrate the Reflection API, I wrote a class called ReflectClass that would take a class known to the Java run time (meaning it is in your class path somewhere) and, through the Reflection API, dump out its structure to the terminal window. To experiment with this class, you will need to have a 1.1 version of the JDK available.

Note: Do not try to use a 1.0 run time as it gets all confused, usually resulting in an incompatible class change exception.

The class ReflectClass begins as follows:

import java.lang.reflect.*; import java.util.*; public class ReflectClass { 

As you can see above, the first thing the code does is import the Reflection API classes. Next, it jumps right into the main method, which starts out as shown below.

 public static void main(String args[]) { Constructor cn[]; Class cc[]; Method mm[]; Field ff[]; Class c = null; Class supClass; String x, y, s1, s2, s3; Hashtable classRef = new Hashtable(); if (args.length == 0) { System.out.println("Please specify a class name on the command line."); System.exit(1); } try { c = Class.forName(args[0]); } catch (ClassNotFoundException ee) { System.out.println("Couldn't find class '"+args[0]+"'"); System.exit(1); } 

The method main declares arrays of constructors, fields, and methods. If you recall, these are three of the four fundamental parts of the class file. The fourth part is the attributes, which the Reflection API unfortunately does not give you access to. After the arrays, I've done some command-line processing. If the user has typed a class name, the code attempts to load it using the forName method of class Class. The forName method takes Java class names, not file names, so to look inside the java.math.BigInteger class, you simply type "java ReflectClass java.math.BigInteger," rather than point out where the class file actually is stored.

Identifying the class's package

Assuming the class file is found, the code proceeds into Step 0, which is shown below.

 /* * Step 0: If our name contains dots we're in a package so put * that out first. */ x = c.getName(); y = x.substring(0, x.lastIndexOf(".")); if (y.length() > 0) { System.out.println("package "+y+";\n\r"); } 

In this step, the name of the class is retrieved using the getName method in class Class. This method returns the fully qualified name, and if the name contains dots, we can presume that the class was defined as part of a package. So Step 0 is to separate the package name part from the class name part, and print out the package name part on a line that starts with "package...."

Collecting class references from declarations and parameters

With the package statement taken care of, we proceed to Step 1, which is to collect all of the other class names that are referenced by this class. This collection process is shown in the code below. Remember that the three most common places where class names are referenced are as types for fields (instance variables), return types for methods, and as the types of the parameters passed to methods and constructors.

 ff = c.getDeclaredFields(); for (int i = 0; i < ff.length; i++) { x = tName(ff[i].getType().getName(), classRef); } 

In the above code, the array ff is initialized to be an array of Field objects. The loop collects the type name from each field and process it through the tName method. The tName method is a simple helper that returns the shorthand name for a type. So java.lang.String becomes String. And it notes in a hashtable which objects have been seen. At this stage, the code is more interested in collecting class references than in printing.

The next source of class references are the parameters supplied to constructors. The next piece of code, shown below, processes each declared constructor and collects the references from the parameter lists.

 cn = c.getDeclaredConstructors(); for (int i = 0; i  0) { for (int j = 0; j < cx.length; j++) { x = tName(cx[j].getName(), classRef); } } } 

As you can see, I've used the getParameterTypes method in the Constructor class to feed me all of the parameters that a particular constructor takes. These are then processed through the tName method.

An interesting thing to note here is the difference between the method getDeclaredConstructors and the method getConstructors. Both methods return an array of constructors, but the getConstructors method only returns those constructors that are accessible to your class. This is useful if you want to know if you actually can invoke the constructor you've found, but it isn't useful for this application because I want to print out all of the constructors in the class, public or not. The field and method reflectors also have similar versions, one for all members and one only for public members.

Il passaggio finale, mostrato di seguito, consiste nel raccogliere i riferimenti da tutti i metodi. Questo codice deve ottenere riferimenti sia dal tipo di metodo (simile ai campi sopra) che dai parametri (simile ai costruttori sopra).

mm = c.getDeclaredMethods (); for (int i = 0; i 0) {for (int j = 0; j <cx.length; j ++) {x = tName (cx [j] .getName (), classRef); }}}

Nel codice sopra, ci sono due chiamate a tName: una per raccogliere il tipo restituito e una per raccogliere il tipo di ogni parametro.