Java Reflexão

Reflexão (em inglês reflection) é a capacidade de um programa Java olhar para si mesmo enquanto roda: descobrir de que classe é um objeto, quais campos e métodos essa classe tem, e até chamar um método cujo nome só é conhecido na hora, guardado num String. Você raramente vai escrever reflexão no dia a dia, mas ela está por trás de quase todo framework Java.

Você vai aprender
  • O que é um objeto Class e como obtê-lo com getClass(), .class e Class.forName.
  • Como listar os campos e os métodos de uma classe (com a saída real).
  • Como chamar um método pelo nome, com getMethod e invoke.
  • Por que JUnit, Spring e companhia usam reflexão.
  • Os cuidados: erros que só aparecem ao rodar, encapsulamento quebrado e as barreiras do próprio Java.

Esta aula é introdutória. Ela usa classes e objetos, modificadores de acesso e exceções checked. Os exemplos rodaram no JDK 25 da máquina da Ana, na pasta ~/java.


A ideia antes do código

Normalmente, quem escreve o programa sabe tudo de antemão: "vou criar um Produto e chamar aplicarDesconto". O compilador confere cada nome antes de o programa rodar.

Agora imagine a Ana montando um sistema de testes: ela quer que o programa descubra sozinho todos os métodos cujo nome começa com testar e rode cada um. Quem escreve esse sistema não sabe que métodos vão existir: eles serão escritos depois, por outras pessoas. É como um raio-x: o programa examina uma classe que nunca viu e enxerga o que tem dentro.

O ponto de partida é um objeto especial do tipo Class ("classe"): para cada classe carregada, o Java mantém um objeto que a descreve, com nome, campos, métodos, classe mãe… A reflexão é conversar com esse objeto.


1. Descobrir a classe de um objeto: getClass()

