Articles
Java Reflection Interview Questions: Annotations, Access Boundaries, and Runtime Types
Practice Java reflection interview questions with annotation discovery, module access gates, generic metadata limits, and safe invocation error handling.
A plugin method appears in a debugger but never enters the registry. Adding setAccessible does not repair it: the annotation was not retained for runtime inspection. Another method is discovered correctly but cannot be opened across a module boundary. These failures look similar from outside the framework, yet they belong to different stages.
For Java reflection interview questions, separate discovery, annotation interpretation, access, and invocation. Finding a Method is not permission to invoke it. Reading a generic declaration does not reveal every type argument of an arbitrary object. A useful answer identifies the failed contract before proposing a workaround.
These original exercises use Java SE 25 documentation, not reported employer prompts.
Which methods did the scanner actually discover?
The first debugging step is to state the scanner's search domain. Class.getMethods returns public methods, including inherited ones. Class.getDeclaredMethods examines methods declared by that class, including nonpublic methods, but does not simply return every inherited declaration. The Class API documents these distinct queries.
In an original plugin exercise, BasePlugin declares a public start method while FilePlugin declares a private rebuild method. A public-only scanner can find inherited start and miss rebuild. A declared-only scanner can find rebuild and miss a start declaration that exists only on the superclass. Neither result means the JVM lost a method.
Do not use the first method whose name matches as your registration strategy. Overloads can share a name while having different parameter types. Bridges and synthetic methods can complicate framework scanning too. Define the required signature and decide whether generated methods belong in your registration policy.
Avoid relying on the order of the returned method array. If registration order matters, establish it through explicit metadata or a deterministic comparator. A test that passes because one local build happened to enumerate methods in a convenient order has not established a portable framework contract.
Why is a source annotation invisible at runtime?
An annotation has its own retention policy. RUNTIME retention makes it available for reflective reading; CLASS retention preserves it in class files without requiring runtime reflective availability. SOURCE annotations need not survive compilation. The default retention is CLASS when no Retention annotation specifies otherwise, as described by RetentionPolicy.
This original marker deliberately makes the runtime contract explicit:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Handler {
String value();
}
class Plugin {
@Handler("rebuild")
public void rebuild() { }
}
Imports from java.lang.annotation are omitted here. Removing the runtime retention is a negative test: the source still displays Handler, but a runtime scanner cannot infer that source-only intention. Changing accessibility cannot make missing annotation metadata reappear.
Next verify the annotated element. A declaration annotation on a method is not interchangeable with a type-use annotation inside its return type. If the scanner asks the wrong reflective object, an empty result can be correct. Log the declaring class, method signature, annotation query, and retention assumption instead of logging only “handler missing.”
What do inherited and repeatable annotations mean?
The AnnotatedElement contract distinguishes directly present, indirectly present, present, and associated annotations. Those terms determine what a query returns; they are not merely alternative names for the same array.
For a repeatable annotation, getAnnotationsByType can look through its container. A scanner that assumes getAnnotation always returns every repeated instance can silently register too little. Write a fixture containing two handler tags and inspect the by-type result rather than flattening an arbitrary annotation array yourself.
The Inherited annotation has a narrower effect than its name suggests. It concerns class annotation lookup through the superclass chain. It does not automatically make annotations on overridden methods appear on the overriding declaration, and interface annotations are not universally inherited that way.
For the plugin exercise, decide whether overriding rebuild replaces the superclass registration or inherits a framework-defined rule. If you want method-annotation inheritance, implement and test that traversal as your own policy. Do not attribute an invented framework behavior to Java's Inherited marker.
Why can a discovered member remain inaccessible?
Reflection normally enforces access controls when a member is used. Java language visibility and module boundaries both matter. An exported package and an opened package serve different access purposes; exporting a public API is not blanket permission for deep reflection into its private state.
The AccessibleObject documentation specifies when checks may be suppressed. Across named modules, a private member in a package that is not opened to the caller can remain inaccessible even though a scanner obtained its reflective description.
Use trySetAccessible when your framework supports an explicit denied-access branch. It can return false when access cannot be enabled, whereas setAccessible(true) can throw InaccessibleObjectException. Neither method is a universal bypass. The caller still needs an intentional supported access arrangement.
In an interview, ask who owns the module configuration. A legitimate integration might use a narrowly qualified opens declaration. A general-purpose library should not prescribe opening every package to every module as its default repair. Prefer public extension points when they provide the required capability without deep reflection.
How should the registry handle denied access?
Consider this original registration branch, assuming a discovered instance method and its intended plugin instance:
if (!method.canAccess(plugin) && !method.trySetAccessible()) {
throw new IllegalStateException(
"Handler access denied: " + method.toGenericString());
}
This is a policy sketch, not a full secure plugin loader. canAccess requires an appropriate receiver for instance members; static-member checks use null. Passing the wrong receiver can produce an argument error before the access policy is even evaluated.
A registry must decide whether denied access is a configuration failure, a skipped optional plugin, or a startup-blocking error. Quietly skipping a mandatory handler can defer a clear startup problem into a confusing production request failure. Record the exact decision with the plugin identity and stage.
Do not expose private configuration contents or credentials in diagnostic messages. A member signature often supplies enough context. Reflection makes it easy to print entire object graphs; useful debugging does not require unrestricted disclosure of plugin state.
Which runtime type information can you trust?
Class objects describe runtime classes. A declared generic field may retain signature metadata that reflection exposes through generic-type APIs. That is different from recovering the element type of every List instance encountered at runtime.
Suppose a class declares List<String> names. Inspecting that field's generic declaration can reveal the String argument. An arbitrary ArrayList object's runtime class does not by itself prove that its current contents are all strings. A declaration, an object's runtime class, and validated instance content answer different questions.
Build an original comparison fixture with a declared generic field, a raw List reference, and a mixed-content list. Ask what the scanner knows at each boundary. Avoid treating a successful cast or a type-token convention as evidence that erasure has disappeared.
Frameworks can carry additional type information explicitly, including specialized superclass signatures or registered serializers. Explain where that information came from. If callers supply a Type descriptor, your API now has a descriptor-validation problem as well as a reflective-discovery problem.
Class identity is another runtime boundary. Two classes can share a fully qualified name while belonging to different class loaders and not being the same runtime type. A plugin registry keyed only by a printed class name can therefore collapse distinct types or report confusing cast failures. Preserve the actual Class identity where that identity is the intended key.
For an isolated plugin-loader exercise, load two copies under separate loaders and compare their class identities before attempting assignment. That requires an actual loader fixture; the name alone cannot establish the result. It also explains why a globally retained cache of reflective members can unintentionally extend a plugin loader's lifetime after the application intended to unload it.
Where did an invocation failure originate?
The Method.invoke contract distinguishes invocation setup failures from an exception thrown by the invoked method. Target exceptions are wrapped in InvocationTargetException; inspect the cause when reporting the plugin's actual failure.
An argument mismatch is not the same thing as a plugin throwing its own validation error. Similarly, an access denial belongs before execution, not inside business logic. A registry that catches every ReflectiveOperationException and labels it “plugin unavailable” destroys information the caller needs.
For the original rebuild handler, test a valid invocation, an incompatible receiver, wrong argument shape, denied access, and a deliberate target exception. The expected outcome ledger should identify whether rebuild began executing. A counter increment inside the target makes that distinction observable.
Decide how to translate errors at the framework boundary. Keep the source exception attached, add the operation identity, and avoid reflecting arbitrary user-selected method names. Registration rules are part of the attack surface even when access checking works correctly.
Design the registration test matrix
A useful registry fixture combines public inherited handlers, private declared handlers, an unretained marker, repeated annotations, an overriding method, and one generic signature. Keep each negative case independent so one inaccessible member does not prevent the remaining expectations from being evaluated.
Run module-sensitive tests in a named-module arrangement, not only on a permissive classpath. The difference is the feature under test. Record the JDK, module declarations, scanner traversal, and exact query methods. A successful classpath run cannot establish that private access works across an unopened named-module boundary.
For deterministic diagnostics, collect registration candidates before invoking them. Validate duplicate handler names, signature compatibility, and access policy as separate steps. Then publish an immutable registry snapshot. This original design suggestion makes startup failures easier to reproduce without asserting that every framework must use the same architecture.
These examples are documentation-reviewed against Java SE 25. The local Java launcher has no usable JDK, so no compilation or module-runtime result is claimed. Preserve that distinction when adapting the fixtures to the interview's actual configuration.
Sources and Further Reading
- Oracle Java SE 25: Class
- Oracle Java SE 25: RetentionPolicy
- Oracle Java SE 25: AnnotatedElement
- Oracle Java SE 25: Inherited
- Oracle Java SE 25: AccessibleObject
- Oracle Java SE 25: Method
Research note: Primary documentation checked September 30, 2026. Original registration policies are design exercises, not claims about a particular framework or employer; module-sensitive behavior requires the recorded JDK and deployment setup.