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.
- O que é um objeto
Classe como obtê-lo comgetClass(),.classeClass.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
getMethodeinvoke. - 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ódigo | O que faz |
|---|---|
Class<?> c | Uma 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.class | Outra 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 comoObject; por isso o cast paraList<String>(e o@SuppressWarningspara 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étodo | Devolve |
|---|---|
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 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 argumento10.0. É o mesmo quecafe.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
newInstancee 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.
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 oprivate. 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ê conhece | A chamada normal: cafe.aplicarDesconto(10). Sempre que possível. |
| Decidir pelo tipo de um objeto | instanceof / pattern matching |
| Escolher entre comportamentos pelo nome | Um Map<String, Runnable> com lambdas: o compilador continua conferindo |
| Mostrar o nome da classe de um objeto | getClass().getSimpleName() |
| Escrever uma ferramenta que examina classes desconhecidas (testes, JSON, injeção) | Reflexão |
Referência
| Código | Para quê | Pode lançar |
|---|---|---|
obj.getClass() | Class do objeto | — |
Produto.class | Class pelo nome escrito no código | — |
Class.forName("pacote.Classe") | Class por um String | ClassNotFoundException |
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úblico | NoSuchMethodException |
c.getDeclaredField("nome") | Um campo | NoSuchFieldException |
c.getDeclaredConstructor().newInstance() | Criar um objeto | várias |
m.invoke(obj, args…) | Chamar o método | IllegalAccessException, InvocationTargetException |
f.get(obj) / f.set(obj, valor) | Ler / escrever o campo | IllegalAccessException |
x.setAccessible(true) | Ignorar o private | InaccessibleObjectException |
Exercícios
1. Complete para mostrar o nome curto da classe do objeto p:
2. Complete para carregar a classe pelo nome completo:
3. Complete para chamar o método m no objeto cafe, sem argumentos:
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());
}
}
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,FieldeMethod. - Obtenha o
Classcomobj.getClass(),Produto.classouClass.forName("…"). getDeclaredFields/getDeclaredMethodslistam o conteúdo (em ordem não garantida);getMethod+invokechamam 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),setAccessiblefura 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.