Todo objeto herda de Object o método getClass(), que devolve o Class dele:

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        Object[] coisas = { "Ana", 42, 3.5, new ArrayList<String>(), new int[3] };
        for (Object o : coisas) {
            Class<?> c = o.getClass();
            System.out.println(c.getSimpleName() + "  |  " + c.getName());
        }
        System.out.println(String.class);
        System.out.println(int.class);
        System.out.println(Integer.class.getSuperclass());
    }
}
$ javac Main.java && java Main
String  |  java.lang.String
Integer  |  java.lang.Integer
Double  |  java.lang.Double
ArrayList  |  java.util.ArrayList
int[]  |  [I
class java.lang.String
int
class java.lang.Number
CódigoO que faz
Class<?> cUma variável do tipo Class. O <?> quer dizer "de uma classe qualquer" (veja generics).
o.getClass()"Qual é a sua classe?" A pergunta é feita ao objeto, enquanto o programa roda: a variável é Object, mas a resposta é a classe verdadeira.
getSimpleName()O nome curto: String.
getName()O nome completo, com o pacote: java.lang.String. Para arrays, o Java usa um código interno: [I é "array de int".
String.classOutra forma de obter o Class, direto pelo nome da classe, sem precisar de um objeto. Funciona até para tipos primitivos: int.class.
getSuperclass()A classe mãe: Integer herda de Number.
getClass() × instanceof

Para decidir o que fazer com um objeto, prefira instanceof e pattern matching: o compilador ajuda e as subclasses também casam. getClass() é para quando você quer o nome exato da classe, por exemplo numa mensagem ou num relatório.

Missão: o programa mostra o conteúdo de cada objeto. Troque o println para mostrar o nome curto da classe de cada um (String, Integer, ArrayList, HashMap), usando getClass().getSimpleName().

import java.util.ArrayList;
import java.util.HashMap;

public class Main {
    public static void main(String[] args) {
        Object[] coisas = { "kiwi", 7, new ArrayList<String>(), new HashMap<String, Integer>() };
        for (Object o : coisas) {
            System.out.println(o);
        }
    }
}
import java.util.ArrayList;
import java.util.HashMap;

public class Main {
    public static void main(String[] args) {
        Object[] coisas = { "kiwi", 7, new ArrayList<String>(), new HashMap<String, Integer>() };
        for (Object o : coisas) {
            System.out.println(o.getClass().getSimpleName());
        }
    }
}

2. Carregar uma classe pelo nome: Class.forName

E se o nome da classe estiver num String, lido de um arquivo de configuração, por exemplo? Class.forName ("classe pelo nome") procura e carrega a classe. Depois, dá até para criar um objeto dela:

import java.util.List;

public class Main {
    public static void main(String[] args) throws Exception {
        Class<?> c = Class.forName("java.util.ArrayList");
        System.out.println("Carreguei: " + c.getName());

        Object lista = c.getDeclaredConstructor().newInstance();
        @SuppressWarnings("unchecked")
        List<String> frutas = (List<String>) lista;
        frutas.add("maçã");
        frutas.add("uva");
        System.out.println(frutas);
    }
}
$ javac Main.java && java Main
Carreguei: java.util.ArrayList
[maçã, uva]
Class.forName("java.util.ArrayList")
Procura a classe pelo nome completo (com o pacote).
.getDeclaredConstructor()
Pega o construtor sem parâmetros.
.newInstance()
Chama esse construtor: é um new ArrayList() feito "à distância". O resultado vem como Object; por isso o cast para List<String> (e o @SuppressWarnings para calar o aviso dessa conversão).

Repare no throws Exception no main. Quase todo método de reflexão pode lançar exceções checked (a classe pode não existir, o método pode não existir…), e o compilador exige que você as declare ou trate. Sem isso:

$ javac Main.java
Main.java:3: error: unreported exception ClassNotFoundException; must be caught or declared to be thrown
        Class<?> c = Class.forName("java.util.ArrayList");
                                  ^
1 error

Veja throw e throws para as duas formas de resolver.


3. Listar campos e métodos

Com o Class em mãos, você pode pedir a lista de tudo o que a classe declara, inclusive o que é private:

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Comparator;

class Produto {
    private String nome;
    private double preco;
    public int estoque;

    public Produto(String nome, double preco) {
        this.nome = nome;
        this.preco = preco;
    }

    public String getNome() { return nome; }
    public double getPreco() { return preco; }
    public void aplicarDesconto(double porcento) { preco = preco - preco * porcento / 100; }
    private void registrar() { }
}

public class Main {
    public static void main(String[] args) {
        Class<?> c = Produto.class;

        System.out.println("Campos de " + c.getSimpleName() + ":");
        for (Field f : c.getDeclaredFields()) {
            System.out.println("  " + Modifier.toString(f.getModifiers()) + " " + f.getType().getSimpleName() + " " + f.getName());
        }

        System.out.println("Métodos:");
        Method[] metodos = c.getDeclaredMethods();
        Arrays.sort(metodos, Comparator.comparing(Method::getName));
        for (Method m : metodos) {
            System.out.println("  " + Modifier.toString(m.getModifiers()) + " " + m.getReturnType().getSimpleName() + " " + m.getName() + Arrays.toString(m.getParameterTypes()));
        }
    }
}
$ javac Main.java && java Main
Campos de Produto:
  private String nome
  private double preco
  public int estoque
Métodos:
  public void aplicarDesconto[double]
  public String getNome[]
  public double getPreco[]
  private void registrar[]
MétodoDevolve
c.getDeclaredFields()Um array de Field ("campo"): os atributos declarados na classe, de qualquer visibilidade.
f.getName(), f.getType()O nome do campo e o Class do tipo dele.
f.getModifiers() + Modifier.toString(…)Os modificadores (private, public, static…) como texto.
c.getDeclaredMethods()Um array de Method ("método"): os métodos declarados na classe.
m.getReturnType(), m.getParameterTypes()O tipo de retorno e os tipos dos parâmetros.
A ordem não é garantida

A documentação do Java avisa que getDeclaredMethods() e getDeclaredFields() devolvem os itens "em nenhuma ordem em particular". Por isso o exemplo ordena os métodos pelo nome com Arrays.sort. Se a ordem importa, ordene.

Existem duas famílias de métodos: os getDeclared… (tudo o que esta classe declara, inclusive private, mas sem os herdados) e os get… sem "Declared" (getMethods(), getFields(): só os public, incluindo os herdados, como toString e equals de Object).


4. Chamar um método pelo nome

Agora a parte que parece mágica: o nome do método está numa variável String, e o programa o chama mesmo assim:

import java.lang.reflect.Method;

class Produto {
    private String nome;
    private double preco;

    public Produto(String nome, double preco) {
        this.nome = nome;
        this.preco = preco;
    }

    public double getPreco() { return preco; }
    public void aplicarDesconto(double porcento) { preco = preco - preco * porcento / 100; }
}

public class Main {
    public static void main(String[] args) throws Exception {
        Produto cafe = new Produto("Café", 20.0);
        String acao = "aplicarDesconto";

        Method m = Produto.class.getMethod(acao, double.class);
        m.invoke(cafe, 10.0);

        Method get = Produto.class.getMethod("getPreco");
        Object preco = get.invoke(cafe);
        System.out.println("Preço depois de " + acao + ": " + preco);
    }
}
$ javac Main.java && java Main
Preço depois de aplicarDesconto: 18.0
Produto.class.getMethod(
"Me dê o método público…"
acao,
"…com este nome…" (o String "aplicarDesconto")
double.class)
"…que recebe um double." Como pode haver sobrecarga, o nome sozinho não basta: é preciso dizer os tipos dos parâmetros.
m.invoke(cafe, 10.0)
"Invocar": chama o método no objeto cafe, com o argumento 10.0. É o mesmo que cafe.aplicarDesconto(10.0).

O invoke devolve o resultado como Object (aqui, o 18.0 do getPreco, embalado num Double). Para um método void, devolve null.


5. Por que os frameworks usam reflexão

Juntando tudo, dá para escrever um "mini JUnit": uma classe que roda todos os métodos cujo nome começa com testar, sem saber de antemão quais são.

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;

class TestesDaLoja {
    void testarSoma() {
        System.out.println("  2 + 2 = " + (2 + 2));
    }
    void testarDesconto() {
        System.out.println("  20 com 10% = " + (20 - 20 * 10 / 100));
    }
    void ajudante() {
        System.out.println("  (não sou teste)");
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        Class<?> c = TestesDaLoja.class;
        Object testes = c.getDeclaredConstructor().newInstance();
        Method[] metodos = c.getDeclaredMethods();
        Arrays.sort(metodos, Comparator.comparing(Method::getName));
        for (Method m : metodos) {
            if (m.getName().startsWith("testar")) {
                System.out.println("Rodando " + m.getName() + ":");
                m.invoke(testes);
            }
        }
    }
}
$ javac Main.java && java Main
Rodando testarDesconto:
  20 com 10% = 18
Rodando testarSoma:
  2 + 2 = 4

O método ajudante não rodou: o nome não começa com testar. O JUnit faz exatamente isso, só que em vez do nome ele procura a anotação @Test (com m.isAnnotationPresent(Test.class)). Outros exemplos do mesmo truque:

  • Spring lê as anotações das suas classes, cria os objetos com newInstance e liga uns aos outros.
  • Bibliotecas de JSON (como Jackson e Gson) listam os campos de um objeto para transformá-lo em texto {"nome": "Café", "preco": 18.0}, e o caminho inverso.
  • IDEs e depuradores mostram os campos de um objeto enquanto o programa está parado (veja Depurar).

6. Cuidados

Reflexão é poderosa, mas tira de você a principal ajuda do Java: o compilador.

Erros só aparecem ao rodar

Um nome digitado errado no código normal é pego pelo javac. Num String, não: o programa compila e quebra quando roda.

Estes códigos têm erros de propósito.
        Method m = Produto.class.getMethod("aplicarDescont", double.class);
$ javac Main.java && java Main
Exception in thread "main" java.lang.NoSuchMethodException: Produto.aplicarDescont(double)
	at java.base/java.lang.Class.getMethod(Class.java:2166)
	at Main.main(Main.java:11)
        Class<?> c = Class.forName("java.util.Arraylist");
$ javac Main.java && java Main
Exception in thread "main" java.lang.ClassNotFoundException: java.util.Arraylist
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:580)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:490)
	at java.base/java.lang.Class.forName0(Native Method)
	at java.base/java.lang.Class.forName(Class.java:467)
	at java.base/java.lang.Class.forName(Class.java:458)
	at Main.main(Main.java:3)

