Objetos Class em Java
Obtenha objetos Class<T> em Java com Object.getClass(), literais .class e Class.forName. Guia completo com exemplos.
Tudo em reflection começa a partir de um objeto Class. Para cada tipo que a JVM carrega — toda classe, interface, tipo array, enum, anotação e até cada primitivo — existe exatamente uma instância de Class que o descreve. Esse objeto é o seu acesso à estrutura do tipo: seu nome, sua superclasse, seus membros, suas anotações. Este capítulo aborda as três formas de obter um Class, o que Class<T> contém, e as pequenas surpresas que pegam as pessoas desprevenidas.
Três formas de obter um Class
Existem exatamente três caminhos, e cada um se adequa a uma situação diferente.
1. O literal .class — você conhece o tipo em tempo de compilação.
Class<String> c1 = String.class;
Class<int[]> c2 = int[].class;
Class<Integer> c3 = int.class == Integer.class ? null : int.class; // see "primitives" belowIsso é seguro em tempo de compilação e o mais rápido — não há busca, o compilador incorpora uma referência direta. Use sempre que puder nomear o tipo.
2. Object.getClass() — você tem uma instância.
Object o = "hello";
Class<?> c = o.getClass(); // class java.lang.StringgetClass() retorna a classe de runtime do objeto, que pode ser uma subclasse do tipo declarado da variável. Object o = new ArrayList<>() faz com que o.getClass() seja ArrayList.class, não Object.class. Seu tipo estático é Class<?> porque o compilador só sabe que o é algum Object.
3. Class.forName(String) — você tem apenas um nome.
Class<?> c = Class.forName("java.util.ArrayList");Este é o caminho dinâmico: um nome de classe totalmente qualificado como string, resolvido em tempo de execução. Lança ClassNotFoundException se nenhuma classe com esse nome puder ser carregada. É o que carregadores de plugins e drivers JDBC usam. Existe uma variante carregada-mas-não-inicializada: Class.forName(name, false, classLoader) ignora os inicializadores estáticos até que a classe seja usada ativamente pela primeira vez.
O que Class<T> contém
Um objeto Class é uma descrição rica. Os métodos principais:
Class<?> c = ArrayList.class;
c.getName(); // "java.util.ArrayList" (binary name)
c.getSimpleName(); // "ArrayList"
c.getCanonicalName(); // "java.util.ArrayList" (source-like)
c.getPackageName(); // "java.util"
c.getSuperclass(); // class java.util.AbstractList
c.getInterfaces(); // [List, RandomAccess, Cloneable, Serializable]
c.getModifiers(); // int bitset → Modifier.isPublic(...) etc.
c.isInterface(); // false
c.isEnum(); c.isArray(); c.isPrimitive(); c.isAnnotation();A partir de um Class você acessa cada membro: getDeclaredFields(), getDeclaredMethods(), getDeclaredConstructors(), além de seus equivalentes públicos/herdados get… (a separação do capítulo introdutório). Os capítulos seguintes aprofundam cada um.
Nome binário vs. nome simples vs. nome canônico
Os métodos de nomenclatura diferem de formas que causam problemas ao registrar ou comparar:
| Tipo | getName() | getSimpleName() | getCanonicalName() |
|---|---|---|---|
String | java.lang.String | String | java.lang.String |
int[] | [I | int[] | int[] |
String[] | [Ljava.lang.String; | String[] | java.lang.String[] |
Map.Entry aninhado | java.util.Map$Entry | Entry | java.util.Map.Entry |
| classe anônima | Outer$1 | "" (vazio) | null |
getName() é o nome binário — a forma interna da JVM, com $ para aninhamento e a codificação críptica [I / [L…; para arrays. É o que Class.forName espera. getCanonicalName() é a forma de código-fonte que você escreveria, e é null para tipos que não podem ser nomeados no código-fonte (locais, classes anônimas). Use getName() para round-trips com forName; use getSimpleName()/getCanonicalName() para saída legível por humanos.
Primitivos e arrays também têm objetos Class
Cada primitivo tem seu próprio Class, distinto do seu wrapper:
int.class == Integer.class // false — two different Class objects
int.class.getName() // "int"
Integer.TYPE == int.class // true — TYPE is the primitive Classvoid até tem void.class (e Void.TYPE). Classes de arrays são sintetizadas pela JVM: int[].class, String[][].class. arrayClass.getComponentType() remove uma dimensão (String[].class.getComponentType() é String.class). Essas distinções importam quando você compara tipos de parâmetros em getMethod — getMethod("foo", int.class) e getMethod("foo", Integer.class) encontram sobrecargas diferentes.
Identidade de classe e carregadores de classe
A identidade de um objeto Class não é apenas seu nome — é o par (nome, carregador de classe que o definiu). O mesmo arquivo .class carregado por dois carregadores de classe diferentes produz dois objetos Class distintos e incompatíveis. Uma conversão entre eles lança ClassCastException mesmo que os nomes sejam iguais. Isso é principalmente invisível em uma aplicação simples (um carregador), mas é a raiz de muitos enigmas "mas é a mesma classe!" em servidores de aplicação, OSGi e sistemas de hot-reload. Para reflection do dia a dia, trate objetos Class como singletons por tipo e compare-os com ==.
Um exemplo prático: sondando tipos de três formas
O programa obtém objetos Class por todos os três caminhos e, em seguida, interroga alguns tipos — uma classe comum, uma interface, um array, um primitivo e um tipo aninhado — para evidenciar as diferenças de nomenclatura e estrutura.
O que aprender com a execução:
- Todos os três caminhos convergiram para o mesmo tipo de objeto: um literal
.class, uma chamadagetClass()e uma pesquisaforNameproduziram umClasstotalmente utilizável. O caminho que você escolhe depende do que você sabe (o tipo, uma instância ou apenas um nome) — o resultado é idêntico em capacidade. getClass()na variávelGreeter gretornouRobot, nãoGreeter. O tipo declarado é irrelevante;getClass()sempre reporta a classe concreta de runtime. É por isso que o despacho polimórfico e a inspeção reflexiva veem o mesmo tipo "real".- Os três métodos de nomenclatura divergiram exatamente onde a tabela prevê:
String[]imprimiu o nome binário[Ljava.lang.String;degetName(), mas a forma legívelString[]das formas simples e canônica. Se você precisar passar um nome de volta paraforName, deve ser no formatogetName(). int.class == Integer.classfoifalseenquantoInteger.TYPE == int.classfoitrue. O primitivo e seu wrapper são objetosClassdistintos, eInteger.TYPEé apenas um alias para o primitivo. Confundi-los é a causa clássica deNoSuchMethodExceptionao pesquisar uma sobrecarga por tipo de parâmetro.Robot.class == new Robot().getClass()foitrue: dentro de um carregador de classe, um tipo é mapeado para exatamente um objetoClass, então==é a comparação correta. Você nunca precisa de.equals()em objetosClassem código de carregador único.
Armadilhas comuns
forNameexecuta inicializadores estáticos (na sua forma de um argumento). Carregar uma classe pode ter efeitos colaterais. Use a forma de três argumentos cominitialize=falsese quiser apenas inspecionar.getSimpleName()pode ser vazio (classes anônimas) egetCanonicalName()pode sernull(locais, anônimas). Não assuma que sempre são identificadores imprimíveis.- Genéricos são apagados.
List<String>.classé ilegal; existe apenasList.class. UmClassnão carrega informações de argumentos de tipo — essas ficam emType/ParameterizedType, uma API de reflection separada (e mais avançada). Veja restrições de genéricos para entender por que o apagamento funciona assim.
Com o objeto Class em mãos, o próximo capítulo abre a primeira gaveta de membros: campos — inspecionando-os, lendo-os e escrevendo neles, mesmo quando são privados ou finais.