W3docs

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" below

This 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.String

getClass() 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:

TypegetName()getSimpleName()getCanonicalName()
Stringjava.lang.StringStringjava.lang.String
int[][Iint[]int[]
String[][Ljava.lang.String;String[]java.lang.String[]
nested Map.Entryjava.util.Map$EntryEntryjava.util.Map.Entry
anonymous classOuter$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 Class

void 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 getMethodgetMethod("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.

java— editable, runs on the server

What to take from the run:

  • All three routes converged on the same kind of object: a .class literal, a getClass() call, and a forName lookup each produced a fully usable Class. 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 the Greeter g variable returned Robot, not Greeter. 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; from getName() but the readable String[] from the simple and canonical forms. If you ever feed a name back into forName, it must be the getName() form.
  • int.class == Integer.class was false while Integer.TYPE == int.class was true. The primitive and its wrapper are distinct Class objects, and Integer.TYPE is just an alias for the primitive one. Mixing them up is the classic cause of NoSuchMethodException when you look up an overload by parameter type.
  • Robot.class == new Robot().getClass() was true: within one class loader, a type maps to exactly one Class object, so == is the correct comparison. You never need .equals() on Class objects in single-loader code.

Common pitfalls

  • forName runs static initializers (in its one-arg form). Loading a class can have side effects. Use the three-arg form with initialize=false if you only want to inspect.
  • getSimpleName() can be empty (anonymous classes) and getCanonicalName() can be null (locals, anonymous). Don't assume they're always printable identifiers.
  • Generics are erased. List<String>.class is illegal; there's only List.class. A Class carries no type-argument information — that lives in Type/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

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'?