Recupera i campi da una classe Java utilizzando Reflection

1. Panoramica

La riflessione è la capacità del software per computer di ispezionare la sua struttura in fase di esecuzione. In Java, otteniamo questo risultato utilizzando l' API Java Reflection . Ci permette di ispezionare gli elementi di una classe come campi, metodi o anche classi interne, tutto in fase di runtime.

Questo tutorial si concentrerà su come recuperare i campi di una classe Java, inclusi i campi privati ​​ed ereditati.

2. Recupero di campi da una classe

Diamo prima uno sguardo a come recuperare i campi di una classe, indipendentemente dalla loro visibilità. Più avanti vedremo come ottenere anche i campi ereditati.

Cominciamo con un esempio di una classe Person con due campi String : lastName e firstName . Il primo è protetto (che sarà utile in seguito) mentre il secondo è privato:

public class Person { protected String lastName; private String firstName; }

Vogliamo ottenere entrambi i campi lastName e firstName usando la reflection. Otterremo questo risultato usando il metodo Class :: getDeclaredFields . Come suggerisce il nome, restituisce tutti i campi dichiarati di una classe, sotto forma di un array di campi :

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)) ); } }

Come possiamo vedere, otteniamo i due campi della classe Person . Controlliamo i loro nomi e tipi che corrispondono alle definizioni dei campi nella classe Person .

3. Recupero dei campi ereditati

Vediamo ora come ottenere i campi ereditati di una classe Java.

Per illustrare ciò, creiamo una seconda classe denominata Employee extending Person , con un proprio campo:

public class Employee extends Person { public int employeeId; }

3.1. Recupero di campi ereditati in una gerarchia di classi semplice

Utilizzando Employee.class.getDeclaredFields () sarebbe tornato solo l'employeeId campo , in quanto questo metodo non restituisce i campi dichiarati nel superclassi. Per ottenere anche campi ereditati dobbiamo anche ottenere i campi della superclasse Persona .

Ovviamente, potremmo usare il metodo getDeclaredFields () su entrambe le classi Person e Employee e unire i loro risultati in un unico array. Ma cosa succede se non vogliamo specificare esplicitamente la superclasse?

In questo caso, possiamo utilizzare un altro metodo dell'API Java Reflection : Class :: getSuperclass . Questo ci dà la superclasse di un'altra classe, senza che abbiamo bisogno di sapere cosa sia quella superclasse.

Raccogliamo i risultati di getDeclaredFields () su Employee.class e Employee.class.getSuperclass () e uniscili in un unico array:

@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()); }

Possiamo vedere qui che abbiamo raccolto i due campi di Persona e il singolo campo di Dipendente .

Ma il campo privato della Persona è davvero un campo ereditato? Non così tanto. Sarebbe lo stesso per un campo privato del pacchetto . Solo i campi pubblici e protetti sono considerati ereditati.

3.2. Filtraggio di campi pubblici e protetti

Sfortunatamente, nessun metodo nell'API Java ci consente di raccogliere campi pubblici e protetti da una classe e dalle sue superclassi. Il metodo Class :: getFields si avvicina al nostro obiettivo in quanto restituisce tutti i campi pubblici di una classe e delle sue superclassi, ma non quelli protetti .

L'unico modo per ottenere solo i campi ereditati è utilizzare il metodo getDeclaredFields () , come abbiamo appena fatto, e filtrarne i risultati utilizzando il metodo Field :: getModifiers . Questo restituisce un int che rappresenta i modificatori del campo corrente. Ad ogni possibile modificatore viene assegnata una potenza di due tra 2 ^ 0 e 2 ^ 7 .

Ad esempio, public è 2 ^ 0 e static è 2 ^ 3 . Pertanto, la chiamata del metodo getModifiers () su un campo pubblico e statico restituirà 9.

Quindi, è possibile eseguire un bit per bit e tra questo valore e il valore di un modificatore specifico per vedere se quel campo ha quel modificatore. Se l'operazione restituisce qualcosa di diverso da 0, viene applicato il modificatore, altrimenti no.

Siamo fortunati perché Java ci fornisce una classe di utilità per verificare se sono presenti modificatori nel valore restituito da getModifiers () . Usiamo i metodi isPublic () e isProtected () per raccogliere solo i campi ereditati nel nostro esempio:

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)) );

Come possiamo vedere, il risultato non porta più il campo privato .

3.3. Recupero di campi ereditati in una gerarchia di classi profonda

Nell'esempio sopra, abbiamo lavorato su una singola gerarchia di classi. Cosa facciamo ora se abbiamo una gerarchia di classi più profonda e vogliamo raccogliere tutti i campi ereditati?

Supponiamo di avere una sottoclasse di Employee o una superclasse di Person - quindi ottenere i campi dell'intera gerarchia richiederà di controllare tutte le superclassi.

Possiamo ottenerlo creando un metodo di utilità che attraversi la gerarchia, costruendo per noi il risultato completo:

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; }

Questo metodo ricorsivo cercherà i campi pubblici e protetti attraverso la gerarchia di classi e restituirà tutto ciò che è stato trovato in un elenco .

Illustriamolo con un piccolo test su una nuova classe MonthEmployee , estendendo quella Employee :

public class MonthEmployee extends Employee { protected double reward; }

Questa classe definisce un nuovo campo: ricompensa . Data tutta la classe gerarchica, il nostro metodo dovrebbe fornirci le seguenti definizioni di campi : Person :: lastName, Employee :: EmployeeId e MonthEmployee :: reward .

Chiamiamo il metodo getAllFields () su 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)) ); }

Come previsto, raccogliamo tutti i campi pubblici e protetti .

4. Conclusione

In questo articolo, abbiamo visto come recuperare i campi di una classe Java utilizzando l' API Java Reflection .

Per prima cosa abbiamo imparato come recuperare i campi dichiarati di una classe. Successivamente, abbiamo visto come recuperare anche i suoi campi di superclasse. Quindi, abbiamo imparato a filtrare i campi non pubblici e non protetti .

Infine, abbiamo visto come applicare tutto questo per raccogliere i campi ereditati di una gerarchia di classi multiple.

Come al solito, il codice completo di questo articolo è disponibile sul nostro GitHub.