Using the Java Reflection API for Runtime Class Analysis
java reflection api: Learn how to inspect classes, invoke methods, and read annotations at runtime with the Java Reflection API, including performance and security tra...
The java reflection api lets a running program inspect its own classes, fields, methods, and annotations. It is the mechanism behind many frameworks that need to work with classes the compiler never saw. The core entry point is Class<?>, which you obtain from an object with getClass(), from a type literal with String.class, or from a fully qualified name with Class.forName("com.example.Order").
Accessing Class Metadata at Runtime
The Class object is the starting point for all reflection work. Once you have it, you can query the class's modifiers, superclass, implemented interfaces, and declared members.
Class<?> clazz = Order.class; System.out.println(clazz.getSimpleName()); // Order System.out.println(Modifier.isPublic(clazz.getModifiers())); // true System.out.println(clazz.getSuperclass()); // class java.lang.Object
The getModifiers() method returns an integer bitmask. The Modifier helper class decodes it into readable checks like isPublic, isFinal, or isAbstract. This is useful when you need to decide whether a class can be instantiated or subclassed at runtime.
There is an important distinction between getFields() and getDeclaredFields(). The first returns public fields from the class and all its superclasses. The second returns every field declared directly on the class, regardless of visibility. Most reflection code uses the getDeclared* variants because framework code usually needs private members too.
Inspecting Fields and Methods
Once you have a Class<?>, you can enumerate its fields and methods.
for (Field field : clazz.getDeclaredFields()) { System.out.println(field.getName() + " : " + field.getType().getSimpleName()); } for (Method method : clazz.getDeclaredMethods()) { System.out.println(method.getName() + "(" + Arrays.stream(method.getParameterTypes()) .map(Class::getSimpleName) .collect(Collectors.joining(", ")) + ")"); }
Each Field and Method object carries metadata: name, type, parameter types, return type, modifiers, and annotations. You can also read or write a field's value on a specific object instance, provided you handle visibility.
Field idField = clazz.getDeclaredField("id"); idField.setAccessible(true); Long id = (Long) idField.get(orderInstance);
setAccessible(true) suppresses Java language access checks for that member. This is how frameworks read private fields. It works in most application code, but the module system introduced in Java 9 can block it when the target class is in a different module that does not open its package.
Invoking Methods Dynamically
The Method class exposes invoke(), which calls the method on a target instance.
Method totalMethod = clazz.getMethod("getTotal"); BigDecimal total = (BigDecimal) totalMethod.invoke(orderInstance);
For a static method, pass null as the target. For a method with parameters, pass them as varargs after the target. The return value is always boxed as Object; primitive results are wrapped in their wrapper types.
Method resolution is exact: getMethod("setName", String.class) looks for a public method named setName that accepts exactly one String parameter. If you pass the wrong parameter types, you get NoSuchMethodException. If the method throws an exception during invocation, reflection wraps it in InvocationTargetException, and you must unwrap the cause with getCause() to see the original failure.
try { totalMethod.invoke(orderInstance); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); // handle the original exception, not the wrapper }
Reading and Applying Annotations
Reflection is the standard way to read annotations at runtime. Annotation types marked with @Retention(RetentionPolicy.RUNTIME) are visible to reflection; those with CLASS or SOURCE retention are not.
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Column { String name(); }
You can then inspect fields for this annotation and act on it.
for (Field field : clazz.getDeclaredFields()) { Column column = field.getAnnotation(Column.class); if (column != null) { String columnName = column.name(); // map field to database column } }
This pattern is the basis for ORMs and serialization libraries. The annotation carries metadata, and reflection applies it at runtime. The same mechanism works on classes, methods, and parameters, using getAnnotation() or getAnnotationsByType() for repeatable annotations.
Performance Costs of Reflection
Reflection is slower than direct code for several reasons. Each lookup, such as getDeclaredField() or getMethod(), performs a search through the class's metadata. Each invoke() call goes through a layer of argument boxing, access checking, and exception wrapping that a direct call does not have.
The practical rule is to cache reflective lookups. If a framework processes many objects of the same type, it should resolve fields and methods once and reuse those Field and Method instances.
private final Map<Class<?>, Field[]> fieldCache = new ConcurrentHashMap<>(); Field[] fieldsFor(Class<?> clazz) { return fieldCache.computeIfAbsent(clazz, Class::getDeclaredFields); }
The JIT can sometimes inline reflective calls after repeated execution, but relying on that is fragile. Caching the metadata is cheap and removes the most expensive part of the operation. When performance is critical, code generation at build time, such as generating accessor classes, avoids reflection entirely.
Security and Compatibility Constraints
Reflection bypasses normal encapsulation. A class that relies on private state can be modified from outside, which breaks invariants. Libraries should document when they mutate private fields and should prefer public or protected accessors when they exist.
The Java module system restricts reflection across module boundaries. A class in another module is not accessible unless that module opens its package. This is why many frameworks require --add-opens flags or use the Module API to open packages programmatically. On Java 8 and earlier, setAccessible(true) worked almost everywhere; on Java 9 and later, it can throw InaccessibleObjectException.
There is also a security angle. Reflection can invoke methods that were never intended to be called from outside, so a library that accepts arbitrary class names from user input must validate that input carefully. Allowing a user to name any class and invoke any method is effectively arbitrary code execution.
When to Avoid Reflection
Reflection is the right tool when you need to handle types that were unknown at compile time: plugin systems, serializers, ORMs, and dependency injection containers all rely on it. But if you know the type at compile time, a direct call is simpler, faster, and type-safe.
For many cases, alternatives exist:
java.util.functionand method references cover the common case of passing behavior around.- Generics and interfaces cover polymorphism without reflection.
- Build-time annotation processing, via
javax.annotation.processing, generates code that reads annotations without runtime reflection. MethodHandlesandVarHandlefromjava.lang.invokeoffer lower-level, often faster access to members while still being dynamic.
A common mistake is using reflection to implement something that a simple interface would solve. If the set of classes you want to support is fixed and known at compile time, an interface with a registry is clearer and easier to debug. Reflection adds indirection, hides errors until runtime, and makes the call stack harder to follow. Reserve it for genuinely dynamic cases where the class is not known until the application runs.