Reflection in Java

Reflection inspects and manipulates classes at runtime. It powers most frameworks and should be rare in application code.

What reflection is

Reflection lets a program examine and use classes, methods and fields that were not known at compile time. Everything the compiler normally does for you, finding a method and calling it, is done by name at runtime instead.

Getting a Class object

Class<?> fromLiteral = Note.class;
Class<?> fromInstance = note.getClass();
Class<?> fromName = Class.forName("com.example.notes.Note");   // also initialises it

System.out.println(fromName.getSimpleName());
System.out.println(fromName.getPackageName());
System.out.println(Modifier.isPublic(fromName.getModifiers()));

Inspecting members

Class<?> type = Note.class;

for (Field field : type.getDeclaredFields()) {          // this class, any visibility
    System.out.println(field.getType().getSimpleName() + " " + field.getName());
}

for (Method method : type.getMethods()) {               // public, including inherited
    System.out.println(method.getName() + " returns " + method.getReturnType());
}

for (Constructor<?> constructor : type.getDeclaredConstructors()) {
    System.out.println(Arrays.toString(constructor.getParameterTypes()));
}
MethodReturns
getFields()Public fields, including inherited
getDeclaredFields()All fields of this class only
getMethods()Public methods, including inherited
getDeclaredMethods()All methods of this class only

Creating and calling

Class<?> type = Class.forName("com.example.notes.Note");

Constructor<?> constructor = type.getDeclaredConstructor(String.class, int.class);
Object note = constructor.newInstance("Java reflection", 120);

Method method = type.getMethod("title");
String title = (String) method.invoke(note);

Field field = type.getDeclaredField("views");
field.setAccessible(true);                 // bypasses private, if permitted
field.set(note, 500);
setAccessible(true) is the part to treat with caution. It defeats encapsulation, can break when the class changes, and is restricted for platform classes by the module system. Under Java 17 and later, accessing internals of a module that has not opened them fails outright.

Reading annotations

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Route {
    String path();
}

public class NoteController {
    @Route(path = "/notes")
    public String list() { return "all notes"; }
}
for (Method method : NoteController.class.getDeclaredMethods()) {
    Route route = method.getAnnotation(Route.class);
    if (route != null) {
        System.out.println(route.path() + " -> " + method.getName());
    }
}

This is the entire mechanism behind annotation driven frameworks: scan the classes, read the annotations, build a routing table, and invoke reflectively when a request arrives.

A small worked example

public static Map<String, Object> toMap(Object value) throws IllegalAccessException {
    Map<String, Object> result = new LinkedHashMap<>();

    for (Field field : value.getClass().getDeclaredFields()) {
        if (Modifier.isStatic(field.getModifiers())) {
            continue;
        }
        field.setAccessible(true);
        result.put(field.getName(), field.get(value));
    }
    return result;
}

Roughly how a serialisation library begins. Note how much is left out: type conversion, nesting, cycles and null handling all have to be added before it is useful.

Generic type information

public class NoteRepository implements Repository<Note, Long> { }

Type type = NoteRepository.class.getGenericInterfaces()[0];
if (type instanceof ParameterizedType parameterized) {
    System.out.println(parameterized.getActualTypeArguments()[0]);   // Note
}

Type arguments used in a declaration survive erasure as metadata, so reflection can read them. The type of a value at runtime is still gone.

MethodHandles

MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType signature = MethodType.methodType(String.class);
MethodHandle handle = lookup.findVirtual(Note.class, "title", signature);

String title = (String) handle.invokeExact(note);

MethodHandle is a more modern and faster alternative introduced in Java 7. Access is checked once when the handle is created rather than on every call, and the JIT can optimise through it. Prefer it for repeated invocation.

The costs

ConcernDetail
PerformanceSlower than a direct call, although far better than it once was
Type safetyErrors move from compile time to runtime
RefactoringA rename breaks a string that no tool will update
EncapsulationsetAccessible reaches past private
ModulesAccess to unopened packages is denied
OptimisationThe JIT can inline reflective calls less effectively

When it is justified

  • Frameworks that must work with classes they have never seen.
  • Serialisation and mapping libraries.
  • Dependency injection containers.
  • Test tools that discover and run test methods.
  • Plugin systems loading code at runtime.

Inside a normal application, a reflective call is almost always a sign that an interface or a factory would be better.

// Reflection for something an interface handles better
Object handler = Class.forName(handlerName).getDeclaredConstructor().newInstance();
handler.getClass().getMethod("handle", Request.class).invoke(handler, request);

// Clearer, checked at compile time
Map<String, Handler> handlers = Map.of("note", new NoteHandler());
handlers.get(name).handle(request);

Common mistakes

  • Using getMethod when the method is not public, or getDeclaredMethod when it is inherited.
  • Passing the wrong parameter types and receiving NoSuchMethodException.
  • Forgetting that invoke wraps any thrown exception in InvocationTargetException; call getCause().
  • Calling setAccessible(true) on platform classes and failing under the module system.
  • Using reflection in a hot loop without caching the Method object.

Best practices

  • Prefer interfaces, factories and dependency injection.
  • Cache Method, Field and Constructor objects; the lookup is the expensive part.
  • Use MethodHandle for repeated invocation.
  • Unwrap InvocationTargetException before logging.
  • Keep reflective code in one small, well tested place.
  • Never use it to work around your own encapsulation.

Practice

  1. List every declared field and method of a class of your own.
  2. Create an instance and call a method entirely by name.
  3. Why does an exception thrown by an invoked method appear as InvocationTargetException?
  4. Write a tiny routing table from an annotation and reflection.
  5. Take a reflective call in your own code and replace it with an interface.

Conclusion

Reflection gives runtime access to structure that is normally a compile time concern. It is what makes frameworks possible and what makes application code fragile, so keep it at the framework boundary.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →
Java

Dynamic Proxies in Java

A dynamic proxy implements an interface at runtime and routes every call through one handler, which is how cross cutting behaviour is added.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.