Java Reflection: Inspección de campos
Inspecciona, lee y modifica campos en tiempo de ejecución en Java con la API de reflection.
Un objeto Field describe un campo de una clase: su nombre, su tipo, sus modificadores y — dado un instancia — su valor. La reflection te permite listar los campos de una clase, leerlos y escribirlos, incluso cuando son private y no tienen getter ni setter. Así es exactamente como los deserializadores JSON pueblan objetos y cómo los ORM hidratan entidades. Este capítulo cubre cómo obtener objetos Field, leer y escribir valores, la compuerta setAccessible y el caso especial de los campos final.
Este capítulo parte de la introducción a reflection. Para las APIs complementarias, consulta inspección de métodos e inspección de constructores.
Obtener objetos Field
Se aplica la misma división público/declarado de la introducción:
Class<?> c = User.class;
Field f1 = c.getField("name"); // public field, incl. inherited — else NoSuchFieldException
Field f2 = c.getDeclaredField("name"); // any access level, this class only
Field[] all = c.getFields(); // public fields, incl. inherited
Field[] mine = c.getDeclaredFields(); // all access levels, this class onlygetField/getFields solo ven campos public pero siguen la cadena de herencia. getDeclaredField/getDeclaredFields también ven campos private/protected/de paquete, pero solo los declarados literalmente en la clase que se consulta. Para recopilar todos los campos, incluidos los private heredados, recorre getSuperclass() y combínalos.
Metadatos de campo: nombre, tipo, modificadores, genéricos
Un Field responde preguntas sobre sí mismo sin necesitar ninguna instancia:
Field f = User.class.getDeclaredField("age");
f.getName(); // "age"
f.getType(); // int.class — the erased type
f.getGenericType(); // int — Type, keeps generic info
f.getModifiers(); // int bitset
Modifier.isPrivate(f.getModifiers()); // true/false
Modifier.isStatic(f.getModifiers());
Modifier.isFinal(f.getModifiers());
f.getDeclaringClass(); // class …UsergetType() da el Class borrado (List); getGenericType() devuelve un Type que, para un campo List<String>, puedes convertir a ParameterizedType para recuperar String. Esa recuperación funciona porque las firmas genéricas de campo se conservan en el archivo de clase aunque las instancias se borren.
Leer y escribir valores
Para leer o escribir necesitas una instancia (o null para un campo static) y debes superar la verificación de acceso:
User u = new User("ada", 36);
Field age = User.class.getDeclaredField("age");
age.setAccessible(true); // bypass the access check for private
int current = age.getInt(u); // typed getter for primitives → 36
age.setInt(u, 37); // typed setter
Object boxed = age.get(u); // generic getter, autoboxes → Integer 37
age.set(u, 40); // generic setter, autounboxesExisten accesores tipados — getInt, getBoolean, getDouble, setLong, … — para campos primitivos, y los genéricos get(Object)/set(Object,Object) para cualquier campo (con boxing de primitivos). Para un campo static, pasa null como destino: staticField.get(null).
La compuerta setAccessible
Por defecto, un Field aplica las reglas de acceso de Java: leer un campo private de forma reflexiva lanza IllegalAccessException. field.setAccessible(true) suprime esa comprobación para este objeto Field. Eso es lo que permite a la reflection acceder a los internos — y lo que la hace peligrosa.
Dos advertencias desde Java 9:
- Límites de módulo. Si el tipo destino está en un módulo que no ha abierto su paquete con
openshacia ti,setAccessible(true)lanzaInaccessibleObjectException. Las bibliotecas te piden añadir--add-openso que el módulo abra el paquete. - Es por objeto. Llamar a
setAccessible(true)afecta solo a la instanciaFielden la que se llama, no al campo globalmente. UnFieldrecién obtenido para el mismo miembro comienza bloqueado de nuevo.
Escribir campos final
Los campos final son un caso especial y delicado. Para un campo final no estático, a veces aún puedes escribirlo tras setAccessible(true):
Field f = Config.class.getDeclaredField("name"); // private final String
f.setAccessible(true);
f.set(config, "changed"); // may work…Pero hay advertencias importantes:
- No funciona para constantes
static finalde tipo primitivo oString— estas son incrustadas por el compilador en cada sitio de uso, por lo que aunque cambies el campo, las lecturas ya compiladas no lo reflejarán. - La JVM y el JIT asumen que los campos
finalnunca cambian; mutar uno es comportamiento indefinido para la visibilidad y puede ser optimizado. - Los JDKs modernos lo prohíben cada vez más de forma rotunda.
La regla honesta: no mutes campos final de forma reflexiva en producción. Los frameworks de serialización que lo hacen (para reconstruir objetos inmutables) usan maquinaria de bajo nivel Unsafe/VarHandle y aceptan el riesgo deliberadamente. El ejemplo a continuación muestra el caso instance-final funcionando para ilustrar el mecanismo, no como recomendación.
Un ejemplo práctico: un pequeño mapeador basado en campos
El programa refleja sobre los campos declarados de un objeto para construir un Map<String,Object> (un mini serializador), luego toma un mapa y escribe sus valores en una instancia nueva (un mini deserializador) — accediendo a campos private en todo momento, sin getters ni setters en ningún lugar.
Lo que se puede extraer de la ejecución:
toMapprodujo una instantánea de cada campo de instancia sin un solo getter —getDeclaredFields()mássetAccessible(true)accedieron directamente al estadoprivate. Eso es, mecánicamente, lo que hacen Jackson y Gson cuando se configuran para acceso por campo. La clase no necesita ninguna API especial; la reflection proporciona la genérica.- El campo estático
countfue excluido porque el bucle verificóModifier.isStatic. Los serializadores habitualmente omiten los camposstatic,transienty sintéticos; el bitset de modificadores es la forma de tomar esas decisiones de manera uniforme en lugar de codificar nombres de campo. fromMapescribió el campoprivate final currencytrassetAccessible(true)y tuvo efecto — demostrando el mecanismo instance-final. Esto funcionó solo porquecurrencyes un final no estático reasignado antes de que algún optimizador lo asumiera constante; depender de esto en código real es frágil, y las constantesstatic finalno habrían cambiado.- Leer metadatos (
bal.getType(),Modifier.toString(...),isFinal(...)) no requirió ninguna instancia deAccount— unFielddescribe la declaración, que es la misma para cada objeto de la clase. Los valores necesitan una instancia; la forma no. - El
getInt(rebuilt)tipado devolvió el primitivo directamente sin boxing, y la lectura del campostaticusócnt.get(null)— pasarnullcomo destino es la convención para los estáticos. Elegir el accesor tipado para primitivos evita una asignación por lectura, lo que importa en rutas de serialización calientes.