Java Reflection API: the Class Object and getClass()
A variable is declared as Object, yet o.getClass() prints class java.lang.String. That is not a bug: getClass() returns the actual runtime type of the object, not the declared type of the variable. This is the starting point of reflection — a program finding out things about its own classes that the compiler did not know at the call site.
The Java Reflection API is a mechanism for inspecting and using a program's classes at runtime. With reflection you can find out which fields, methods and constructors a class has, create an object from a class name, call a method and read or change a field — even a private one. The entry point is the java.lang.Class class (the «class object»), and the tools for working with class members live in the java.lang.reflect package.
1. What is reflection in Java
Reflection in Java is the ability of a running program to get a description of any loaded class (its name, superclass, modifiers, fields, methods and constructors) and to work with its members without knowing their names at compile time. The Reflection API consists of the Class class plus Field, Method and Constructor from java.lang.reflect.
Application code rarely uses reflection directly, but the tools you rely on every day are built on it:
- JUnit finds methods annotated with
@Testand calls them; - Spring creates beans and injects dependencies into fields and constructors;
- Hibernate and Jackson read and fill object fields when saving to a database or parsing JSON;
- IDEs and debuggers display the structure of classes.
2. The Class object in Java
The java.lang.Class class describes a type: for every loaded class, interface, enum, array type and primitive type the JVM creates exactly one Class object. Through this class object you can find out:
- whether the type is an interface, an array or a primitive;
- what its superclass is and which interfaces it implements;
- its fully qualified and simple name, its package and its modifiers;
- which constructors, fields, methods and nested classes it declares.
Class has no public constructors: the JVM creates the object when it loads the class. The class is generic — Class<String> describes String; when the type is not known in advance, write Class<?> rather than the raw Class (type parameters and wildcards are explained in Java Generics).
3. getClass() vs .class vs Class.forName()
The getClass() method is a final method of Object that returns the Class object describing the actual runtime type of an object. Without an object you get the same Class with the class literal Name.class (type known at compile time) or with Class.forName("fully.qualified.Name") (name known only at runtime). All three return the very same object.
Other methods every class inherits from Object are covered in Object Class in Java and Its Methods.
Example 1. Three ways to get the Class object for String
public class ClassExample1 {
public static void main(String[] args) throws ClassNotFoundException {
Object s = "Hello";
Class<?> c1 = s.getClass(); // from an object
Class<?> c2 = String.class; // class literal
Class<?> c3 = Class.forName("java.lang.String"); // from a fully qualified name
System.out.println(c1);
System.out.println(c1 == c2 && c2 == c3);
System.out.println(c1.getName() + " / " + c1.getSimpleName());
}
}
class java.lang.String
true
java.lang.String / String
All three variables reference the same object, so == returns true. The declared type of s is Object, but getClass() returned String.
| Way | When to use it | Initializes the class (static blocks) | What happens on error |
|---|---|---|---|
obj.getClass() | You have an object and need its actual runtime type | Already initialized — the object exists | NullPointerException if obj == null; does not compile on a primitive variable |
Name.class | Type known at compile time; also works for primitives and arrays: int.class, int[].class | No | Unknown class — compile error |
Class.forName("pkg.Name") | The name comes from configuration, a plugin or a string; the classic way to load a JDBC driver | Yes | Checked ClassNotFoundException; also for primitive names such as "int" |
Because ClassNotFoundException is a checked exception, code calling Class.forName() must catch it or declare it with throws — see Java exceptions.
4. Checking the kind of type: isArray(), isInterface(), isPrimitive()
The boolean methods isArray(), isInterface() and isPrimitive() tell you what kind of type a Class object describes: an array, an interface or one of the eight primitive types (plus void.class). Wrapper classes such as Integer are not primitives.
Example 2. An array, an interface, a wrapper and a primitive
import java.io.Serializable;
public class ClassExample2 {
public static void main(String[] args) {
int[] array = new int[4];
printInfo(array.getClass());
printInfo(Serializable.class);
printInfo(Integer.class);
printInfo(double.class);
}
private static void printInfo(Class<?> c) {
System.out.println(c.getName() + " (" + c.getSimpleName() + ")");
System.out.println(" isArray: " + c.isArray()
+ ", isInterface: " + c.isInterface()
+ ", isPrimitive: " + c.isPrimitive());
}
}
[I (int[])
isArray: true, isInterface: false, isPrimitive: false
java.io.Serializable (Serializable)
isArray: false, isInterface: true, isPrimitive: false
java.lang.Integer (Integer)
isArray: false, isInterface: false, isPrimitive: false
double (double)
isArray: false, isInterface: false, isPrimitive: true
Note the [I: this is how getName() writes the int[] array type (the JVM's internal name). For a readable name use getSimpleName() or getTypeName().
5. Superclass, package and modifiers
The methods getSuperclass(), getPackageName() and getModifiers() return the parent class, the package name and the declaration modifiers of a class. Modifiers come back as an int bit mask, so to read them you pass the value to java.lang.reflect.Modifier.toString().
import java.lang.reflect.Modifier;
public class ClassInfoExample {
public static void main(String[] args) {
Class<?> c = Integer.class;
System.out.println(c.getSuperclass()); // class java.lang.Number
System.out.println(c.getPackageName()); // java.lang
System.out.println(Modifier.toString(c.getModifiers())); // public final
System.out.println(Object.class.getSuperclass()); // null
System.out.println(Runnable.class.getSuperclass()); // null
System.out.println(int[].class.getSuperclass()); // class java.lang.Object
}
}
class java.lang.Number
java.lang
public final
null
null
class java.lang.Object
| Method | What it returns | Edge case |
|---|---|---|
getName() | Fully qualified (binary) name: java.lang.String | For arrays — [I, [Ljava.lang.String;; for a nested class — java.util.Map$Entry |
getSimpleName() | Name without the package: String, int[] | Empty string for an anonymous class |
getSuperclass() | The Class object of the parent class | null for Object, interfaces, primitives and void |
getInterfaces() | Array of interfaces listed in implements (or in extends of an interface) | Interfaces of superclasses are not included; empty array if there are none |
getPackageName() (Java 9+), getPackage() | Package name as a string / a Package object | getPackageName() returns an empty string for a class in the default package |
getModifiers() | Bit mask of modifiers (int) | Read with Modifier.toString(), Modifier.isFinal() and similar |
isArray(), isInterface(), isPrimitive() | true/false for the kind of type | isPrimitive() is false for Integer.class and true for void.class |
6. Constructors, methods and fields: getDeclared*() vs get*()
Class has two groups of methods for class members. getDeclaredFields(), getDeclaredMethods() and getDeclaredConstructors() return everything declared in the class itself, including private members, but nothing inherited. getFields(), getMethods() and getConstructors() return only public members, but together with inherited ones.
| Members | All declared in the class (any access) | Public only, including inherited |
|---|---|---|
| Fields | getDeclaredFields() | getFields() |
| Methods | getDeclaredMethods() | getMethods() — including the public methods of Object |
| Constructors | getDeclaredConstructors() | getConstructors() — own ones only: constructors are not inherited |
| Nested classes | getDeclaredClasses() | getClasses() |
To get a single member by name there are singular versions: getDeclaredField("name"), getMethod("name", Class<?>... parameterTypes) and so on. If the member does not exist, they throw NoSuchFieldException or NoSuchMethodException.
Example 3. How getDeclared*() differs from get*() (HeavyBox extends Box — see Java Inheritance and the extends Keyword)
import java.lang.reflect.Member;
import java.util.Arrays;
class Box {
public double width;
protected double height;
double depth;
public Box() {
}
public double volume() {
return width * height * depth;
}
}
class HeavyBox extends Box {
public double weight;
private String label;
public HeavyBox() {
}
private HeavyBox(double weight) {
this.weight = weight;
}
public double getWeight() {
return weight;
}
private void secret() {
}
}
public class ClassExample3 {
public static void main(String[] args) {
Class<HeavyBox> c = HeavyBox.class;
System.out.println("Declared constructors: " + c.getDeclaredConstructors().length);
System.out.println("Public constructors: " + c.getConstructors().length);
System.out.println("Declared methods: " + names(c.getDeclaredMethods()));
System.out.println("Public methods: " + c.getMethods().length);
System.out.println("Declared fields: " + names(c.getDeclaredFields()));
System.out.println("Public fields: " + names(c.getFields()));
}
// The order of array elements is not guaranteed, so we sort the names
private static String names(Member[] members) {
return Arrays.toString(Arrays.stream(members).map(Member::getName).sorted().toArray());
}
}
Declared constructors: 2
Public constructors: 1
Declared methods: [getWeight, secret]
Public methods: 11
Declared fields: [label, weight]
Public fields: [weight, width]
Breakdown: getDeclaredMethods() sees the private secret() but not the inherited volume(). getMethods() returns 11 methods: getWeight(), volume() from Box and the 9 public methods of Object (equals, hashCode, toString, getClass, notify, notifyAll and three wait overloads). getFields() includes the inherited width, but not protected height or package-private depth.
7. Reflection API example: invoking methods and accessing fields
Constructor, Method and Field objects do more than describe class members — they let you use them: newInstance() creates an object, invoke() calls a method, and get()/set() read and write a field. Access to private members is opened with setAccessible(true).
Example 4. Create an object from a class name, call a method, change a private field (uses the Box and HeavyBox classes from example 3 — put the files in the same directory)
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ReflectionInvokeExample {
public static void main(String[] args) throws Exception {
Class<?> c = Class.forName("HeavyBox"); // class in the default package
Object box = c.getDeclaredConstructor().newInstance();
Field weight = c.getField("weight");
weight.setDouble(box, 12.5);
Method getWeight = c.getMethod("getWeight");
System.out.println(getWeight.invoke(box)); // 12.5
Field label = c.getDeclaredField("label"); // private field
label.setAccessible(true);
label.set(box, "Fragile");
System.out.println(label.get(box)); // Fragile
Method secret = c.getDeclaredMethod("secret"); // private method
secret.setAccessible(true);
secret.invoke(box);
System.out.println("secret() called");
c.getMethod("secret"); // getMethod() sees public methods only
}
}
12.5
Fragile
secret() called
Exception in thread "main" java.lang.NoSuchMethodException: HeavyBox.secret()
The private secret() is found by getDeclaredMethod() and runs after setAccessible(true). The last line fails because getMethod() searches public methods only, so the same private method is invisible to it.
Deprecated approach
Older examples create objects with c.newInstance(). Since Java 9 this method is @Deprecated: it rethrows checked exceptions of the constructor, bypassing the compiler. Use c.getDeclaredConstructor().newInstance() instead — constructor exceptions then arrive wrapped in InvocationTargetException.
Modules and setAccessible()
For your own classes setAccessible(true) works without restrictions. For internal fields of JDK classes (for example, value in String) it throws InaccessibleObjectException since Java 16–17: the packages of the java.base module are closed to deep reflection. You can open them only explicitly, with the JVM flag --add-opens.
8. Where developers get tripped up
The most common mistakes with the Class object are three: confusing getClass() with instanceof, expecting generic types to differ at runtime, and forgetting that Class.forName() runs the static blocks of a class.
getClass() compares the exact type, instanceof also accepts subclasses. Generics are erased at compile time, so ArrayList<String> and ArrayList<Integer> share one Class object:
Box box = new HeavyBox();
System.out.println(box instanceof Box); // true
System.out.println(box.getClass() == Box.class); // false: the runtime type is HeavyBox
System.out.println(new ArrayList<String>().getClass()
== new ArrayList<Integer>().getClass()); // true
Object o = null;
o.getClass(); // NullPointerException
A nested class in Class.forName() is written with $. Class.forName("java.util.Map.Entry") throws ClassNotFoundException; the correct binary name is "java.util.Map$Entry", and the call returns interface java.util.Map$Entry. The class literal Map.Entry.class, on the other hand, uses a dot.
What does this code print?
class Config {
static {
System.out.println("Config loaded");
}
}
public class InitDemo {
public static void main(String[] args) throws Exception {
Class<?> a = Config.class;
System.out.println("after .class");
Class<?> b = Class.forName("Config");
System.out.println(a == b);
}
}
after .class
Config loaded
true
The literal Config.class does not initialize the class, so the static block does not run. Class.forName(String) does initialize it — that is why JDBC drivers used to be loaded with Class.forName("com.mysql.cj.jdbc.Driver"): the driver registered itself in a static block. If you do not need initialization, use the overload Class.forName(name, false, classLoader).
Frequently asked questions
Should equals() use getClass() or instanceof?
Both are valid, but they mean different things. getClass() != o.getClass() treats a subclass object as not equal to a parent object and keeps equals() symmetric under inheritance. instanceof allows comparison with subclasses — fine when subclasses add no fields that take part in the comparison, or when the class is final. The difference between the two checks is shown in Where developers get tripped up.
Why is reflection discouraged in everyday code?
Errors the compiler would catch immediately (a typo in a method name, a wrong argument type) surface only at runtime as NoSuchMethodException or IllegalArgumentException. A call through Method.invoke() is slower than a direct call, and setAccessible(true) bypasses encapsulation. Reflection pays off in frameworks, serialization libraries, tests and tools where types are not known in advance.
Can reflection change a private or final field?
A private field of your own class — yes, after setAccessible(true). A final instance field of an ordinary class can also be overwritten with Field.set(), but not in records and not static final fields — there you get IllegalAccessException. Compile-time constants (static final int MAX = 10) are inlined by the compiler, so even a successful write does not change already compiled code. Newer JDK releases are gradually restricting final field mutation through reflection (JEP 500).
Why write Class<?> instead of plain Class?
The raw type Class switches off generic type checks and triggers a raw type compiler warning. Class<?> means «a Class object of some unknown type» and is safe, while Class<HeavyBox> gives typed results: HeavyBox.class.getDeclaredConstructor().newInstance() returns a HeavyBox without a cast. getClass() itself is typed as Class<? extends X>, where X is the static type of the expression.
Comments