Guia para reflexão em Java
1. Visão geral
Neste artigo, exploraremos a reflexão em java, que nos permite inspecionar ou modificar atributos de tempo de execução de classes, interfaces, campos e métodos. Isso é particularmente útil quando não sabemos seus nomes no momento da compilação.
Além disso, podemos instanciar novos objetos, invocar métodos e obter ou definir valores de campo usando reflexão.
2. Configuração do Projeto
To use java reflection, we do not need to include any special jars, qualquer configuração especial ou dependências Maven. O JDK é enviado com um grupo de classes que são agrupadas no pacotejava.lang.reflect especificamente para esse propósito.
Então, tudo o que precisamos fazer é fazer a seguinte importação no nosso código:
import java.lang.reflect.*;
e estamos prontos para ir.
Para obter acesso às informações de classe, método e campo de uma instância, chamamos o métodogetClass que retorna a representação da classe em tempo de execução do objeto. O objetoclass retornado fornece métodos para acessar informações sobre uma classe.
3. Exemplo Simples
Para molhar os pés, vamos dar uma olhada em um exemplo muito básico que inspeciona os campos de um simples objeto java em tempo de execução.
Vamos criar uma classePerson simples com apenas os camposnameeage e nenhum método. Aqui está a classe Person:
public class Person {
private String name;
private int age;
}
Agora usaremos a reflexão java para descobrir os nomes de todos os campos dessa classe. Para apreciar o poder de reflexão, construiremos um objetoPerson e usaremos Object como o tipo de referência:
@Test
public void givenObject_whenGetsFieldNamesAtRuntime_thenCorrect() {
Object person = new Person();
Field[] fields = person.getClass().getDeclaredFields();
List actualFieldNames = getFieldNames(fields);
assertTrue(Arrays.asList("name", "age")
.containsAll(actualFieldNames));
}
Este teste nos mostra que somos capazes de obter um array de objetosField de nosso objetoperson, mesmo se a referência ao objeto for um tipo pai desse objeto.
No exemplo acima, estávamos interessados apenas nos nomes desses campos, mas há muito mais a ser feito e veremos mais exemplos disso nas seções subseqüentes.
Observe como usamos um método auxiliar para extrair os nomes de campo reais, é um código muito básico:
private static List getFieldNames(Field[] fields) {
List fieldNames = new ArrayList<>();
for (Field field : fields)
fieldNames.add(field.getName());
return fieldNames;
}
4. Casos de uso do Java Reflection
Antes de prosseguirmos para os diferentes recursos da reflexão em java, discutiremos alguns dos usos comuns que podemos encontrar para ela. A reflexão sobre Java é extremamente poderosa e pode ser muito útil de várias maneiras.
Por exemplo, em muitos casos, temos uma convenção de nomenclatura para tabelas de banco de dados. Podemos escolher adicionar consistência ao pré-fixar os nomes de nossas tabelas comtbl_, de forma que uma tabela com dados de alunos seja chamadatbl_student_data.
Nesses casos, podemos nomear o objeto java que contém os dados do aluno comoStudent ouStudentData. Então, usando o paradigma CRUD, temos um ponto de entrada para cada operação de modo que as operaçõesCreate recebam apenas um ParâmetroObject.
Em seguida, usamos a reflexão para recuperar o nome do objeto e os nomes dos campos. Nesse ponto, podemos mapear esses dados para uma tabela de banco de dados e atribuir os valores do campo de objeto aos nomes de campo de banco de dados apropriados.
5. Inspecionando classes Java
Nesta seção, exploraremos o componente mais fundamental na API de reflexão do java. objetos de classe java, como mencionamos anteriormente, nos dão acesso aos detalhes internos de qualquer objeto.
Vamos examinar detalhes internos, como o nome da classe de um objeto, seus modificadores, campos, métodos, interfaces implementadas etc.
5.1. Preparando-se
Para obter um controle firme sobre a API de reflexão aplicada às classes java e ter exemplos com variedade, criaremos uma classeAnimal abstrata que implementa a interfaceEating. Essa interface define o comportamento alimentar de qualquer objetoAnimal concreto que criamos.
Em primeiro lugar, aqui está a interfaceEating:
public interface Eating {
String eats();
}
e então a implementação concreta deAnimal da interfaceEating:
public abstract class Animal implements Eating {
public static String CATEGORY = "domestic";
private String name;
protected abstract String getSound();
// constructor, standard getters and setters omitted
}
Vamos também criar outra interface chamadaLocomotion que descreve como um animal se move:
public interface Locomotion {
String getLocomotion();
}
Vamos agora criar uma classe concreta chamadaGoat que estendeAnimale implementaLocomotion. Uma vez que a superclasse implementaEating,Goat terá que implementar os métodos dessa interface também:
public class Goat extends Animal implements Locomotion {
@Override
protected String getSound() {
return "bleat";
}
@Override
public String getLocomotion() {
return "walks";
}
@Override
public String eats() {
return "grass";
}
// constructor omitted
}
A partir deste ponto, usaremos a reflexão java para inspecionar aspectos de objetos java que aparecem nas classes e interfaces acima.
5.2. Nomes de classe
Vamos começar obtendo o nome de um objeto deClass:
@Test
public void givenObject_whenGetsClassName_thenCorrect() {
Object goat = new Goat("goat");
Class> clazz = goat.getClass();
assertEquals("Goat", clazz.getSimpleName());
assertEquals("com.example.reflection.Goat", clazz.getName());
assertEquals("com.example.reflection.Goat", clazz.getCanonicalName());
}
Observe que o métodogetSimpleName deClass retorna o nome básico do objeto como apareceria em sua declaração. Os outros dois métodos retornam o nome completo da classe, incluindo a declaração do pacote.
Vejamos também como podemos criar um objeto da classeGoat se soubermos apenas o nome de classe totalmente qualificado:
@Test
public void givenClassName_whenCreatesObject_thenCorrect(){
Class> clazz = Class.forName("com.example.reflection.Goat");
assertEquals("Goat", clazz.getSimpleName());
assertEquals("com.example.reflection.Goat", clazz.getName());
assertEquals("com.example.reflection.Goat", clazz.getCanonicalName());
}
Observe que o nome que passamos para o método estáticoforName deve incluir as informações do pacote, caso contrário, obteremos umClassNotFoundException.
5.3. Modificadores de classe
Podemos determinar os modificadores usados em uma classe chamando o métodogetModifiers que retorna umInteger.. Cada modificador é um bit de sinalizador que é definido ou apagado.
A classejava.lang.reflect.Modifier oferece métodos estáticos que analisam oInteger retornado para a presença ou ausência de um modificador específico.
Vamos confirmar os modificadores de algumas das classes que definimos acima:
@Test
public void givenClass_whenRecognisesModifiers_thenCorrect() {
Class> goatClass = Class.forName("com.example.reflection.Goat");
Class> animalClass = Class.forName("com.example.reflection.Animal");
int goatMods = goatClass.getModifiers();
int animalMods = animalClass.getModifiers();
assertTrue(Modifier.isPublic(goatMods));
assertTrue(Modifier.isAbstract(animalMods));
assertTrue(Modifier.isPublic(animalMods));
}
Podemos inspecionar modificadores de qualquer classe localizada em um jar de biblioteca que estamos importando para o nosso projeto.
Na maioria dos casos, podemos precisar usar a abordagemforName em vez da instanciação completa, pois isso seria um processo caro no caso de classes com muita memória.
5.4. Informação do pacote
Usando a reflexão java, também podemos obter informações sobre o pacote de qualquer classe ou objeto. Esses dados são agrupados dentro da classePackage que é retornada por uma chamada ao métodogetPackage no objeto de classe.
Vamos executar um teste para recuperar o nome do pacote:
@Test
public void givenClass_whenGetsPackageInfo_thenCorrect() {
Goat goat = new Goat("goat");
Class> goatClass = goat.getClass();
Package pkg = goatClass.getPackage();
assertEquals("com.example.reflection", pkg.getName());
}
5.5. Super Class
Também podemos obter a superclasse de qualquer classe java usando a reflexão java.
Em muitos casos, especialmente ao usar classes de biblioteca ou classes integradas de java, podemos não saber de antemão a superclasse de um objeto que estamos usando, esta subseção mostrará como obter essas informações.
Então, vamos prosseguir e determinar a superclasse deGoat, adicionalmente, também mostramos que a classejava.lang.String é uma subclasse da classejava.lang.Object:
@Test
public void givenClass_whenGetsSuperClass_thenCorrect() {
Goat goat = new Goat("goat");
String str = "any string";
Class> goatClass = goat.getClass();
Class> goatSuperClass = goatClass.getSuperclass();
assertEquals("Animal", goatSuperClass.getSimpleName());
assertEquals("Object", str.getClass().getSuperclass().getSimpleName());
}
5.6. Interfaces Implementadas
Usando reflexão de java, também somos capazes deget the list of interfaces implemented by a given class.
Vamos recuperar os tipos de classe das interfaces implementadas pela classeGoat e pela classe abstrataAnimal:
@Test
public void givenClass_whenGetsImplementedInterfaces_thenCorrect(){
Class> goatClass = Class.forName("com.example.reflection.Goat");
Class> animalClass = Class.forName("com.example.reflection.Animal");
Class>[] goatInterfaces = goatClass.getInterfaces();
Class>[] animalInterfaces = animalClass.getInterfaces();
assertEquals(1, goatInterfaces.length);
assertEquals(1, animalInterfaces.length);
assertEquals("Locomotion", goatInterfaces[0].getSimpleName());
assertEquals("Eating", animalInterfaces[0].getSimpleName());
}
Observe pelas afirmações de que cada classe implementa apenas uma única interface. Examinando os nomes dessas interfaces, descobrimos queGoat implementaLocomotioneAnimal implementaEating, exatamente como aparece em nosso código.
Você deve ter observado queGoat é uma subclasse da classe abstrataAnimale implementa o método de interfaceeats(), entãoGoat também implementa a interfaceEating.
Portanto, é importante notar que apenas as interfaces que uma classe declara explicitamente como implementadas com a palavra-chaveimplements aparecem no array retornado.
Portanto, mesmo que uma classe implemente métodos de interface porque é uma superclasse que implementa essa interface, mas a subclasse não declara diretamente essa interface com a palavra-chaveimplements, então essa interface não aparecerá na matriz de interfaces.
5.7. Construtores, métodos e campos
Com a reflexão java, somos capazes de inspecionar os construtores de qualquer classe de objeto, bem como métodos e campos.
Mais tarde, poderemos ver inspeções mais profundas em cada um desses componentes de uma classe, mas, por enquanto, basta apenas obter seus nomes e compará-los com o que esperamos.
Vamos ver como obter o construtor da classeGoat:
@Test
public void givenClass_whenGetsConstructor_thenCorrect(){
Class> goatClass = Class.forName("com.example.reflection.Goat");
Constructor>[] constructors = goatClass.getConstructors();
assertEquals(1, constructors.length);
assertEquals("com.example.reflection.Goat", constructors[0].getName());
}
Também podemos inspecionar os campos da classeAnimal assim:
@Test
public void givenClass_whenGetsFields_thenCorrect(){
Class> animalClass = Class.forName("com.example.java.reflection.Animal");
Field[] fields = animalClass.getDeclaredFields();
List actualFields = getFieldNames(fields);
assertEquals(2, actualFields.size());
assertTrue(actualFields.containsAll(Arrays.asList("name", "CATEGORY")));
}
Assim como podemos inspecionar os métodos da classeAnimal:
@Test
public void givenClass_whenGetsMethods_thenCorrect(){
Class> animalClass = Class.forName("com.example.java.reflection.Animal");
Method[] methods = animalClass.getDeclaredMethods();
List actualMethods = getMethodNames(methods);
assertEquals(4, actualMethods.size());
assertTrue(actualMethods.containsAll(Arrays.asList("getName",
"setName", "getSound")));
}
Assim comogetFieldNames, adicionamos um método auxiliar para recuperar nomes de métodos de uma matriz de objetosMethod:
private static List getMethodNames(Method[] methods) {
List methodNames = new ArrayList<>();
for (Method method : methods)
methodNames.add(method.getName());
return methodNames;
}
6. Inspecionando Construtores
Com a reflexão de java, podemosinspect constructors de qualquer classe e atécreate class objects at runtime. Isso é possível pela classejava.lang.reflect.Constructor.
Anteriormente, vimos apenas como obter a matriz de objetosConstructor, a partir da qual fomos capazes de obter os nomes dos construtores.
Nesta seção, focaremos em como recuperar construtores específicos. Em java, como sabemos, dois construtores de uma classe não compartilham exatamente a mesma assinatura de método. Portanto, usaremos essa singularidade para obter um construtor de muitos.
Para apreciar os recursos desta classe, criaremos uma subclasseBird deAnimal com três construtores. Não implementaremosLocomotion para que possamos especificar esse comportamento usando um argumento do construtor, para adicionar ainda mais variedade:
public class Bird extends Animal {
private boolean walks;
public Bird() {
super("bird");
}
public Bird(String name, boolean walks) {
super(name);
setWalks(walks);
}
public Bird(String name) {
super(name);
}
public boolean walks() {
return walks;
}
// standard setters and overridden methods
}
Vamos confirmar por reflexão que esta classe tem três construtores:
@Test
public void givenClass_whenGetsAllConstructors_thenCorrect() {
Class> birdClass = Class.forName("com.example.reflection.Bird");
Constructor>[] constructors = birdClass.getConstructors();
assertEquals(3, constructors.length);
}
A seguir, recuperaremos cada construtor para a classeBird passando os tipos de classe de parâmetro do construtor na ordem declarada:
@Test
public void givenClass_whenGetsEachConstructorByParamTypes_thenCorrect(){
Class> birdClass = Class.forName("com.example.reflection.Bird");
Constructor> cons1 = birdClass.getConstructor();
Constructor> cons2 = birdClass.getConstructor(String.class);
Constructor> cons3 = birdClass.getConstructor(String.class, boolean.class);
}
Não há necessidade de asserção, pois quando não existe um construtor com determinados tipos de parâmetro na ordem fornecida, obteremos umNoSuchMethodExceptione o teste falhará automaticamente.
No último teste, veremos como instanciar objetos em tempo de execução, fornecendo seus parâmetros:
@Test
public void givenClass_whenInstantiatesObjectsAtRuntime_thenCorrect() {
Class> birdClass = Class.forName("com.example.reflection.Bird");
Constructor> cons1 = birdClass.getConstructor();
Constructor> cons2 = birdClass.getConstructor(String.class);
Constructor> cons3 = birdClass.getConstructor(String.class,
boolean.class);
Bird bird1 = (Bird) cons1.newInstance();
Bird bird2 = (Bird) cons2.newInstance("Weaver bird");
Bird bird3 = (Bird) cons3.newInstance("dove", true);
assertEquals("bird", bird1.getName());
assertEquals("Weaver bird", bird2.getName());
assertEquals("dove", bird3.getName());
assertFalse(bird1.walks());
assertTrue(bird3.walks());
}
Instanciamos os objetos de classe chamando o métodonewInstance da classeConstructore passando os parâmetros necessários na ordem declarada. Em seguida, lançamos o resultado no tipo necessário.
Parabird1, usamos o construtor padrão que, a partir de nosso códigoBird, define automaticamente o nome como pássaro e confirmamos isso com um teste.
Em seguida, instanciamosbird2 com apenas um nome e teste também, lembre-se de que quando não definimos o comportamento de locomoção, pois o padrão é falso, como visto nas duas últimas afirmações.
7. Inspecionando Campos
Anteriormente, inspecionávamos apenas os nomes dos campos, nesta seção,we will show how toget and set their values at runtime.
Existem dois métodos principais usados para inspecionar os campos de uma classe em tempo de execução:getFields()egetField(fieldName).
O métodogetFields() retorna todos os campos públicos acessíveis da classe em questão. Ele retornará todos os campos públicos da classe e de todas as super classes.
Por exemplo, quando chamamos este método na classeBird, obteremos apenas o campoCATEGORY de sua superclasse,Animal, já que o próprioBird não declara nenhum campos públicos:
@Test
public void givenClass_whenGetsPublicFields_thenCorrect() {
Class> birdClass = Class.forName("com.example.reflection.Bird");
Field[] fields = birdClass.getFields();
assertEquals(1, fields.length);
assertEquals("CATEGORY", fields[0].getName());
}
Este método também tem uma variante chamadagetField que retorna apenas um objetoField pegando o nome do campo:
@Test
public void givenClass_whenGetsPublicFieldByName_thenCorrect() {
Class> birdClass = Class.forName("com.example.reflection.Bird");
Field field = birdClass.getField("CATEGORY");
assertEquals("CATEGORY", field.getName());
}
Não podemos acessar campos particulares declarados em super classes e não declarados na classe filho. É por isso que não conseguimos acessar o camponame.
No entanto, podemos inspecionar os campos privados declarados na classe com a qual estamos lidando chamando o métodogetDeclaredFields:
@Test
public void givenClass_whenGetsDeclaredFields_thenCorrect(){
Class> birdClass = Class.forName("com.example.reflection.Bird");
Field[] fields = birdClass.getDeclaredFields();
assertEquals(1, fields.length);
assertEquals("walks", fields[0].getName());
}
Também podemos usar sua outra variante caso saibamos o nome do campo:
@Test
public void givenClass_whenGetsFieldsByName_thenCorrect() {
Class> birdClass = Class.forName("com.example.reflection.Bird");
Field field = birdClass.getDeclaredField("walks");
assertEquals("walks", field.getName());
}
Se obtivermos o nome do campo errado ou digitarmos um campo inexistente, obteremos umNoSuchFieldException.
Obtemos o tipo de campo da seguinte maneira:
@Test
public void givenClassField_whenGetsType_thenCorrect() {
Field field = Class.forName("com.example.reflection.Bird")
.getDeclaredField("walks");
Class> fieldClass = field.getType();
assertEquals("boolean", fieldClass.getSimpleName());
}
A seguir, veremos como acessar os valores dos campos e modificá-los. Para ser capaz de obter o valor de um campo, quanto mais defini-lo, devemos primeiro defini-lo como acessível chamando o métodosetAccessible no objetoField e passartrue booleano para ele:
@Test
public void givenClassField_whenSetsAndGetsValue_thenCorrect() {
Class> birdClass = Class.forName("com.example.reflection.Bird");
Bird bird = (Bird) birdClass.newInstance();
Field field = birdClass.getDeclaredField("walks");
field.setAccessible(true);
assertFalse(field.getBoolean(bird));
assertFalse(bird.walks());
field.set(bird, true);
assertTrue(field.getBoolean(bird));
assertTrue(bird.walks());
}
No teste acima, verificamos se realmente o valor do campowalks é falso antes de defini-lo como verdadeiro.
Observe como usamos o objetoField para definir e obter valores, passando a instância da classe com a qual estamos lidando e, possivelmente, o novo valor que queremos que o campo tenha nesse objeto.
Uma coisa importante a notar sobre objetosField é que quando ele é declarado comopublic static, então não precisamos de uma instância da classe que os contém, podemos apenas passarnull em seu coloque e ainda obtenha o valor padrão do campo, assim:
@Test
public void givenClassField_whenGetsAndSetsWithNull_thenCorrect(){
Class> birdClass = Class.forName("com.example.reflection.Bird");
Field field = birdClass.getField("CATEGORY");
field.setAccessible(true);
assertEquals("domestic", field.get(null));
}
8. Métodos de inspeção
No exemplo anterior, usamos a reflexão apenas para inspecionar os nomes dos métodos. No entanto, a reflexão de java é mais poderosa que isso.
Com a reflexão de java, podemosinvoke methods atruntimee passar os parâmetros necessários, assim como fizemos para os construtores. Da mesma forma, também podemos chamar métodos sobrecarregados especificando os tipos de parâmetros de cada um.
Assim como os campos, existem dois métodos principais que usamos para recuperar métodos de classe. O métodogetMethods retorna uma matriz de todos os métodos públicos da classe e superclasses.
Isso significa que, com este método, podemos obter métodos públicos da classejava.lang.Object comotoString, hashCodeenotifyAll:
@Test
public void givenClass_whenGetsAllPublicMethods_thenCorrect(){
Class> birdClass = Class.forName("com.example.java.reflection.Bird");
Method[] methods = birdClass.getMethods();
List methodNames = getMethodNames(methods);
assertTrue(methodNames.containsAll(Arrays
.asList("equals", "notifyAll", "hashCode",
"walks", "eats", "toString")));
}
Para obter apenas os métodos públicos da classe em que estamos interessados, temos que usar o métodogetDeclaredMethods:
@Test
public void givenClass_whenGetsOnlyDeclaredMethods_thenCorrect(){
Class> birdClass = Class.forName("com.example.java.reflection.Bird");
List actualMethodNames
= getMethodNames(birdClass.getDeclaredMethods());
List expectedMethodNames = Arrays
.asList("setWalks", "walks", "getSound", "eats");
assertEquals(expectedMethodNames.size(), actualMethodNames.size());
assertTrue(expectedMethodNames.containsAll(actualMethodNames));
assertTrue(actualMethodNames.containsAll(expectedMethodNames));
}
Cada um desses métodos tem uma variação singular que retorna um único objetoMethod cujo nome conhecemos:
@Test
public void givenMethodName_whenGetsMethod_thenCorrect() {
Class> birdClass = Class.forName("com.example.reflection.Bird");
Method walksMethod = birdClass.getDeclaredMethod("walks");
Method setWalksMethod = birdClass.getDeclaredMethod("setWalks", boolean.class);
assertFalse(walksMethod.isAccessible());
assertFalse(setWalksMethod.isAccessible());
walksMethod.setAccessible(true);
setWalksMethod.setAccessible(true);
assertTrue(walksMethod.isAccessible());
assertTrue(setWalksMethod.isAccessible());
}
Observe como recuperamos métodos individuais e especifique quais tipos de parâmetros eles usam. Aqueles que não aceitam tipos de parâmetro são recuperados com um argumento de variável vazio, deixando-nos com apenas um único argumento, o nome do método.
A seguir, mostraremos como chamar um método em tempo de execução. Sabemos por padrão que o atributowalks da classeBird éfalse, queremos chamar seu métodosetWalks e defini-lo comotrue:
@Test
public void givenMethod_whenInvokes_thenCorrect() {
Class> birdClass = Class.forName("com.example.reflection.Bird");
Bird bird = (Bird) birdClass.newInstance();
Method setWalksMethod = birdClass.getDeclaredMethod("setWalks", boolean.class);
Method walksMethod = birdClass.getDeclaredMethod("walks");
boolean walks = (boolean) walksMethod.invoke(bird);
assertFalse(walks);
assertFalse(bird.walks());
setWalksMethod.invoke(bird, true);
boolean walks2 = (boolean) walksMethod.invoke(bird);
assertTrue(walks2);
assertTrue(bird.walks());
}
Observe como primeiro invocamos o métodowalks, convertemos o tipo de retorno para o tipo de dados apropriado e, em seguida, verificamos seu valor. Depois, invocamos o métodosetWalks para alterar esse valor e testar novamente.
9. Conclusão
Neste tutorial, abordamos a API Java Reflection e analisamos como usá-la para inspecionar classes, interfaces, campos e métodos em tempo de execução sem o conhecimento prévio de suas partes internas em tempo de compilação.
O código-fonte completo e exemplos para este tutorial podem ser encontrados em meu projetoGithub.