1. Información general
La reflexión es la capacidad del software de computadora para inspeccionar su estructura en tiempo de ejecución. En Java, lo logramos mediante el uso de la API de reflexión de Java . Nos permite inspeccionar los elementos de una clase, como campos, métodos o incluso clases internas, todo en tiempo de ejecución.
Este tutorial se centrará en cómo recuperar los campos de una clase Java, incluidos los campos privados y heredados.
2. Recuperar campos de una clase
Primero echemos un vistazo a cómo recuperar los campos de una clase, independientemente de su visibilidad. Más adelante, veremos cómo obtener campos heredados también.
Comencemos con un ejemplo de una clase Person con dos campos String : lastName y firstName . El primero está protegido (que será útil más adelante) mientras que el segundo es privado:
public class Person { protected String lastName; private String firstName; }
Queremos obtener los campos lastName y firstName usando la reflexión. Lo lograremos usando el método Class :: getDeclaredFields . Como sugiere su nombre, esto devuelve todos los campos declarados de una clase, en forma de matriz de campo :
public class PersonAndEmployeeReflectionUnitTest { /* ... constants ... */ @Test public void givenPersonClass_whenGetDeclaredFields_thenTwoFields() { Field[] allFields = Person.class.getDeclaredFields(); assertEquals(2, allFields.length); assertTrue(Arrays.stream(allFields).anyMatch(field -> field.getName().equals(LAST_NAME_FIELD) && field.getType().equals(String.class)) ); assertTrue(Arrays.stream(allFields).anyMatch(field -> field.getName().equals(FIRST_NAME_FIELD) && field.getType().equals(String.class)) ); } }
Como podemos ver, obtenemos los dos campos de la clase Person . Verificamos sus nombres y tipos que coinciden con las definiciones de campos en la clase Person .
3. Recuperación de campos heredados
Veamos ahora cómo obtener los campos heredados de una clase Java.
Para ilustrar esto, creemos una segunda clase llamada Employee extendiendo Person , con un campo propio:
public class Employee extends Person { public int employeeId; }
3.1. Recuperar campos heredados en una jerarquía de clases simple
El uso de Employee.class.getDeclaredFields () solo devolvería el campo employeeId , ya que este método no devuelve los campos declarados en superclases. Para obtener también los campos heredados, también debemos obtener los campos de la superclase Person .
Por supuesto, podríamos usar el método getDeclaredFields () en las clases Person y Employee y fusionar sus resultados en una sola matriz. Pero, ¿y si no queremos especificar explícitamente la superclase?
En este caso, podemos hacer uso de otro método de la API de reflexión de Java : Class :: getSuperclass . Esto nos da la superclase de otra clase, sin que necesitemos saber cuál es esa superclase.
Recopilemos los resultados de getDeclaredFields () en Employee.class y Employee.class.getSuperclass () y fusionámoslos en una sola matriz:
@Test public void givenEmployeeClass_whenGetDeclaredFieldsOnBothClasses_thenThreeFields() { Field[] personFields = Employee.class.getSuperclass().getDeclaredFields(); Field[] employeeFields = Employee.class.getDeclaredFields(); Field[] allFields = new Field[employeeFields.length + personFields.length]; Arrays.setAll(allFields, i -> (i < personFields.length ? personFields[i] : employeeFields[i - personFields.length])); assertEquals(3, allFields.length); Field lastNameField = allFields[0]; assertEquals(LAST_NAME_FIELD, lastNameField.getName()); assertEquals(String.class, lastNameField.getType()); Field firstNameField = allFields[1]; assertEquals(FIRST_NAME_FIELD, firstNameField.getName()); assertEquals(String.class, firstNameField.getType()); Field employeeIdField = allFields[2]; assertEquals(EMPLOYEE_ID_FIELD, employeeIdField.getName()); assertEquals(int.class, employeeIdField.getType()); }
Podemos ver aquí que hemos reunido los dos campos de Persona , así como el campo único de Empleado .
Pero, ¿es el campo privado de Person realmente un campo heredado? No tanto. Eso sería lo mismo para un campo de paquete privado . Solo los campos públicos y protegidos se consideran heredados.
3.2. Filtrado de campos públicos y protegidos
Desafortunadamente, ningún método en la API de Java nos permite recopilar campos públicos y protegidos de una clase y sus superclases. El método Class :: getFields se acerca a nuestro objetivo ya que devuelve todos los campos públicos de una clase y sus superclases, pero no los protegidos .
La única forma que tenemos de obtener solo campos heredados es usar el método getDeclaredFields () , como acabamos de hacer, y filtrar sus resultados usando el método Field :: getModifiers . Éste devuelve un int que representa los modificadores del campo actual. A cada posible modificador se le asigna una potencia de dos entre 2 ^ 0 y 2 ^ 7 .
Por ejemplo, public es 2 ^ 0 y static es 2 ^ 3 . Por lo tanto, llamar al método getModifiers () en un campo público y estático devolvería 9.
Luego, es posible realizar un bit a bit y entre este valor y el valor de un modificador específico para ver si ese campo tiene ese modificador. Si la operación devuelve algo distinto de 0, se aplica el modificador; de lo contrario, no.
Tenemos suerte ya que Java nos proporciona una clase de utilidad para comprobar si hay modificadores presentes en el valor devuelto por getModifiers () . Usemos los métodos isPublic () e isProtected () para recopilar solo campos heredados en nuestro ejemplo:
List personFields = Arrays.stream(Employee.class.getSuperclass().getDeclaredFields()) .filter(f -> Modifier.isPublic(f.getModifiers()) || Modifier.isProtected(f.getModifiers())) .collect(Collectors.toList()); assertEquals(1, personFields.size()); assertTrue(personFields.stream().anyMatch(field -> field.getName().equals(LAST_NAME_FIELD) && field.getType().equals(String.class)) );
Como podemos ver, el resultado ya no incluye el campo privado .
3.3. Recuperar campos heredados en una jerarquía de clases profunda
En el ejemplo anterior, trabajamos en una jerarquía de clases única. ¿Qué hacemos ahora si tenemos una jerarquía de clases más profunda y queremos reunir todos los campos heredados?
Supongamos que tenemos una subclase de Empleado o una superclase de Persona; luego, obtener los campos de toda la jerarquía requerirá verificar todas las superclases.
Podemos lograr eso creando un método de utilidad que se ejecute a través de la jerarquía, generando el resultado completo para nosotros:
List getAllFields(Class clazz) { if (clazz == null) { return Collections.emptyList(); } List result = new ArrayList(getAllFields(clazz.getSuperclass())); List filteredFields = Arrays.stream(clazz.getDeclaredFields()) .filter(f -> Modifier.isPublic(f.getModifiers()) || Modifier.isProtected(f.getModifiers())) .collect(Collectors.toList()); result.addAll(filteredFields); return result; }
Este método recursivo buscará campos públicos y protegidos a través de la jerarquía de clases y devuelve todo lo que se ha encontrado en una lista .
Vamos a ilustrarlo con una pequeña prueba en una nueva clase MonthEmployee , ampliando la de Employee :
public class MonthEmployee extends Employee { protected double reward; }
Esta clase define un nuevo campo: recompensa . Dada toda la clase de jerarquía, nuestro método debería darnos las siguientes definiciones de campos : Person :: lastName, Employee :: employeeId y MonthEmployee :: recompensa .
Llamemos al método getAllFields () en MonthEmployee :
@Test public void givenMonthEmployeeClass_whenGetAllFields_thenThreeFields() { List allFields = getAllFields(MonthEmployee.class); assertEquals(3, allFields.size()); assertTrue(allFields.stream().anyMatch(field -> field.getName().equals(LAST_NAME_FIELD) && field.getType().equals(String.class)) ); assertTrue(allFields.stream().anyMatch(field -> field.getName().equals(EMPLOYEE_ID_FIELD) && field.getType().equals(int.class)) ); assertTrue(allFields.stream().anyMatch(field -> field.getName().equals(MONTH_EMPLOYEE_REWARD_FIELD) && field.getType().equals(double.class)) ); }
Como era de esperar, reunimos a todos los campos públicos y protegidos .
4. Conclusión
En este artículo, vimos cómo recuperar los campos de una clase Java utilizando la API de reflexión de Java .
Primero aprendimos cómo recuperar los campos declarados de una clase. Después de eso, vimos cómo recuperar sus campos de superclase también. Luego, aprendimos a filtrar los campos no públicos y no protegidos .
Finalmente, vimos cómo aplicar todo esto para recopilar los campos heredados de una jerarquía de clases múltiples.
Como de costumbre, el código completo de este artículo está disponible en nuestro GitHub.