Core Java ยท 9. More core APIs, lesson 4 of 6

Reflection: how frameworks read your classes

Advanced3 min read@since 16Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

Reflection lets code inspect and use classes at run time, without knowing them at compile time.

  • Get a Class object: obj.getClass(), Order.class or Class.forName("com.shop.Order").
  • Inspect it: getDeclaredFields(), getDeclaredMethods(), getDeclaredConstructors(), getAnnotation(), getSuperclass(), isRecord().
  • Use it: field.get/set, method.invoke, constructor.newInstance. Private members need setAccessible(true).

This is how frameworks work without you writing glue code: Spring creates and injects beans, JUnit finds @Test methods, Jackson maps JSON to fields, Hibernate reads @Entity classes, and validation reads @NotBlank.

In your own application code, prefer normal method calls: reflection is slower, skips compile-time checks, breaks with refactoring and can bypass encapsulation.

Diagram

Example

Java
@Retention(RetentionPolicy.RUNTIME)          // keep the annotation available at run time
@Target(ElementType.FIELD)
@interface NotBlank {}

record Signup(@NotBlank String name, @NotBlank String email, String referralCode) {}

static List<String> validate(Object target) throws IllegalAccessException {
    List<String> errors = new ArrayList<>();
    for (Field field : target.getClass().getDeclaredFields()) {
        if (field.isAnnotationPresent(NotBlank.class)) {
            field.setAccessible(true);                    // fields of a record are private
            Object value = field.get(target);
            if (value == null || value.toString().isBlank()) {
                errors.add(field.getName() + " must not be blank");
            }
        }
    }
    return errors;
}

System.out.println(validate(new Signup("Asha", " ", null)));   // [email must not be blank]
Calling a method by name, and creating an object without new
Class<?> type = Class.forName("java.util.ArrayList");
Object list = type.getDeclaredConstructor().newInstance();       // like new ArrayList<>()
Method add = type.getMethod("add", Object.class);
add.invoke(list, "hello");
System.out.println(list);                                        // [hello]

Common mistake

Using reflection to reach private internals of libraries. It breaks on upgrades and is blocked for JDK classes by strong encapsulation.

Under the hood

For annotations to be visible to reflection they need @Retention(RUNTIME); the default (CLASS) keeps them in the .class file but not at run time. Since Java 16 the JDK's internals are strongly encapsulated, so setAccessible(true) on JDK private members throws InaccessibleObjectException unless the module is explicitly opened. MethodHandles (java.lang.invoke) are a faster, safer alternative for dynamic calls, and frameworks increasingly generate code at build time instead of reflecting at startup (for faster startup and native images).

Check yourself

Which call reads a private field's value via reflection?

How this connects

Where this leads

You've reached the end of this thread. Try a learning path for what's next.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.