Java Reflection: A Beginner’s Guide to the API That Powers Everything
You’ve used Spring, Hibernate, and JUnit for years. This is the mechanism that makes all of them work — and it’s simpler than you think.
Java Reflection: A Beginner’s Guide to the API That Powers Everything
You’ve used Spring, Hibernate, and JUnit for years. This is the mechanism that makes all of them work — and it’s simpler than you think.

Image generated by Gemini
If you’ve ever written @Autowired, @Entity, or @Test, you've benefited from Java Reflection without knowing it. These annotations don't do anything on their own — a framework reads them at runtime using the Reflection API and acts accordingly.
This article explains what reflection is, how it works step by step, and when you should (and should not) use it yourself.
The core idea
At its simplest, reflection lets a Java program inspect and manipulate itself while it’s running.
Normally, your code is fixed at compile time. You call methods by name, access fields directly, and the compiler checks everything. Reflection breaks that contract — it lets you ask a class “what fields do you have?”, “what methods are you hiding?”, and then act on those answers dynamically, even bypassing private and final access modifiers.
The entry point is always the Class object. Every loaded class in the JVM has one, and you retrieve it in one of three ways:
// From an existing instance
Class<?> c1 = someObject.getClass();
// From the class literal (preferred when you know the type)
Class<?> c2 = Dog.class;
// From a fully qualified name (used by frameworks at runtime)
Class<?> c3 = Class.forName("com.example.Dog");
Once you have the Class object, the entire Reflection API opens up.
A concrete example: the Dog class
Throughout this guide we’ll work with a single example class that covers the common cases you’ll encounter:
public class Dog {
private final String name; // private AND final
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public void bark() { System.out.println(name + " says woof"); }
private void growl() { System.out.println(name + " growls"); }
public static void breathe() { System.out.println("breathing..."); }
private static void sleep() { System.out.println("sleeping..."); }
}
This class has private fields, a final field, public and private instance methods, and public and private static methods. It covers every scenario the Reflection API can handle.
Step 1 — Inspecting fields
getDeclaredFields() returns every field declared in the class, regardless of its access modifier. This is different from getFields(), which returns only public fields including those inherited from superclasses.
Dog dog = new Dog("Buddy", 3);
Field[] fields = dog.getClass().getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName() + " : " + field.getType().getSimpleName());
}
// Output:
// name : String
// age : int
At this point we can see the fields, but we can’t read or write them yet. Attempting to call field.get(dog) on a private field throws IllegalAccessException.
Step 2 — Reading and modifying private fields
To access a private field, you must call **setAccessible(true)** first. This instructs the JVM to skip its normal access checks for that specific Field object.
Field nameField = dog.getClass().getDeclaredField("name");
nameField.setAccessible(true);
// Read the current value
String currentName = (String) nameField.get(dog);
System.out.println(currentName); // Buddy
// Write a new value — even though the field is final
nameField.set(dog, "John John");
System.out.println(dog.getName()); // John John
A few precise points worth noting:
getDeclaredField(String name)takes the exact field name as a string. Rename the field and this line throwsNoSuchFieldExceptionat runtime — no compile-time warning.- Modifying a
finalfield via reflection is technically possible but produces undefined behaviour in some JVM implementations. Avoid it in production code. setAccessible(true)is per-Fieldinstance, not global. Other code holding a separateFieldreference to the same field is unaffected.
Step 3 — Invoking methods
getDeclaredMethods() returns every method declared in the class. To call one, use method.invoke(target, args...).
Public instance method
Method bark = dog.getClass().getDeclaredMethod("bark");
bark.invoke(dog); // Buddy says woof
No setAccessible needed for public methods.
Private instance method
Method growl = dog.getClass().getDeclaredMethod("growl");
growl.setAccessible(true);
growl.invoke(dog); // Buddy growls
Static method
Static methods belong to the class, not an instance. Pass null as the target object:
Method breathe = Dog.class.getDeclaredMethod("breathe");
breathe.invoke(null); // breathing...
Method sleep = Dog.class.getDeclaredMethod("sleep");
sleep.setAccessible(true);
sleep.invoke(null); // sleeping...
Step 4 — Handling exceptions
Every reflective operation declares checked exceptions. You must handle them explicitly — the compiler enforces this.
ExceptionWhen it occursNoSuchFieldExceptionThe field name passed to getDeclaredField() doesn't existNoSuchMethodExceptionThe method name or parameter types don't matchIllegalAccessExceptionYou called get, set, or invoke without calling setAccessible(true) firstInvocationTargetExceptionThe method was invoked successfully but threw an exception itself
A typical pattern wraps everything in a try-catch and preserves the original cause:
try {
Field f = dog.getClass().getDeclaredField("name");
f.setAccessible(true);
f.set(dog, "Josh John");
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("Reflection failed", e);
}
Why frameworks rely on this
Consider what Spring does when it starts up. It doesn’t have a list of your beans compiled in — it discovers them at runtime by:
- Scanning the classpath for classes annotated with
@Component,@Service,@Repository, etc. - Using
Class.forName()to load each one. - Calling
getDeclaredFields()andgetDeclaredMethods()to find injection points marked with@Autowired. - Using
setAccessible(true)andfield.set(instance, dependency)to inject the dependency — even into private fields.
Every step in that process uses the exact API you’ve just learned. The annotation syntax is just a marker; reflection is the engine that reads the marker and acts on it.
The same applies to:
- Hibernate — maps private fields to database columns without requiring getters.
- JUnit — discovers and invokes test methods annotated with
@Test. - Jackson — serialises and deserialises private fields to and from JSON.
What you should not do
Reflection is a sharp tool. These are the precise failure modes to watch for:
Silent breakage on rename. If you rename growl to growlSoftly, the string "growl" in your reflective call becomes wrong. No compile error. A NoSuchMethodException surfaces at runtime — possibly in production.
Performance cost. Reflective method calls bypass JIT optimisations and involve additional object allocations. In a tight loop this is measurable. Cache Field and Method objects rather than looking them up repeatedly if performance matters.
Encapsulation erosion. private is a contract: "this is an implementation detail, do not depend on it." Reflection breaks that contract. If the class owner changes the internal structure, your reflective code fails — and they had no way of knowing you were depending on it.
GraalVM native image. If you compile to a native binary with GraalVM, the reflective metadata is stripped at build time unless you explicitly register the classes. Frameworks like Quarkus solve this by processing annotations at build time instead.
When reflection is the right choice
Use reflection when you are:
- Writing a framework or library that must work with user-defined classes it cannot reference at compile time.
- Writing test utilities that need to reach private state to set up a specific scenario.
- Building serialisation tools that need field-level access without requiring the class to implement an interface.
Do not use reflection in ordinary application logic. If you find yourself reaching for it there, the API you’re working against probably needs to be redesigned.
Summary
- Class object: Obtain it via
instance.getClass(),ClassName.class, orClass.forName("...") - Field access: Use
getDeclaredFields()to list fields,setAccessible(true)to bypassprivate, thenget()/set()to read/write values - Method invocation: Use
getDeclaredMethod()to look up a method by name, theninvoke(target, args...)to call it (passnullfor static methods) - Exception handling: Handle
NoSuchFieldException,NoSuchMethodException,IllegalAccessException, andInvocationTargetException
Reflection is not magic — it’s a well-defined API that exposes the JVM’s internal model of your classes. Understanding it transforms the “how does Spring actually work?” question from a mystery into a straightforward answer. And that understanding makes you a sharper Java developer.
Futher Readings
📣 Call to Action
If you are interested in following along with my journey, I invite you to dive into all the details provided below:
Thanks for reading
- 👏 Please clap for the story (50 claps) to help the article to be spread
- 🌐 Share the story on Social Media
- ➕More stories about Programming, Career, AI and more.
- 🔔 Follow me: Medium | LinkedIn | Twitter
- ✉️ Subscribe to the newsletter
메타데이터
- post_id
- 65160866ea05
- slug
- java-reflection-a-beginners-guide-to-the-api-that-powers-everything-65160866ea05
- url
- https://itnext.io/java-reflection-a-beginners-guide-to-the-api-that-powers-everything-65160866ea05
- canonical_url
- https://itnext.io/java-reflection-a-beginners-guide-to-the-api-that-powers-everything-65160866ea05
- author_url
- https://medium.com/@wagnerjfr
- status
- ok
- fetched_at
- 2026-06-10 08:17:25