Para que serve a API de reflexão?

O mecanismo de reflexão do Java permite que um desenvolvedor faça alterações e obtenha informações sobre classes, interfaces, campos e métodos em tempo de execução sem saber seus nomes.

A API de reflexão também permite criar novos objetos, chamar métodos e obter ou definir valores de campo.

Vamos fazer uma lista de tudo o que você pode fazer usando a reflexão:

  • Identificar/determinar a classe de um objeto
  • Obtenha informações sobre modificadores de classe, campos, métodos, constantes, construtores e superclasses
  • Descubra quais métodos pertencem à(s) interface(s) implementada(s)
  • Crie uma instância de uma classe cujo nome de classe não seja conhecido até que o programa seja executado
  • Obter e definir o valor de um campo de instância por nome
  • Chamar um método de instância pelo nome

Quase todas as tecnologias Java modernas usam reflexão. Ele é a base da maioria das estruturas e bibliotecas Java/Java EE atuais, por exemplo:

  • Estruturas Spring para criar aplicativos da Web
  • o framework de teste JUnit

Se você nunca encontrou esses mecanismos antes, provavelmente está se perguntando por que tudo isso é necessário. A resposta é bastante simples, mas também muito vaga: a reflexão aumenta drasticamente a flexibilidade e a capacidade de personalizar nosso aplicativo e código.

Mas sempre há prós e contras. Então, vamos mencionar alguns contras:

  • Violações de segurança do aplicativo. A reflexão nos permite acessar o código que não deveríamos (violação de encapsulamento).
  • Restrições de segurança. A reflexão requer permissões de tempo de execução que não estão disponíveis para sistemas que executam um gerenciador de segurança.
  • Baixa performance. A reflexão em Java determina os tipos dinamicamente, examinando o classpath para localizar a classe a ser carregada. Isso reduz o desempenho do programa.
  • Difícil de manter. O código que usa reflexão é difícil de ler e depurar. É menos flexível e mais difícil de manter.

Trabalhando com classes usando a API de reflexão

Todas as operações de reflexão começam com um objeto java.lang.Class . Para cada tipo de objeto, uma instância imutável de java.lang.Class é criada. Ele fornece métodos para obter propriedades de objetos, criar novos objetos e chamar métodos.

Vejamos a lista de métodos básicos para trabalhar com java.lang.Class :

Método Ação
String getNome(); Retorna o nome da classe
int getModifiers(); Retorna modificadores de acesso
Pacote getPackage(); Retorna informações sobre um pacote
Classe getSuperclass(); Retorna informações sobre uma classe pai
Classe[] getInterfaces(); Retorna um array de interfaces
Construtor[] getConstructors(); Retorna informações sobre construtores de classe
Campos[] getCampos(); Retorna os campos de uma classe
Campo getField(String fieldName); Retorna um campo específico de uma classe por nome
Método[] getMethods(); Retorna um array de métodos

Esses são os métodos mais importantes para obter dados sobre classes, interfaces, campos e métodos. Também existem métodos que permitem obter ou definir valores de campo e acessar campos privados . Vamos olhar para eles um pouco mais tarde.

Agora falaremos sobre como obter a própria java.lang.Class . Temos três maneiras de fazer isso.

1. Usando Class.forName

Em um aplicativo em execução, você deve usar o método forName(String className) para obter uma classe.

Este código demonstra como podemos criar classes usando reflexão. Vamos criar uma classe Person com a qual podemos trabalhar:


package com.company;

