Objetos Class de Java
Obtén objetos Class<T> en Java con Object.getClass(), literales .class y Class.forName. Guía práctica con ejemplos.
Todo en reflection parte de un objeto Class. Para cada tipo que carga la JVM — cada clase, interfaz, tipo de array, enum, anotación e incluso cada primitivo — existe exactamente una instancia de Class que lo describe. Ese objeto es tu punto de acceso a la estructura del tipo: su nombre, su superclase, sus miembros, sus anotaciones. Este capítulo cubre las tres formas de obtener un Class, qué contiene Class<T> y las pequeñas sorpresas que suelen confundir a la gente.
Tres formas de obtener un Class
Hay exactamente tres rutas, y cada una se adapta a una situación diferente.
1. El literal .class — conoces el tipo en tiempo de compilación.
Class<String> c1 = String.class;
Class<int[]> c2 = int[].class;
Class<Integer> c3 = int.class == Integer.class ? null : int.class; // see "primitives" belowEs la forma más segura en tiempo de compilación y la más rápida — no hay búsqueda; el compilador incorpora la referencia directamente. Úsala siempre que puedas nombrar el tipo.
2. Object.getClass() — tienes una instancia.
Object o = "hello";
Class<?> c = o.getClass(); // class java.lang.StringgetClass() devuelve la clase en tiempo de ejecución del objeto, que puede ser una subclase del tipo declarado de la variable. Object o = new ArrayList<>() hace que o.getClass() sea ArrayList.class, no Object.class. Su tipo estático es Class<?> porque el compilador solo sabe que o es algún Object.
3. Class.forName(String) — solo tienes un nombre.
Class<?> c = Class.forName("java.util.ArrayList");Esta es la ruta dinámica: un nombre de clase completamente calificado como string, resuelto en tiempo de ejecución. Lanza ClassNotFoundException si no se puede cargar dicha clase. Es lo que usan los cargadores de plugins y los drivers JDBC. Existe una variante que carga sin inicializar: Class.forName(name, false, classLoader) omite los inicializadores estáticos hasta que la clase se usa activamente por primera vez.
Qué contiene Class<T>
Un objeto Class es una descripción completa. Los métodos principales:
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();Desde un Class puedes acceder a todos los miembros: getDeclaredFields(), getDeclaredMethods(), getDeclaredConstructors(), además de sus contrapartes públicas/heredadas get… (la distinción explicada en el capítulo de introducción). Los capítulos siguientes profundizan en cada uno.
Nombre binario vs. nombre simple vs. nombre canónico
Los métodos de nombrado difieren en aspectos que pueden causar problemas al registrar o 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 anidado | java.util.Map$Entry | Entry | java.util.Map.Entry |
| clase anónima | Outer$1 | "" (vacío) | null |
getName() es el nombre binario — la forma interna de la JVM, con $ para el anidamiento y la críptica codificación de arrays [I / [L…;. Es lo que espera Class.forName. getCanonicalName() es la forma fuente que escribirías, y es null para tipos que no puedes nombrar en el código fuente (locales, clases anónimas). Usa getName() para round-trips con forName; usa getSimpleName()/getCanonicalName() para salida legible.
Los primitivos y los arrays también tienen objetos Class
Cada primitivo tiene su propio Class, distinto de su wrapper:
int.class == Integer.class // false — two different Class objects
int.class.getName() // "int"
Integer.TYPE == int.class // true — TYPE is the primitive Classvoid incluso tiene void.class (y Void.TYPE). Las clases de array son sintetizadas por la JVM: int[].class, String[][].class. arrayClass.getComponentType() elimina una dimensión (String[].class.getComponentType() es String.class). Estas distinciones importan cuando comparas tipos de parámetros en getMethod — getMethod("foo", int.class) y getMethod("foo", Integer.class) encuentran sobrecargas distintas.
Identidad de clase y cargadores de clases
La identidad de un objeto Class no es solo su nombre — es el par (nombre, cargador de clase que lo define). El mismo archivo .class cargado por dos cargadores de clase distintos produce dos objetos Class distintos e incompatibles. Un cast entre ellos lanza ClassCastException aunque los nombres coincidan. Esto es en su mayoría invisible en una aplicación sencilla (un solo cargador), pero es la raíz de muchos puzzles de "¡pero es la misma clase!" en servidores de aplicaciones, OSGi y sistemas de hot-reload. En reflection cotidiana, trata los objetos Class como singletons por tipo y compáralos con ==.
Ejemplo práctico: examinando tipos de tres formas
El programa obtiene objetos Class a través de las tres rutas y luego examina varios tipos — una clase normal, una interfaz, un array, un primitivo y un tipo anidado — para mostrar las diferencias de nombrado y estructurales.
Lo que se puede extraer de la ejecución:
- Las tres rutas convergen en el mismo tipo de objeto: un literal
.class, una llamada agetClass()y una búsqueda conforNameprodujeron cada uno unClasscompletamente utilizable. La ruta que eliges depende de lo que sabes (el tipo, una instancia o solo un nombre) — el resultado tiene las mismas capacidades. getClass()sobre la variableGreeter gdevolvióRobot, noGreeter. El tipo declarado es irrelevante;getClass()siempre reporta la clase concreta en tiempo de ejecución. Por eso el despacho polimórfico y la inspección reflectiva ven el mismo tipo "real".- Los tres métodos de nombrado divergieron exactamente donde predice la tabla:
String[]imprimió el nombre binario[Ljava.lang.String;congetName(), pero la forma legibleString[]con las formas simple y canónica. Si alguna vez necesitas pasar un nombre de vuelta aforName, debe ser la forma degetName(). int.class == Integer.classfuefalsemientras queInteger.TYPE == int.classfuetrue. El primitivo y su wrapper son objetosClassdistintos, eInteger.TYPEes simplemente un alias del primitivo. Confundirlos es la causa clásica deNoSuchMethodExceptioncuando buscas una sobrecarga por tipo de parámetro.Robot.class == new Robot().getClass()fuetrue: dentro de un solo cargador de clases, un tipo se corresponde con exactamente un objetoClass, por lo que==es la comparación correcta. Nunca necesitas.equals()en objetosClassen código de un solo cargador.
Errores comunes
forNameejecuta inicializadores estáticos (en su forma de un argumento). Cargar una clase puede tener efectos secundarios. Usa la forma de tres argumentos coninitialize=falsesi solo quieres inspeccionar.getSimpleName()puede estar vacío (clases anónimas) ygetCanonicalName()puede sernull(locales, anónimas). No asumas que siempre son identificadores imprimibles.- Los genéricos se borran.
List<String>.classes ilegal; solo existeList.class. UnClassno lleva información de argumentos de tipo — eso vive enType/ParameterizedType, una API de reflection separada (y más avanzada). Consulta restricciones de genéricos para entender por qué funciona así el borrado.
Con el objeto Class en mano, el siguiente capítulo abre el primer cajón de miembros: campos — cómo inspeccionarlos, leerlos y escribirlos, incluso cuando son privados o finales.