DEV Community

Mustafa Bingül
Mustafa Bingül

Posted on

Java Reflection API: Inspect & Manipulate Code at Runtime

Java Reflection API: Inspect & Manipulate Code at Runtime

A practical deep dive into one of Java's most powerful — and misunderstood — features

Tags: #java #reflection #jvm #tutorial


Table of Contents

  1. What is Reflection?
  2. Getting a Class Object
  3. Inspecting Fields, Methods & Constructors
  4. Invoking Methods Dynamically
  5. Accessing Private Members
  6. Annotations via Reflection
  7. Real-World Use Cases
  8. Performance & Pitfalls

1. What is Reflection?

Java Reflection is the ability of a program to inspect and modify its own structure and behaviour at runtime. Through the java.lang.reflect package, you can examine classes, interfaces, fields, methods, and constructors — even if you don't know their names at compile time.

Think of it as a mirror held up to your own code: you can look at what exists, read its metadata, and call it dynamically.

💡 When does it shine? Frameworks like Spring, Hibernate, and JUnit rely heavily on Reflection to wire dependencies, map entities, and discover test methods — all without you writing explicit calls.


2. Getting a Class Object

Every Reflection operation starts with a Class<?> object. There are three main ways to get one:

// 1. From the class literal (compile-time known type)
Class<String> c1 = String.class;

// 2. From an instance at runtime
String s = "hello";
Class<?> c2 = s.getClass();

// 3. By fully-qualified name (most dynamic)
Class<?> c3 = Class.forName("com.example.MyService");
Enter fullscreen mode Exit fullscreen mode

Class.forName() is the most powerful form — it lets you load classes whose names are only known at runtime (e.g. read from a config file or database).


3. Inspecting Fields, Methods & Constructors

Fields

Class<?> clazz = Person.class;

// Only public fields (includes inherited)
Field[] publicFields = clazz.getFields();

// All declared fields (private, protected, package) — no inherited
Field[] allFields = clazz.getDeclaredFields();

for (Field f : allFields) {
    System.out.println(f.getName() + " → " + f.getType().getSimpleName());
}
Enter fullscreen mode Exit fullscreen mode

Methods

Method[] methods = clazz.getDeclaredMethods();

for (Method m : methods) {
    System.out.println(
        m.getName()
        + " | params: " + m.getParameterCount()
        + " | returns: " + m.getReturnType().getSimpleName()
    );
}
Enter fullscreen mode Exit fullscreen mode

Constructors

// Instantiate with a specific constructor
Constructor<?> ctor = clazz.getDeclaredConstructor(String.class, int.class);
Object instance = ctor.newInstance("Alice", 30);
Enter fullscreen mode Exit fullscreen mode

4. Invoking Methods Dynamically

Once you have a Method object, call invoke() with the target instance and arguments. This is the core of dynamic dispatch.

public class Calculator {
    public int add(int a, int b) { return a + b; }
}

// --- Reflection call ---
Calculator calc = new Calculator();
Method addMethod = Calculator.class.getMethod("add", int.class, int.class);

Object result = addMethod.invoke(calc, 3, 7);
System.out.println(result); // 10
Enter fullscreen mode Exit fullscreen mode

💡 Tip: getMethod() searches public methods including inherited ones. Use getDeclaredMethod() to target a specific class's own methods regardless of visibility.


5. Accessing Private Members

Reflection can bypass Java's access control — a double-edged sword. You must call setAccessible(true) before reading or writing a private member.

public class Secret {
    private String password = "hunter2";
}

Secret obj = new Secret();
Field f = Secret.class.getDeclaredField("password");
f.setAccessible(true);

System.out.println(f.get(obj)); // hunter2
f.set(obj, "newpass");          // mutation!
Enter fullscreen mode Exit fullscreen mode

⚠️ Warning: In Java 16+ (strong encapsulation via the Module System), setAccessible() on JDK internal classes may throw InaccessibleObjectException. Always handle this with a try-catch and prefer proper APIs when available.


6. Annotations via Reflection

Reflection is the standard way to read custom annotations at runtime. This is the backbone of frameworks that use annotations as metadata (e.g. @Autowired, @Entity, @Test).

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log {
    String level() default "INFO";
}

public class Service {
    @Log(level = "DEBUG")
    public void processOrder() { /* ... */ }
}

// Reading annotation at runtime
Method m = Service.class.getMethod("processOrder");
if (m.isAnnotationPresent(Log.class)) {
    Log log = m.getAnnotation(Log.class);
    System.out.println("Log level: " + log.level()); // DEBUG
}
Enter fullscreen mode Exit fullscreen mode

7. Real-World Use Cases

Use Case Example How Reflection Helps
Dependency Injection Spring @Autowired Scans beans and injects dependencies without manual wiring
ORM Mapping Hibernate @Entity Maps Java fields to DB columns using annotations
Serialization Jackson ObjectMapper Reads fields/getters to convert objects to JSON automatically
Unit Testing JUnit @Test Discovers and invokes test methods dynamically
Plugin Systems Custom CLIs / IDEs Loads and runs classes from external JARs at runtime

8. Performance & Pitfalls

Performance cost

Reflective calls are significantly slower than direct calls — typically 10–100x for cold invocations. The JIT compiler cannot inline or optimize them as effectively. Always cache Method, Field, and Constructor objects rather than looking them up on every call.

// ❌ Slow: lookup inside a hot loop
for (Object o : items) {
    Method m = o.getClass().getMethod("process"); // repeated lookup!
    m.invoke(o);
}

// ✅ Fast: cache the Method once
Method m = MyClass.class.getMethod("process");
for (Object o : items) {
    m.invoke(o); // cached — much cheaper
}
Enter fullscreen mode Exit fullscreen mode

Key pitfalls to avoid

  • Breaking encapsulation — calling setAccessible(true) carelessly can violate the module system in Java 16+
  • Swallowed exceptionsinvoke() wraps target exceptions in InvocationTargetException; always unwrap and handle them
  • Overusing reflection — prefer type-safe, compile-time alternatives when the type is known
  • AOT incompatibility — reflection can hinder GraalVM Native Image compilation; register reflective access explicitly if needed

💡 Modern alternative: For high-performance dynamic dispatch, consider MethodHandle (Java 7+) or VarHandle (Java 9+). They are faster than classic Reflection and integrate better with the JIT.


Wrapping Up

Java Reflection is an incredibly powerful tool — it powers the frameworks you use every day. The key is to use it intentionally: reach for it when you genuinely need runtime flexibility, cache your reflective objects, and always handle the checked exceptions it throws. When performance is critical, MethodHandle is your friend.

Now go build something meta. Happy coding! 🚀


If you found this helpful, drop a ❤️ and feel free to share!

Top comments (0)