public class Person {
    private int age;
    private String name;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

E a segunda parte do nosso exemplo é o código que usa reflexão:


public class TestReflection {
    public static void main(String[] args) {
        try {
            Class<?> aClass = Class.forName("com.company.Person");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Essa abordagem é possível se o nome completo da classe for conhecido. Então você pode obter a classe correspondente usando o método estático Class.forName() . Este método não pode ser usado para tipos primitivos.

2. Usando .class

Se um tipo estiver disponível, mas não houver nenhuma instância dele, você poderá obter a classe adicionando .class ao nome do tipo. Esta é a maneira mais fácil de obter a classe de um tipo primitivo.


Class aClass = Person.class;

3. Usando .getClass()

Se um objeto estiver disponível, a maneira mais fácil de obter uma classe é chamar object.getClass() .


Person person = new Person();
Class aClass = person.getClass();

Qual é a diferença entre as duas últimas abordagens?

Use A.class se você souber em qual objeto de classe está interessado no momento da codificação. Se nenhuma instância estiver disponível, você deve usar .class .

Obtendo os métodos de uma classe

Vejamos os métodos que retornam os métodos da nossa classe: getDeclaredMethods() e getMethods() .

getDeclaredMethods() retorna uma matriz que contém objetos Method para todos os métodos declarados da classe ou interface representada pelo objeto Class, incluindo métodos públicos, privados, padrão e protegidos, mas não métodos herdados.

getMethods() retorna um array contendo objetos Method para todos os métodos públicos da classe ou interface representados pelo objeto Class — aqueles declarados pela classe ou interface, bem como aqueles herdados de superclasses e superinterfaces.

Vamos dar uma olhada em como cada um deles funciona.

Vamos começar com getDeclaredMethods() . Para novamente nos ajudar a entender a diferença entre os dois métodos, abaixo trabalharemos com a classe abstrata Numbers . Vamos escrever um método estático que converterá nosso array Method em List<String> :


import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class TestReflection {
    public static void main(String[] args) {
        final Method[] declaredMethods = Number.class.getDeclaredMethods();
        List<String> actualMethodNames = getMethodNames(declaredMethods);
        actualMethodNames.forEach(System.out::println);
    }

    private static List<String> getMethodNames(Method[] methods) {
        return Arrays.stream(methods)
                .map(Method::getName)
                .collect(Collectors.toList());
    }
}

Aqui está o resultado da execução deste código:

byteValue
shortValue
intValue
longValue
float floatValue;
doubleValue

Esses são os métodos declarados dentro da classe Number . O que getMethods () retorna? Vamos alterar duas linhas no exemplo:


final Method[] methods = Number.class.getMethods();
List<String> actualMethodNames = getMethodNames(methods);

Fazendo isso, veremos o seguinte conjunto de métodos:

byteValue
shortValue
intValue
longValue
float floatValue;
doubleValue
esperar
esperar
esperar
igual a
toString
hashCode
getClass
notificar
notifyAll

Como todas as classes herdam Object , nosso método também retorna os métodos públicos da classe Object .

Obtendo os campos de uma classe

Os métodos getFields e getDeclaredFields são usados ​​para obter os campos de uma classe. Como exemplo, vejamos a classe LocalDateTime . Vamos reescrever nosso código:


import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class TestReflection {
    public static void main(String[] args) {
        final Field[] declaredFields = LocalDateTime.class.getDeclaredFields();
        List<String> actualFieldNames = getFieldNames(declaredFields);
        actualFieldNames.forEach(System.out::println);
    }

    private static List<String> getFieldNames(Field[] fields) {
        return Arrays.stream(fields)
                .map(Field::getName)
                .collect(Collectors.toList());
    }
}

Como resultado da execução desse código, obtemos o conjunto de campos contidos na classe LocalDateTime.

MIN
MAX
serialVersionUID
data
hora

Por analogia com nosso exame anterior de métodos, vejamos o que acontece se mudarmos um pouco o código:


final Field[] fields = LocalDateTime.class.getFields();
List<String> actualFieldNames = getFieldNames(fields);

Saída:

MIN
MAX

Agora vamos descobrir a diferença entre esses métodos.

O método getDeclaredFields retorna um array de objetos Field para todos os campos declarados pela classe ou interface representada por esteAulaobjeto.

O método getFields retorna um array de objetos Field para todos os campos públicos da classe ou interface representada peloAulaobjeto.

Agora vamos olhar dentro de LocalDateTime .

o da turmaMINeMAXos campos são públicos, o que significa que serão visíveis através do método getFields . Em contraste, odata,tempo,serialVersionUIDOs métodos possuem o modificador private , o que significa que não serão visíveis através do método getFields , mas podemos obtê-los usando getDeclaredFields . É assim que podemos acessar os objetos Field para campos privados .

Descrições de outros métodos

Agora é hora de falar sobre alguns métodos da classe Class , a saber:

Método Ação
getModifiers Obtendo os modificadores para nossa classe
getPackage Obtendo o pacote que contém nossa classe
getSuperclass Obtendo a classe pai
getInterfaces Obtendo uma matriz de interfaces implementadas por uma classe
obterNome Obtendo o nome de classe totalmente qualificado
getSimpleName Obtendo o nome de uma classe

getModifiers()

Modificadores podem ser acessados ​​usando umAulaobjeto.

Modificadores são palavras-chave como public , static , interface , etc. Obtemos modificadores usando o método getModifiers() :


Class<Person> personClass = Person.class;
int classModifiers = personClass.getModifiers();

Este código define o valor de umintvariável que é um campo de bit. Cada modificador de acesso pode ser ativado ou desativado configurando ou limpando o bit correspondente. Podemos verificar modificadores usando os métodos na classe java.lang.reflect.Modifier :


import com.company.Person;
import java.lang.reflect.Modifier;

public class TestReflection {
    public static void main(String[] args) {
        Class<Person> personClass = Person.class;
        int classModifiers = personClass.getModifiers();

        boolean isPublic = Modifier.isPublic(classModifiers);
        boolean isStatic = Modifier.isStatic(classModifiers);
        boolean isFinal = Modifier.isFinal(classModifiers);
        boolean isAbstract = Modifier.isAbstract(classModifiers);
        boolean isInterface = Modifier.isInterface(classModifiers);

        System.out.printf("Class modifiers: %d%n", classModifiers);
        System.out.printf("Is public: %b%n", isPublic);
        System.out.printf("Is static: %b%n", isStatic);
        System.out.printf("Is final: %b%n", isFinal);
        System.out.printf("Is abstract: %b%n", isAbstract);
        System.out.printf("Is interface: %b%n", isInterface);
    }
}

Lembre-se de como é a declaração de nossa Pessoa :


public class Person {
   …
}

Obtemos a seguinte saída:

Modificadores de classe: 1
É público: verdadeiro
É estático: falso
É final: falso
É abstrato: falso
É interface: falso

Se tornarmos nossa classe abstrata, teremos:


public abstract class Person { … }

e esta saída:

Modificadores de classe: 1025
É público: verdadeiro
É estático: falso
É final: falso
É abstrato: verdadeiro
É interface: falso

Alteramos o modificador de acesso, o que significa que também alteramos os dados retornados pelos métodos estáticos da classe Modifier .

getPackage()

Conhecendo apenas uma classe, podemos obter informações sobre seu pacote:


Class<Person> personClass = Person.class;
final Package aPackage = personClass.getPackage();
System.out.println(aPackage.getName());

getSuperclass()

Se tivermos um objeto Class, podemos acessar sua classe pai:


public static void main(String[] args) {
    Class<Person> personClass = Person.class;
    final Class<? super Person> superclass = personClass.getSuperclass();
    System.out.println(superclass);
}

Obtemos a conhecida classe Object :


class java.lang.Object

Mas se nossa classe tiver outra classe pai, veremos isso:


package com.company;

class Human {
    // Some info
}

public class Person extends Human {
    private int age;
    private String name;

    // Some info
}

Aqui temos nossa classe pai:


class com.company.Human

getInterfaces()

Veja como podemos obter a lista de interfaces implementadas pela classe:


public static void main(String[] args) {
    Class<Person> personClass = Person.class;
    final Class<?>[] interfaces = personClass.getInterfaces();
    System.out.println(Arrays.toString(interfaces));
}

E não vamos esquecer de mudar nossa classe Person :


public class Person implements Serializable { … }

Saída:

[interface java.io.Serializable]

Uma classe pode implementar muitas interfaces. É por isso que temos uma série deAulaobjetos. Na API Java Reflection, as interfaces também são representadas porAulaobjetos.

Observação: o método retorna apenas as interfaces implementadas pela classe especificada, não sua classe pai. Para obter uma lista completa de interfaces implementadas pela classe, você precisa se referir à classe atual e a todos os seus ancestrais na cadeia de herança.

getName() & getSimpleName() & getCanonicalName()

Vamos escrever um exemplo envolvendo uma primitiva, uma classe aninhada, uma classe anônima e a classe String :


public class TestReflection {
    public static void main(String[] args) {
        printNamesForClass(int.class, "int class (primitive)");
        printNamesForClass(String.class, "String.class (ordinary class)");
        printNamesForClass(java.util.HashMap.SimpleEntry.class,
                "java.util.HashMap.SimpleEntry.class (nested class)");
        printNamesForClass(new java.io.Serializable() {
                }.getClass(),
                "new java.io.Serializable(){}.getClass() (anonymous inner class)");
    }

    private static void printNamesForClass(final Class<?> clazz, final String label) {
        System.out.printf("%s:%n", label);
        System.out.printf("\tgetName()):\t%s%n", clazz.getName());
        System.out.printf("\tgetCanonicalName()):\t%s%n", clazz.getCanonicalName());
        System.out.printf("\tgetSimpleName()):\t%s%n", clazz.getSimpleName());
        System.out.printf("\tgetTypeName():\t%s%n%n", clazz.getTypeName());
    }
}

Resultado do nosso programa:

int class (primitivo):
getName()): int
getCanonicalName()): int
getSimpleName()): int
getTypeName(): int

String.class (classe comum):
getName()): java.lang.String
getCanonicalName() ): java.lang.String
getSimpleName()): String
getTypeName(): java.lang.String

java.util.HashMap.SimpleEntry.class (classe aninhada):
getName()): java.util.AbstractMap$SimpleEntry
getCanonicalName( )): java.util.AbstractMap.SimpleEntry
getSimpleName()): SimpleEntry
getTypeName(): java.util.AbstractMap$SimpleEntry

novo java.io.Serializable(){}.getClass() (classe interna anônima):
getName() ): TestReflection$1
getCanonicalName()): nulo
getSimpleName()):
getTypeName(): TestReflection$1

Agora vamos analisar a saída do nosso programa:

  • getName() retorna o nome da entidade.

  • getCanonicalName() retorna o nome canônico da classe base, conforme definido pela Java Language Specification. Retorna nulo se a classe base não tiver um nome canônico (ou seja, se for uma classe local ou anônima ou uma matriz cujo tipo de elemento não tenha um nome canônico).

  • getSimpleName() retorna o nome simples da classe base conforme especificado no código-fonte. Retorna uma string vazia se a classe base for anônima.

  • getTypeName() retorna uma string informativa para o nome desse tipo.