Reflection in Java
Reflection inspects and manipulates classes at runtime. It powers most frameworks and should be rare in application code.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
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()));
}| Method | Returns |
|---|---|
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
| Concern | Detail |
|---|---|
| Performance | Slower than a direct call, although far better than it once was |
| Type safety | Errors move from compile time to runtime |
| Refactoring | A rename breaks a string that no tool will update |
| Encapsulation | setAccessible reaches past private |
| Modules | Access to unopened packages is denied |
| Optimisation | The 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
getMethodwhen the method is not public, orgetDeclaredMethodwhen it is inherited. - Passing the wrong parameter types and receiving
NoSuchMethodException. - Forgetting that
invokewraps any thrown exception inInvocationTargetException; callgetCause(). - Calling
setAccessible(true)on platform classes and failing under the module system. - Using reflection in a hot loop without caching the
Methodobject.
Best practices
- Prefer interfaces, factories and dependency injection.
- Cache
Method,FieldandConstructorobjects; the lookup is the expensive part. - Use
MethodHandlefor repeated invocation. - Unwrap
InvocationTargetExceptionbefore logging. - Keep reflective code in one small, well tested place.
- Never use it to work around your own encapsulation.
Practice
- List every declared field and method of a class of your own.
- Create an instance and call a method entirely by name.
- Why does an exception thrown by an invoked method appear as
InvocationTargetException? - Write a tiny routing table from an annotation and reflection.
- 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.