Java Class Objects
Obtain Class<T> objects in Java with Object.getClass(), .class literals, and Class.forName.
Java Class Objects
Everything in reflection starts from a Class object. For every type the JVM loads — every class, interface, array type, enum, annotation, and even each primitive — there is exactly one Class instance describing it. That object is your handle on the type's structure: its name, its superclass, its members, its annotations. This chapter covers the three ways to obtain a Class, what Class<T> carries, and the small surprises that catch people out.
Three ways to get a Class
There are exactly three routes, and each fits a different situation.
1. The .class literal — you know the type at compile time.
Class<String> c1 = String.class;
Class<int[]> c2 = int[].class;
Class<Integer> c3 = int.class == Integer.class ? null : int.class; // see "primitives" belowThis is compile-time safe and the fastest — there's no lookup, the compiler bakes in a reference. Use it whenever you can name the type.
2. Object.getClass() — you have an instance.
Object o = "hello";
Class<?> c = o.getClass(); // class java.lang.StringgetClass() returns the runtime class of the object, which may be a subclass of the declared variable type. Object o = new ArrayList<>() gives o.getClass() == ArrayList.class, not Object.class. Its static type is Class<?> because the compiler only knows o is some Object.
3. Class.forName(String) — you have only a name.
Class<?> c = Class.forName("java.util.ArrayList");This is the dynamic route: a fully-qualified class name as a string, resolved at runtime. It throws ClassNotFoundException if no such class is loadable. This is what plugin loaders and JDBC drivers use. A loaded-but-not-initialised variant exists: Class.forName(name, false, classLoader) skips static initializers until the class is first actively used.
What Class<T> carries
A Class object is a rich description. The headline methods:
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();From a Class you reach every member: getDeclaredFields(), getDeclaredMethods(), getDeclaredConstructors(), plus their public/inherited get… counterparts (the split from the intro chapter). The later chapters drill into each.
Binary name vs. simple name vs. canonical name
The naming methods differ in ways that bite when you log or compare them:
| Type | getName() | getSimpleName() | getCanonicalName() |
|---|---|---|---|
String | java.lang.String | String | java.lang.String |
int[] | [I | int[] | int[] |
String[] | [Ljava.lang.String; | String[] | java.lang.String[] |
nested Map.Entry | java.util.Map$Entry | Entry | java.util.Map.Entry |
| anonymous class | Outer$1 | "" (empty) | null |
getName() is the binary name — the JVM's internal form, with $ for nesting and the cryptic [I / [L…; array encoding. It's what Class.forName expects. getCanonicalName() is the source form you'd write, and it's null for types you can't name in source (locals, anonymous classes). Use getName() for forName round-trips; use getSimpleName()/getCanonicalName() for human output.
Primitives and arrays have Class objects too
Every primitive has its own Class, distinct from its wrapper:
int.class == Integer.class // false — two different Class objects
int.class.getName() // "int"
Integer.TYPE == int.class // true — TYPE is the primitive Classvoid even has void.class (and Void.TYPE). Array classes are synthesised by the JVM: int[].class, String[][].class. arrayClass.getComponentType() peels one dimension (String[].class.getComponentType() is String.class). These distinctions matter when you match parameter types in getMethod — getMethod("foo", int.class) and getMethod("foo", Integer.class) find different overloads.
Class identity and class loaders
A Class object's identity isn't just its name — it's the pair (name, defining class loader). The same .class file loaded by two different class loaders produces two distinct, incompatible Class objects. A cast between them throws ClassCastException even though the names match. This is mostly invisible in a plain application (one loader) but is the root of many "but it's the same class!" puzzles in app servers, OSGi, and hot-reload systems. For everyday reflection, treat Class objects as singletons per type and compare them with ==.
A worked example: probing types three ways
The program obtains Class objects through all three routes, then interrogates a handful of types — a regular class, an interface, an array, a primitive, and a nested type — to surface the naming and structural differences.
What to take from the run:
- All three routes converged on the same kind of object: a
.classliteral, agetClass()call, and aforNamelookup each produced a fully usableClass. The route you pick is about what you know (the type, an instance, or just a name) — the result is identical in capability. getClass()on theGreeter gvariable returnedRobot, notGreeter. The declared type is irrelevant;getClass()always reports the concrete runtime class. That's why polymorphic dispatch and reflective inspection see the same "real" type.- The three naming methods diverged exactly where the table predicts:
String[]printed binary name[Ljava.lang.String;fromgetName()but the readableString[]from the simple and canonical forms. If you ever feed a name back intoforName, it must be thegetName()form. int.class == Integer.classwasfalsewhileInteger.TYPE == int.classwastrue. The primitive and its wrapper are distinctClassobjects, andInteger.TYPEis just an alias for the primitive one. Mixing them up is the classic cause ofNoSuchMethodExceptionwhen you look up an overload by parameter type.Robot.class == new Robot().getClass()wastrue: within one class loader, a type maps to exactly oneClassobject, so==is the correct comparison. You never need.equals()onClassobjects in single-loader code.
Common pitfalls
forNameruns static initializers (in its one-arg form). Loading a class can have side effects. Use the three-arg form withinitialize=falseif you only want to inspect.getSimpleName()can be empty (anonymous classes) andgetCanonicalName()can benull(locals, anonymous). Don't assume they're always printable identifiers.- Generics are erased.
List<String>.classis illegal; there's onlyList.class. AClasscarries no type-argument information — that lives inType/ParameterizedType, a separate (and more advanced) reflection API.
With the Class object in hand, the next chapter opens the first drawer of members: fields — inspecting them, reading them, and writing them, even when they're private or final.
Practice
You have 'Object o = new java.util.LinkedList<String>();' declared as 'Object'. You call 'o.getClass().getName()'. What string comes back, and why isn't it 'java.lang.Object'?