Introducción a Java Reflection
Qué es la reflexión en Java, cuándo usarla y una descripción general del paquete java.lang.reflect.
Reflection es la capacidad de un programa para inspeccionar y manipular su propia estructura en tiempo de ejecución: preguntar a una clase qué campos, métodos y constructores tiene, leer y escribir esos campos, llamar a esos métodos y crear nuevas instancias — todo sin nombrar los tipos en tiempo de compilación. Donde el Java ordinario es estático (el compilador conoce cada tipo que usas), la reflexión es dinámica (descubres tipos a partir de cadenas, configuración o lo que sea que se cargue en tiempo de ejecución). Esta parte del libro es un recorrido por java.lang.reflect; este capítulo establece el contexto.
Qué permite hacer la reflexión
El código normal nombra el tipo con el que trabaja:
User u = new User("ada");
String name = u.getName();El código reflectivo obtiene el mismo resultado sin escribir User ni getName como tokens en tiempo de compilación — son cadenas resueltas en tiempo de ejecución:
Class<?> cls = Class.forName("com.example.User");
Object u = cls.getDeclaredConstructor(String.class).newInstance("ada");
Object name = cls.getMethod("getName").invoke(u);La segunda forma es mucho más verbosa y mucho más lenta, y descarta la verificación de tipos en tiempo de compilación. Nunca la escribirías para lógica de aplicación ordinaria. Recurres a ella precisamente cuando el tipo no se conoce hasta el tiempo de ejecución.
Cuándo la reflexión merece la pena
La reflexión es el motor detrás de los frameworks, no del código cotidiano. Usos típicos:
- Inyección de dependencias (Spring, Guice, CDI): el contenedor lee anotaciones y firmas de constructores, luego instancia y conecta beans cuyo código fuente nunca ha visto.
- Serialización (Jackson, Gson): una biblioteca JSON recorre los campos de un objeto para leerlos o rellenarlos sin que tengas que escribir código de mapeo por clase.
- ORMs (Hibernate, JPA): mapean columnas a campos reflejando sobre una clase de entidad.
- Ejecutores de pruebas (JUnit): encuentran métodos anotados con
@Testy los invocan. - Sistemas de plugins: cargan una clase nombrada en un archivo de configuración y llaman a un método de interfaz conocido sobre ella.
El hilo común: un mecanismo general que opera sobre tipos de usuario arbitrarios contra los que no fue compilado. Ese es exactamente el problema que resuelve la reflexión.
Los tipos principales en java.lang.reflect
La reflexión comienza desde un objeto Class (cubierto en el siguiente capítulo, Objetos Class de Java) y se ramifica en una pequeña familia de clases, cada una describiendo un tipo de miembro:
| Tipo | Representa | Obtenido de Class mediante |
|---|---|---|
Field | un campo | getField / getDeclaredField(s) |
Method | un método | getMethod / getDeclaredMethod(s) |
Constructor<T> | un constructor | getConstructor / getDeclaredConstructor(s) |
Parameter | un parámetro de método/constructor | Executable.getParameters() |
Modifier | un helper para el bitset int de modificadores | métodos estáticos |
Field, Method y Constructor comparten una jerarquía de superclases común: Member (la interfaz) y AccessibleObject (que lleva setAccessible). Esa base compartida es por qué "hacerlo accesible" y "leer sus anotaciones" se ven idénticos sin importar qué miembro tengas.
La diferencia entre get… y getDeclared…
Casi todos los métodos de búsqueda existen en dos variantes, y la distinción importa en cada capítulo posterior:
getField/getMethod/getConstructor— devuelve miembros públicos, incluidos los heredados de superclases e interfaces.getDeclaredField/getDeclaredMethod/getDeclaredConstructor— devuelve miembros de cualquier nivel de acceso (private,protected, de paquete), pero solo los declarados en esta clase exacta — nada heredado.
Así, getMethods() ve un método público heredado de un padre pero no un helper private en la propia clase; getDeclaredMethods() ve el helper private pero no el método público heredado. Para acceder a un miembro privado heredado, subes por getSuperclass() llamando a getDeclared… en cada nivel.
El costo: velocidad, seguridad y encapsulamiento
La reflexión es poderosa, y cada poder tiene un precio.
- Rendimiento. Las llamadas reflectivas son más lentas que las directas — las búsquedas de métodos/campos, las verificaciones de acceso y el boxing de argumentos añaden sobrecarga. El JIT optimiza bien las llamadas reflectivas frecuentes, pero la reflexión en un bucle ajustado es una señal de alerta. Guarda en caché los objetos
Method/Field; nunca los busques por cada llamada. - Sin seguridad en tiempo de compilación. Un error tipográfico en el nombre de un método compila bien y falla en tiempo de ejecución con
NoSuchMethodException. Las herramientas de refactorización renombrangetNameen todos lados — excepto dentro de tu string"getName". - Romper el encapsulamiento.
setAccessible(true)te permite leer y escribir estadoprivate. Así es como los serializadores rellenan campos sin setter, pero te acopla a elementos internos que el autor de la clase nunca prometió mantener estables. - Restricciones de módulos. Desde Java 9, el sistema de módulos puede denegar el acceso reflectivo a paquetes no exportados. Llamar a
setAccessible(true)a través de un límite de módulo que no ha declaradoopensel paquete lanzaInaccessibleObjectException.
Un ejemplo práctico: un pequeño volcador genérico de objetos
Para concretar el alcance, aquí hay una rutina reflectiva que imprime los campos y sus valores de cualquier objeto — el tipo de cosa que hace un depurador o una biblioteca de registro. No nombra ningún tipo de aplicación; funciona con lo que sea que le pases.
Qué extraer de la ejecución:
- El método
dumpno nombró ningún tipo concreto y, sin embargo, imprimió tantoPointcomoUser. Su único contrato es "dame unObject"; todo sobre la estructura — nombres de campos, tipos, valores — provino degetClass()en tiempo de ejecución. Ese es el movimiento definitorio de la reflexión: una rutina, entradas arbitrarias. getDeclaredFields()devolvió todos los campos incluyendo losprivate, pero leerlos requirió llamar primero asetAccessible(true). Sin esa llamada,f.get(obj)sobre un campo privado lanzaIllegalAccessException. La búsqueda y el acceso son dos puertas separadas.Modifier.toString(f.getModifiers())convirtió el bitset de modificadores en texto legible comoprivate final. Los modificadores se almacenan como unintde bits de bandera; el helperModifierlos decodifica para que no tengas que probar bits manualmente.- El tercer objeto fue construido sin ningún
new User(...)en el código fuente —Class.forName("…$User")resolvió la clase anidada desde un string (nótese el separador$para tipos anidados), ygetDeclaredConstructor(...).newInstance(...)lo construyó. Este es el patrón de carga de plugins en miniatura: un nombre de entrada, un objeto de salida. - Leer el valor del campo (
f.get(obj)) y leer los metadatos del campo (f.getName(),f.getModifiers()) son independientes. Los metadatos no necesitan instancia ni accesibilidad; los valores necesitan el objeto y, para campos privados, el indicador de accesibilidad.
Cómo está organizado el resto de esta parte
Cada capítulo restante profundiza en un área:
- Objetos Class — las tres formas de obtener un
Class<T>y qué te dice. - Campos — inspección, lectura y escritura de campos (incluyendo
privateyfinal). - Métodos — búsqueda e invocación de métodos, resolución de sobrecarga, valores de retorno.
- Constructores — construcción de instancias de forma reflectiva, incluyendo constructores privados.
- Anotaciones — lectura de los metadatos que adjuntaste con anotaciones, en tiempo de ejecución.
- Proxies dinámicos — síntesis de implementaciones completas de interfaces en tiempo de ejecución.
A lo largo de todo esto, ten presente el equilibrio: la reflexión es la herramienta adecuada cuando el tipo genuinamente no se conoce hasta el tiempo de ejecución, y la herramienta incorrecta cuando sí se conoce. El siguiente capítulo comienza en la raíz de todo — el objeto Class.