NoSuchMethodException = "não existe esse método" (faltou o o de aplicarDesconto). ClassNotFoundException = "classe não encontrada" (Arraylist com l minúsculo). Os dois passaram pelo javac sem problema.

Furar o private

Ler um campo private por reflexão é barrado, como no código normal:

import java.lang.reflect.Field;

class Produto {
    private double preco = 20.0;
}

public class Main {
    public static void main(String[] args) throws Exception {
        Produto cafe = new Produto();
        Field campo = Produto.class.getDeclaredField("preco");
        System.out.println(campo.get(cafe));
    }
}
$ javac Main.java && java Main
Exception in thread "main" java.lang.IllegalAccessException: class Main cannot access a member of class Produto with modifiers "private"
	at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:400)
	at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:670)
	at java.base/java.lang.reflect.Field.checkAccess(Field.java:1152)
	at java.base/java.lang.reflect.Field.get(Field.java:437)
	at Main.main(Main.java:11)

Mas existe uma "chave mestra": campo.setAccessible(true). Com ela, o programa da Ana mudou o preço de um café para um centavo, passando por cima de todo o encapsulamento. E aí aparece a segunda barreira: as classes do próprio Java são protegidas pelos módulos, e a chave mestra não funciona nelas:

        Field campo = Produto.class.getDeclaredField("preco");
        campo.setAccessible(true);
        campo.set(cafe, 0.01);
        System.out.println("Preço: " + cafe.getPreco());

        Field interno = String.class.getDeclaredField("value");
        interno.setAccessible(true);
