Cap 14: Reflexão¶
🪞 O que é reflexão¶
Reflexão é a capacidade de um programa examinar a si mesmo e o seu ambiente, e mudar seu comportamento de acordo com o que encontra. Mais precisamente: reflexão é o processo pelo qual um programa observa e modifica sua própria estrutura e comportamento, exigindo acesso à sua representação interna — os chamados metadados.
Isso torna reflexão um tipo de metaprogramação: o programa realiza computações a respeito de si mesmo. A diferença em relação a macros (outro mecanismo clássico de metaprogramação) é o momento em que isso acontece — macros operam em nível sintático, antes da execução; reflexão opera em tempo de execução.
Em programação orientada a objetos, os metadados são organizados em metaobjetos. A capacidade de acessá-los em tempo de execução é chamada introspecção — um subconjunto da reflexão, que permite ao programa obter informações sobre si mesmo. Para dar suporte a metaprogramação, uma linguagem precisa expor os elementos usados para codificar suas próprias entidades — no caso do Java, dados sobre classes, métodos e atributos de um objeto.
🏷️ Anotações¶
Anotações são metadados adicionados a classes, atributos e métodos — expressam alguma intenção do programador em relação ao elemento anotado. Vale marcar a diferença em relação a comentários:
- Comentários existem apenas no código-fonte (o compilador os descarta na análise léxica); anotações podem permanecer no código compilado.
- Comentários são texto livre, difícil de processar automaticamente; anotações são estruturadas e projetadas justamente para processamento automatizado.
🐍 Motivação: o problema que reflexão resolve¶
Em linguagens dinamicamente tipadas como Python, é comum usar o conceito de duck typing — "se anda como um pato e faz quack como um pato, então é um pato". Considere três classes independentes (talvez de bibliotecas diferentes, escritas por autores diferentes) que coincidentemente têm um método getNome:
class Docente:
def __init__(self, nome):
self.nome = nome
def getNome(self):
return self.nome
class Discente:
def __init__(self, nome):
self.nome = nome
def getNome(self):
return self.nome
Como Python não checa tipos em tempo de compilação, uma função pode aplicar getNome() a qualquer objeto da lista, sem exigir que compartilhem uma superclasse comum — o único pré-requisito é que o método exista, verificado só em tempo de execução:
Em Java, essa abordagem não compila: uma variável do tipo Object não tem um método getNome() conhecido estaticamente.
public static void imprimeNomes(List<Object> lista) {
for (Object i : lista) {
System.out.println(i.getNome()); // erro de compilação!
}
}
Se as classes fossem escritas pelo mesmo time, a solução natural seria herança e polimorfismo (Capítulos 6 e 7). Mas quando isso não é possível — classes de terceiros, sem controle sobre o design — Java oferece reflexão como alternativa, ao custo de perder a checagem estática:
public static void imprimeNomes(List<Object> lista) {
for (Object i : lista) {
Class<?> c = i.getClass();
try {
Method m = c.getMethod("getNome");
String nome = (String) m.invoke(i);
System.out.println(nome);
} catch (ReflectiveOperationException e) {
e.printStackTrace();
}
}
}
c.getMethod("getNome") procura, em tempo de execução, um método público chamado getNome na classe de i; m.invoke(i) o chama. Se o método não existir, o erro só aparece em execução — a mesma troca de confiabilidade por flexibilidade discutida no Capítulo 3 quando comparamos tipagem estática e dinâmica.
Reflexão tem custo — use com moderação
Chamadas via reflexão são significativamente mais lentas que chamadas diretas (o JIT, visto no Capítulo 2, otimiza muito pior código que passa por Method.invoke), e o próprio compilador perde a capacidade de checar erros de tipo e nome de método antecipadamente. Reflexão é uma ferramenta poderosa para frameworks — código genérico que precisa operar sobre tipos que só existirão depois, escritos por quem usa o framework — não uma ferramenta de uso cotidiano em lógica de negócio comum.
🏗️ Um exemplo mais realista: anotações + reflexão em conjunto¶
Retomando o exemplo de JTable do capítulo anterior: escrever manualmente uma TableModel para cada classe de domínio é repetitivo. Usando anotações e reflexão, é possível escrever uma única TableModel genérica que funciona para qualquer classe anotada.
Primeiro, uma anotação customizada para marcar quais atributos devem virar colunas:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME) // precisa estar disponível em tempo de execução
public @interface Coluna {
String nome();
int posicao();
}
Aplicada a uma classe de domínio:
public class Filme {
@Coluna(nome = "Ano", posicao = 0)
private String ano;
@Coluna(nome = "Diretor", posicao = 1)
private String diretor;
@Coluna(nome = "Nome", posicao = 2)
private String nome;
// getters
}
Uma interface para abstrair a origem dos dados (o padrão DAO — Data Access Object — combinado com generics, visto no Capítulo 7):
E, por fim, a TableModel genérica, que usa reflexão para descobrir colunas e valores em tempo de execução, a partir das anotações:
public class TabelaGenerica<T> extends AbstractTableModel {
private final List<T> dados;
public TabelaGenerica(RepositorioGenerico<T> repositorio) {
this.dados = repositorio.listar();
}
@Override
public int getRowCount() {
return dados.size();
}
@Override
public int getColumnCount() {
return (int) camposAnotados().count();
}
@Override
public String getColumnName(int coluna) {
return camposAnotados()
.filter(f -> f.getAnnotation(Coluna.class).posicao() == coluna)
.findFirst()
.map(f -> f.getAnnotation(Coluna.class).nome())
.orElse(null);
}
@Override
public Object getValueAt(int linha, int coluna) {
T objeto = dados.get(linha);
Field campo = camposAnotados()
.filter(f -> f.getAnnotation(Coluna.class).posicao() == coluna)
.findFirst()
.orElseThrow();
try {
campo.setAccessible(true); // permite ler campos private
return campo.get(objeto);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private Stream<Field> camposAnotados() {
return Arrays.stream(dados.get(0).getClass().getDeclaredFields())
.filter(f -> f.isAnnotationPresent(Coluna.class));
}
}
Com isso, qualquer classe de domínio anotada com @Coluna pode virar uma JTable sem escrever uma nova TableModel para cada uma — é exatamente esse tipo de generalização que sustenta frameworks amplamente usados em Java, como Spring (@Autowired, @RequestMapping) e Jakarta Persistence/Hibernate (@Entity, @Column): todos usam reflexão sobre anotações para conectar seu código a comportamento genérico da biblioteca, sem exigir que você escreva boilerplate repetitivo.
📝 Atividades¶
- Implemente a classe
TabelaGenericadeste capítulo (ou uma versão simplificada) e use-a com uma classeFilmeanotada, exibindo os dados em umaJTablereal. - Explique, com suas palavras, a diferença entre introspecção e reflexão — dado que o texto afirma que Java oferece, estritamente, apenas o primeiro subconjunto completo.
- Pesquise: como a anotação
@Override, que você já usou em capítulos anteriores, é diferente de@Colunadeste capítulo? (Dica: pesquise sobreRetentionPolicy.SOURCEvs.RUNTIME.) - Compare o exemplo de duck typing em Python com a solução via reflexão em Java — em que sentido reflexão é uma forma de "recuperar", em uma linguagem estaticamente tipada, parte da flexibilidade que uma linguagem dinamicamente tipada tem por padrão?
🔗 Para saber mais¶
- Forman, I. & Forman, N. — Java Reflection in Action
- Documentação oficial: The Reflection API (Oracle)
- Pesquise sobre metaprogramação em Ruby, para comparar com o modelo de reflexão do Java (Ruby permite reflexão muito mais ampla, incluindo modificar classes existentes em tempo de execução — o chamado monkey patching).