Reflection in Java starts with one type: java.lang.Class. Every operation that inspects code at runtime begins by getting a Class object for the type you want to look at. This lesson covers what a Class object is, why every loaded type has exactly one of them, and the four ways you can obtain one. It also covers the generic shape Class<T>, the Class objects that exist for primitives and arrays, and the small set of Class methods that return type-level information.
java.lang.Class IsA Class object is the JVM's runtime description of a type. When source code mentions Product, the compiler resolves it to a class file at build time. When the JVM loads that class, it builds an in-memory representation of the type and exposes it as an instance of java.lang.Class. From that point on, anything to be known about the type at runtime, its name, its modifiers, its superclass, its declared methods, its annotations, comes from that one Class object.
The Class type is generic: Class<T>. The T is the type the Class describes. Class<Product> is the Class object that describes the Product type. The generic parameter lets the compiler track which type a particular Class reference belongs to, which matters for the cast() and newInstance() style methods.
Here is the shortest possible program that gets a Class object and prints something about it.
Product.class is a special expression called a class literal. It evaluates to the Class object that describes Product. The fully qualified name shows the enclosing class because Product is a nested type.
The Class object is not the same thing as an instance of the class. A Product is created with new Product(). The Class<Product> is the metadata about the Product type itself. There is exactly one Class<Product> in the JVM (for a given class loader), but there can be billions of Product instances.
Class Object Per Loaded TypeThe JVM loads a class once per class loader and caches the resulting Class object. Every other reference to that type, every new expression, every Product.class, every getClass() call, returns the same Class instance. Reference equality verifies this.
Both cart1 and cart2 are different Cart instances, but they share the same Class<Cart> object. The class literal Cart.class is also the same object. This is a strong invariant of the JVM: identity comparison with == is the correct way to test whether two Class references refer to the same type.
The diagram shows three Cart instances all pointing at the same Class<Cart> object. The instances live on the heap and store the per-object state (item counts, prices). The Class<Cart> lives in the JVM's metaspace and stores the type-level information (field declarations, method signatures, annotations, superclass).
The "per class loader" caveat matters in containers like web servers and IDE plugins, where the same class file can be loaded by two different class loaders and end up as two distinct Class objects. For ordinary single-classloader applications, one Class per type is the working model.
Class ObjectJava has four different paths to a Class object. Each one suits a different situation: is the type known at compile time, or only at runtime as a string? Is there an instance in hand, or only the type? The four paths are summarized below, then walked through one at a time.
The decision tree captures the rule of thumb. With an instance in hand, ask it. When the type name is known at compile time, use the class literal. With only a string at runtime, use Class.forName. To control which class loader resolves the name, go straight to the class loader.
Type.classA class literal is the expression SomeType.class. It is the simplest way to get a Class object when the type is known at compile time. The compiler resolves it like any other type reference, so a misspelled type is a compile error rather than a runtime exception.
The class literal is the fastest and safest of the four approaches. The JVM has already loaded the class to compile the literal, so there is no extra work to do at runtime. The variable's static type is Class<Product>, which lets the compiler track the relationship between the literal and any later uses.
Use the class literal when the type is fixed and known at compile time, which is the usual case in application code that calls into a framework, for example handing Product.class to a serializer or a query builder.
Type.class has effectively no runtime cost. The reference is loaded directly from the constant pool. There is no name lookup, no class loader call, no exception to catch.
getClass() on an InstanceEvery object has a getClass() method inherited from Object. It returns the Class of the actual runtime type of the object, not the declared type of the variable holding it.
The variable item is declared as Product, but it holds a DiscountedProduct. getClass() returns the runtime type, DiscountedProduct, not the declared type. This is the most common way to write code that reacts to the actual type of an object, for example a logger that prints the concrete class name in error messages.
The return type of getClass() is Class<? extends ActualType>, where ActualType is the declared type of the receiver. That bound is what lets the compiler reason about cart.getClass() versus product.getClass().
Use getClass() with an object in hand to inspect its real type, for example in equals implementations, in serialization code that branches on concrete type, or in debug logging.
Class.forName(name)When the type is not known at compile time, only as a string supplied by configuration or user input, use Class.forName. The method takes the fully qualified class name and asks the current class loader to load that class, returning its Class object.
The return type is Class<?> because the compiler cannot know the type at compile time; the name is just a String. If the class cannot be found on the classpath, Class.forName throws ClassNotFoundException, which is a checked exception that must be declared or caught.
The catch block runs because the JVM cannot resolve com.shop.NotARealType against the classpath. This is the typical failure mode of Class.forName, and it is one of the reasons to prefer the class literal whenever the type is known at compile time. A typo in Product.class is a compile error visible immediately. A typo in Class.forName("Prdouct") is a runtime exception that only surfaces when that code path runs.
Class.forName does more than just resolve the name. By default it also initializes the class, which means it runs any static initializers in the class. This is a subtle side effect.
Loading the class triggered the static block. For most types this is harmless, but for classes that open files, connect to databases, or register listeners in their static initializers, calling Class.forName has visible side effects. A three-argument overload skips the initialization step.
The second argument is initialize. Passing false loads the class without running its static initializers. The third argument is the class loader to use. The static block will still run later, the first time the class is actually used (an instance is created, a static field is read, a static method is called), but not as part of the forName call.
Class.forName(name) involves a class-loader lookup plus, by default, full class initialization. The lookup itself is fast for already-loaded classes (a hash map probe) but slow on a cold path. Cache the result if it's called in a loop.
Use Class.forName when the type name comes from configuration, a database, a serialized format, a command-line argument, or any other runtime source. Avoid using it for types that are already on the compile-time classpath; the class literal is shorter and safer.
ClassLoaderThe fourth path is the lowest-level: ask a specific ClassLoader to load a class by name. Class.forName is really a convenience that delegates to a class loader anyway; calling loadClass directly chooses which one.
ClassLoader.loadClass does not initialize the class. It just resolves the name to a Class object. For both behaviors, the loader plus the initialization, use Class.forName.
This path matters with multiple class loaders in play. Application servers, plugin systems, OSGi containers, and isolated test runners each load classes in their own loader. To load a class out of a specific loader (say, a plugin's loader rather than the application's loader), call loadClass on that loader directly.
For ordinary application code that is not building its own class loaders, loadClass is rarely called directly. Class.forName covers the common cases.
| Way | Syntax | Compile-time check | Initializes the class | Throws |
|---|---|---|---|---|
| Class literal | Product.class | Yes (type must exist) | No, the class is already initialized when the JVM reaches the literal in a running program (it's typically initialized on use) | None |
getClass() | product.getClass() | Yes (must be a reference type) | N/A (class is already loaded; you have an instance) | None |
Class.forName | Class.forName("com.shop.Product") | No (string only) | Yes by default; no with the 3-arg overload when initialize=false | ClassNotFoundException (checked) |
ClassLoader.loadClass | loader.loadClass("com.shop.Product") | No (string only) | No | ClassNotFoundException (checked) |
The pattern across the table: stronger compile-time guarantees on the left, more runtime flexibility on the right. Move to the right only when the flexibility is required.
Class<T>Class is a generic type. The parameter T records which type the Class object describes. Using it well removes most of the awkward casts that scarred earlier reflection code.
The commented-out line would not compile, because Class<String> is not assignable to Class<Product>. This is the payoff of the generic parameter: it prevents confusing one type's Class object with another's.
The raw form, Class with no parameter, still exists for backwards compatibility but should be avoided in new code. It has no type safety and forces casts at every use site.
All three variables point at the same Class object. The difference is what the compiler allows for each one. The raw form Class produces unchecked warnings on most operations. The wildcard Class<?> is fine but limits what can be called. The fully typed Class<String> gets the most help from the compiler.
A type token is the pattern of passing a Class<T> argument to a method to indicate which type to work with. Jackson's readValue(json, Order.class), JDBC's getObject(column, Long.class), and Spring's getBean(MyService.class) all use this pattern. The token serves two purposes: it tells the method which type to produce, and the generic parameter T flows through so the caller does not need a cast.
The method signature <T> T parseAs(String, Class<T>) ties the return type to the class token. Calling parseAs("42", Integer.class) returns Integer, calling it with Double.class returns Double, and so on, all without a cast at the call site. This is the same shape Jackson and friends use internally.
Class.cast(Object)The cast method on Class is a typed version of a Java cast. Given an Object, it checks at runtime whether the object is an instance of T and either returns it as T or throws ClassCastException.
The first call succeeds because raw is a Product. The second fails because "not a product" is a String. The cast method is useful inside reflection code where a Class<T> token is available but the value is typed as Object. targetType.cast(value) is the typed equivalent of (T) value without the unchecked-warning baggage.
voidReflection does not stop at reference types. Primitives, arrays, and even void each have their own Class object. They behave a little differently from regular types, but they show up in method signatures and return types, so they have to be representable somehow.
Class ObjectsEvery primitive type has a class literal: int.class, double.class, boolean.class, and so on for the eight primitives. They are the canonical way to refer to a primitive type from reflection.
The getName() of a primitive Class object is just the primitive's name, like int or boolean. The isPrimitive() method returns true. The generic parameter is the corresponding wrapper type, which is why int.class is typed as Class<Integer>. That coupling matches autoboxing at the language level.
Each wrapper class also exposes its primitive partner through a static field named TYPE. Integer.TYPE is the same object as int.class, Long.TYPE is the same as long.class, and so on.
The first comparison is true because Integer.TYPE is documented to be int.class. The second is false because Integer.class is the Class object for the wrapper, not for the primitive. They are two different Class objects, and reflection code that mixes them up will silently look at the wrong type.
void.classvoid is not a type for declaring variables, but it can appear as a method return type, and reflection needs a way to represent it. void.class is its Class object, and Void.TYPE is the equivalent through the Void wrapper.
Reflection-driven serializers and proxy generators look at the return type of a method to decide whether to return a value, and == void.class is the standard test.
Class ObjectsArrays also have Class objects, and they have a distinctive name format. The name of an array type starts with [ characters (one per dimension) followed by a type code or a class name.
The internal name format is JVM-level. [I is "array of int". [LSomeClass; is "array of reference type SomeClass". [[I is "array of array of int", the two-dimensional case. Most application code only sees these when it prints Class.getName(); for display purposes, getSimpleName() (which would be int[], Product[], int[][]) is friendlier.
The getComponentType() method returns the Class of the element type. For int[].class it returns int.class. For Product[].class it returns Product.class. For int[][].class it returns int[].class, peeling off one dimension at a time.
| Expression | Internal name | getSimpleName() | isArray() | isPrimitive() |
|---|---|---|---|---|
int.class | int | int | false | true |
Integer.class | java.lang.Integer | Integer | false | false |
int[].class | [I | int[] | true | false |
Integer[].class | [Ljava.lang.Integer; | Integer[] | true | false |
Product[].class | [L...Product; | Product[] | true | false |
int[][].class | [[I | int[][] | true | false |
void.class | void | void | false | true |
The table shows the full picture. isPrimitive() returns true for primitives and for void. isArray() returns true only for arrays. Everything else, including wrapper classes, returns false for both.
Class Methods for Type InformationThe Class API is large, but a small subset gives the high-level shape of a type. The methods below answer "what is this type" without enumerating its members.
Each line corresponds to one method on Class. The next table walks through them.
| Method | What it returns | Use case |
|---|---|---|
getName() | JVM-internal name including enclosing class with $ and array prefixes like [I | Logging, serialization, anything that needs a stable identifier |
getSimpleName() | The short name of the class, no package, no enclosing class | Display strings, user-facing messages |
getCanonicalName() | The dotted source-level name, or null for anonymous and local classes | Error messages, generating source code |
getPackage() | The Package of the class, or null for unnamed packages and primitives | Reading package-level annotations |
getSuperclass() | The Class of the direct superclass, or null for Object, interfaces, primitives, and void | Walking the inheritance chain |
getInterfaces() | The directly implemented interfaces as an array of Class objects | Checking whether a type implements something |
getModifiers() | An int bitmask of modifiers (public, static, final, etc.) | Filtering for static, abstract, or public types |
isInterface() | true if the Class represents an interface | Skipping interfaces during class scanning |
isArray() | true if it represents an array type | Branching on array versus scalar handling |
isPrimitive() | true for the eight primitives and void | Wrapper/primitive conversions |
isEnum() | true if it represents an enum class | Generating enum-aware code |
The naming methods are easy to confuse. The three of them, getName, getSimpleName, and getCanonicalName, return three different strings for a nested class.
getName() uses the JVM's internal naming convention with $ for nested types. getSimpleName() strips everything except the immediate class name. getCanonicalName() returns the form written in Java source code, with dots. For anonymous classes, getSimpleName() returns the empty string and getCanonicalName() returns null.
The modifier methods are integer bitmasks. Modifier.toString(int) formats them as the source-level keywords.
Modifier is a separate utility class in java.lang.reflect. It exposes one bit-test method per modifier (isFinal, isStatic, isAbstract, isPublic, and so on), plus the toString formatter. The bit layout is part of the JVM specification, so the bits can also be tested manually, but the helpers are clearer.
The pieces above compose into a small utility that prints a summary of any type. The same pattern shows up in JSON serializers, logging libraries, and IDE inspectors.
Four different Class objects, obtained four different ways: a class literal, a primitive literal, an array literal, and Class.forName. They all share the same API. The summary picks out the bits that matter and prints them in a uniform shape. A curious result for int.class and String[].class: their modifiers include public abstract final. The JVM reports primitives and arrays this way; the bits are not meaningful in the source-level sense, just a quirk of how reflection presents them.
This is roughly the shape of what frameworks do at startup, but multiplied across hundreds or thousands of classes. They scan the classpath, get a Class object for each one, look at the modifiers and interfaces to filter the candidates (skip abstract types, keep types annotated with @Service, etc.), and then dig into the members.
10 quizzes