$ javac Main.java && java Main
Preço: 0.01
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module @764c12b6
	at java.base/java.lang.reflect.AccessibleObject.throwInaccessibleObjectException(AccessibleObject.java:353)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:329)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:277)
	at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:179)
	at java.base/java.lang.reflect.Field.setAccessible(Field.java:173)
	at Main.main(Main.java:17)

"Não foi possível tornar acessível o campo privado value de String: o módulo java.base não abre o pacote java.lang." Desde o Java 17, as entranhas do JDK ficam sempre trancadas. (O número depois de @ muda a cada execução.)

Resumo dos cuidados

  • Sem ajuda do compilador: nomes errados viram exceções ao rodar.
  • Encapsulamento: setAccessible(true) ignora o private. Use só em ferramentas, nunca para "consertar" uma classe por fora.
  • Mais lento e mais difícil de ler do que a chamada direta.
  • Exceções checked em quase todo método, e as exceções do método chamado chegam embrulhadas numa InvocationTargetException.

Qual usar?

Você quer…Use
Chamar um método que você conheceA chamada normal: cafe.aplicarDesconto(10). Sempre que possível.
Decidir pelo tipo de um objetoinstanceof / pattern matching
Escolher entre comportamentos pelo nomeUm Map<String, Runnable> com lambdas: o compilador continua conferindo
Mostrar o nome da classe de um objetogetClass().getSimpleName()
Escrever uma ferramenta que examina classes desconhecidas (testes, JSON, injeção)Reflexão

Referência

