Eche un vistazo en profundidad a la API de reflexión de Java

En "Java In-Depth" del mes pasado, hablé sobre la introspección y las formas en que una clase Java con acceso a datos de clase sin procesar podría mirar "dentro" de una clase y descubrir cómo se construyó la clase. Además, mostré que con la adición de un cargador de clases, esas clases podrían cargarse en el entorno de ejecución y ejecutarse. Ese ejemplo es una forma de introspección estática . Este mes echaré un vistazo a la API de reflexión de Java, que brinda a las clases de Java la capacidad de realizar una introspección dinámica : la capacidad de mirar dentro de las clases que ya están cargadas.

La utilidad de la introspección

Una de las fortalezas de Java es que fue diseñado con la suposición de que el entorno en el que se estaba ejecutando cambiaría dinámicamente. Las clases se cargan dinámicamente, la vinculación se realiza dinámicamente y las instancias de objetos se crean dinámicamente sobre la marcha cuando se necesitan. Lo que no ha sido históricamente muy dinámico es la capacidad de manipular clases "anónimas". En este contexto, una clase anónima es aquella que se carga o se presenta a una clase Java en tiempo de ejecución y cuyo tipo era previamente desconocido para el programa Java.

Clases anónimas

Apoyar clases anónimas es difícil de explicar y aún más difícil de diseñar en un programa. El desafío de admitir una clase anónima puede expresarse así: "Escriba un programa que, cuando se le dé un objeto Java, pueda incorporar ese objeto en su funcionamiento continuo". La solución general es bastante difícil, pero al limitar el problema, se pueden crear algunas soluciones especializadas. Hay dos ejemplos de soluciones especializadas para esta clase de problemas en la versión 1.0 de Java: los subprogramas de Java y la versión de línea de comandos del intérprete de Java.

Los subprogramas de Java son clases de Java que se cargan mediante una máquina virtual Java en ejecución en el contexto de un navegador web y se invocan. Estas clases de Java son anónimas porque el tiempo de ejecución no conoce de antemano la información necesaria para invocar cada clase individual. Sin embargo, el problema de invocar una clase en particular se resuelve usando la clase Java java.applet.Applet.

Las superclases comunes, como Applet, y las interfaces Java, como AppletContext, abordan el problema de las clases anónimas creando un contrato previamente acordado. Específicamente, un proveedor de entorno de ejecución anuncia que puede usar cualquier objeto que se ajuste a una interfaz específica, y el consumidor del entorno de ejecución usa esa interfaz específica en cualquier objeto que pretenda suministrar al tiempo de ejecución. En el caso de los applets, existe una interfaz bien especificada en forma de una superclase común.

La desventaja de una solución de superclase común, especialmente en ausencia de herencia múltiple, es que los objetos construidos para ejecutarse en el entorno no pueden usarse también en algún otro sistema a menos que ese sistema implemente todo el contrato. En el caso de las Appletinterfaces, el entorno de alojamiento tiene que implementarse AppletContext. Lo que esto significa para la solución de subprogramas es que la solución solo funciona cuando está cargando subprogramas. Si coloca una instancia de un Hashtableobjeto en su página web y apunta su navegador hacia él, no se cargará porque el sistema de subprogramas no puede operar fuera de su rango limitado.

Además del ejemplo del subprograma, la introspección ayuda a resolver un problema que mencioné el mes pasado: averiguar cómo iniciar la ejecución en una clase que acaba de cargar la versión de línea de comandos de la máquina virtual Java. En ese ejemplo, la máquina virtual tiene que invocar algún método estático en la clase cargada. Por convención, ese método se nombra mainy toma un solo argumento: una matriz de Stringobjetos.

La motivación para una solución más dinámica