CódigoPara quêPode lançar
obj.getClass()Class do objeto—
Produto.classClass pelo nome escrito no código—
Class.forName("pacote.Classe")Class por um StringClassNotFoundException
c.getName() / getSimpleName()Nome completo / curto—
c.getSuperclass()Classe mãe—
c.getDeclaredFields() / getFields()Campos declarados / públicos (com herdados)—
c.getDeclaredMethods() / getMethods()Métodos declarados / públicos (com herdados)—
c.getMethod("nome", tipos…)Um método públicoNoSuchMethodException
c.getDeclaredField("nome")Um campoNoSuchFieldException
c.getDeclaredConstructor().newInstance()Criar um objetovárias
m.invoke(obj, args…)Chamar o métodoIllegalAccessException, InvocationTargetException
f.get(obj) / f.set(obj, valor)Ler / escrever o campoIllegalAccessException
x.setAccessible(true)Ignorar o privateInaccessibleObjectException

Exercícios

1. Complete para mostrar o nome curto da classe do objeto p:

System.out.println(p..getSimpleName());

2. Complete para carregar a classe pelo nome completo:

Class<?> c = Class.("java.util.HashMap");

3. Complete para chamar o método m no objeto cafe, sem argumentos:

m.(cafe);

4. getMethod("aplicarDescont", double.class), com o nome errado, dá erro quando?

5. Por que o JUnit consegue rodar os seus métodos @Test sem você chamá-los?

Missão final: conte quantos animais de cada classe há no array, usando o nome da classe (getClass().getSimpleName()) como chave do mapa. Depois mostre o nome da classe mãe de Gato com Gato.class.getSuperclass().getSimpleName(). Saída esperada: Gato: 3, Cachorro: 1 e Mãe de Gato: Animal.

import java.util.HashMap;
import java.util.Map;

class Animal { }
class Gato extends Animal { }
class Cachorro extends Animal { }

public class Main {
    public static void main(String[] args) {
        Animal[] animais = { new Gato(), new Cachorro(), new Gato(), new Gato() };
        Map<String, Integer> contagem = new HashMap<>();
        for (Animal a : animais) {
            // descubra o nome da classe de a e some 1 no mapa
        }
        System.out.println("Gato: " + contagem.get("Gato"));
        System.out.println("Cachorro: " + contagem.get("Cachorro"));
        // mostre a classe mãe de Gato
    }
}
import java.util.HashMap;
import java.util.Map;

class Animal { }
class Gato extends Animal { }
class Cachorro extends Animal { }

public class Main {
    public static void main(String[] args) {
        Animal[] animais = { new Gato(), new Cachorro(), new Gato(), new Gato() };
        Map<String, Integer> contagem = new HashMap<>();
        for (Animal a : animais) {
            String nome = a.getClass().getSimpleName();
            contagem.put(nome, contagem.getOrDefault(nome, 0) + 1);
        }
        System.out.println("Gato: " + contagem.get("Gato"));
        System.out.println("Cachorro: " + contagem.get("Cachorro"));
        System.out.println("Mãe de Gato: " + Gato.class.getSuperclass().getSimpleName());
    }
}
Sobre o editor do guia

O simulador do guia não faz reflexão completa (veja O Editor do Guia): as missões desta aula usam só getClass() e companhia. Para experimentar getDeclaredMethods, invoke e setAccessible, use o JDK no seu computador; as saídas mostradas nesta aula são as do JDK 25 real.


Resumo

  • Reflexão é o programa examinar classes enquanto roda, através de objetos Class, Field e Method.
  • Obtenha o Class com obj.getClass(), Produto.class ou Class.forName("…").
  • getDeclaredFields/getDeclaredMethods listam o conteúdo (em ordem não garantida); getMethod + invoke chamam um método pelo nome.
  • Frameworks (JUnit, Spring, JSON) usam reflexão para trabalhar com classes que não conheciam.
  • Cuidados: erros viram exceções ao rodar (NoSuchMethodException, ClassNotFoundException), setAccessible fura o encapsulamento e é barrado nas classes do JDK (InaccessibleObjectException). Prefira o código normal sempre que der.

Próxima aula: Novidades do Java 17 ao 25.