El desafío con la arquitectura Java 1.0 existente es que hay problemas que podrían resolverse mediante un entorno de introspección más dinámico, como componentes de IU cargables, controladores de dispositivos cargables en un sistema operativo basado en Java y entornos de edición configurables dinámicamente. La "aplicación asesina", o el problema que provocó la creación de la API de reflexión de Java, fue el desarrollo de un modelo de componentes de objetos para Java. Ese modelo ahora se conoce como JavaBeans.

Los componentes de la interfaz de usuario son un punto de diseño ideal para un sistema de introspección porque tienen dos consumidores muy diferentes. Por un lado, los objetos componentes están vinculados entre sí para formar una interfaz de usuario como parte de alguna aplicación. Alternativamente, debe haber una interfaz para herramientas que manipulen los componentes del usuario sin tener que saber cuáles son los componentes o, lo que es más importante, sin acceso al código fuente de los componentes.

La API de Java Reflection surgió de las necesidades de la API del componente de interfaz de usuario de JavaBeans.

¿Qué es la reflexión?

Básicamente, la API de Reflection consta de dos componentes: objetos que representan las distintas partes de un archivo de clase y un medio para extraer esos objetos de forma segura. Esto último es muy importante, ya que Java proporciona muchas salvaguardas de seguridad, y no tendría sentido proporcionar un conjunto de clases que invalidaran esas salvaguardas.

El primer componente de la API Reflection es el mecanismo que se utiliza para obtener información sobre una clase. Este mecanismo está integrado en la clase nombrada Class. La clase especial Classes el tipo universal para la metainformación que describe objetos dentro del sistema Java. Los cargadores de clases en el sistema Java devuelven objetos de tipo Class. Hasta ahora, los tres métodos más interesantes de esta clase eran:

  • forName, que cargaría una clase de un nombre dado, utilizando el cargador de clases actual

  • getName, que devolvería el nombre de la clase como un Stringobjeto, lo cual fue útil para identificar referencias a objetos por su nombre de clase

  • newInstance, que invocaría el constructor nulo en la clase (si existe) y le devolvería una instancia de objeto de esa clase de objeto

A estos tres métodos útiles, la API Reflection agrega algunos métodos adicionales a la clase Class. Estos son los siguientes:

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

Además de estos métodos, se agregaron muchas clases nuevas para representar los objetos que estos métodos devolverían. Las nuevas clases en su mayoría son parte del java.lang.reflectpaquete, pero algunas de las nuevas clases de tipos básicos ( Void, Byte, etc.) están en el java.langpaquete. Se tomó la decisión de colocar las nuevas clases donde están colocando clases que representaban metadatos en el paquete de reflexión y clases que representaban tipos en el paquete de lenguaje.

Por lo tanto, la API de Reflection representa una serie de cambios en la clase Classque le permiten hacer preguntas sobre los aspectos internos de la clase y un montón de clases que representan las respuestas que le brindan estos nuevos métodos.

¿Cómo utilizo la API de Reflection?

La pregunta "¿Cómo utilizo la API?" es quizás la pregunta más interesante que "¿Qué es la reflexión?"

La API de Reflection es simétrica , lo que significa que si tiene un Classobjeto, puede preguntar sobre sus componentes internos, y si tiene uno de los componentes internos, puede preguntarle qué clase lo declaró. Por lo tanto, puede ir y venir de una clase a otro, de un parámetro a otro, de una clase a otro, y así sucesivamente. Un uso interesante de esta tecnología es descubrir la mayoría de las interdependencias entre una clase determinada y el resto del sistema.

Un ejemplo de trabajo

Sin embargo, en un nivel más práctico, puede usar la API de Reflection para deshacerse de una clase, como lo dumpclasshizo mi clase en la columna del mes pasado.

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.

El paso final, que se muestra a continuación, es recopilar las referencias de todos los métodos. Este código debe obtener referencias tanto del tipo de método (similar a los campos anteriores) como de los parámetros (similar a los constructores anteriores).

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

En el código anterior, hay dos llamadas a tName: una para recopilar el tipo de retorno y otra para recopilar el tipo de cada parámetro.