1. Serialization via reflection
You already know that serialization is the process of converting an object into a stream of bytes or a textual representation. In Java there is standard serialization (Serializable), but more often JSON/XML formats are used via libraries like Jackson and Gson.
Why is reflection needed here?
To serialize an object, you need to discover its fields and their values. Fields can be private, and ordinary code cannot reach them — but reflection can. Therefore, JSON libraries dynamically traverse the object's structure and read/write fields via Field and Method.
Example: simple serialization of an object to a string
Data class:
public class Person {
private String name;
private int age;
private boolean active;
public Person(String name, int age, boolean active) {
this.name = name;
this.age = age;
this.active = active;
}
}
The simplest reflection-based serializer:
import java.lang.reflect.Field;
public class SimpleSerializer {
public static String serialize(Object obj) {
StringBuilder sb = new StringBuilder();
Class<?> clazz = obj.getClass();
sb.append(clazz.getSimpleName()).append("{");
Field[] fields = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
field.setAccessible(true); // Access private fields
try {
sb.append(field.getName()).append("=")
.append(field.get(obj));
} catch (IllegalAccessException e) {
sb.append(field.getName()).append("=<?>");
}
if (i < fields.length - 1) sb.append(", ");
}
sb.append("}");
return sb.toString();
}
}
Usage:
Person p = new Person("Alice", 30, true);
System.out.println(SimpleSerializer.serialize(p));
Person{name=Alice, age=30, active=true}
How does it work?
- Get the declared fields via getDeclaredFields().
- Make them accessible via setAccessible(true).
- Read field names and values and build a string.
Limitations of the example: no handling of nested objects, collections, arrays, and cyclic references — this is just a demonstration of the idea.
How do production-grade libraries do it?
Jackson/Gson can handle nested objects and collections, respect annotations (@JsonIgnore, @SerializedName), date formats, and much more — all built on top of reflection.
Example with Jackson:
import com.fasterxml.jackson.databind.ObjectMapper;
Person p = new Person("Bob", 25, false);
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(p);
// {"name":"Bob","age":25,"active":false}
2. Dependency Injection (DI) and reflection
Dependency Injection is a pattern where dependencies are injected from the outside rather than created inside the class. This makes code flexible, testable, and extensible. In Java this is done by frameworks like Spring, Guice, Dagger, marking injection points with annotations such as @Autowired, @Inject.
Why is reflection needed here?
A DI container needs to find fields/constructors, read annotations, and create instances at runtime — this is done via the Class/Constructor/Field API.
Example: a mini DI using reflection
Injection annotation:
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Inject {
}
Classes with a dependency:
public class Service {
public void doWork() {
System.out.println("Service is working!");
}
}
public class Client {
@Inject
private Service service;
public void useService() {
service.doWork();
}
}
Mini container:
import java.lang.reflect.*;
public class MiniDIContainer {
// Creates an object by class and injects dependencies into fields with @Inject
public static Object createObject(Class<?> clazz) throws Exception {
Object obj = clazz.getDeclaredConstructor().newInstance();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Inject.class)) {
Object dependency = createObject(field.getType()); // recursively
field.setAccessible(true);
field.set(obj, dependency);
}
}
return obj;
}
}
Usage:
public class Main {
public static void main(String[] args) throws Exception {
Client client = (Client) MiniDIContainer.createObject(Client.class);
client.useService(); // Service is working!
}
}
How does it work? The container finds fields annotated with @Inject, creates dependencies by their type, and uses reflection to set them into private fields.
Important: this is a simplified scheme. Real DI containers support scopes, singletons, configuration, proxies, handling of cyclic dependencies, and more.
3. Dynamic proxies
A proxy is a stand-in object that intercepts calls and adds behavior: logging, security, transactions, etc. In Java this is done by java.lang.reflect.Proxy together with InvocationHandler. It is the basis of many Spring AOP capabilities, mocks in Mockito, and more.
Example: a logging proxy
public interface HelloService {
void sayHello(String name);
}
public class HelloServiceImpl implements HelloService {
public void sayHello(String name) {
System.out.println("Hello, " + name + "!");
}
}
import java.lang.reflect.*;
public class LoggingProxy {
@SuppressWarnings("unchecked")
public static <T> T createProxy(T target, Class<T> iface) {
return (T) Proxy.newProxyInstance(
iface.getClassLoader(),
new Class<?>[]{iface},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Method call: " + method.getName());
return method.invoke(target, args);
}
}
);
}
}
Usage:
HelloService original = new HelloServiceImpl();
HelloService proxy = LoggingProxy.createProxy(original, HelloService.class);
proxy.sayHello("World");
Method call: sayHello
Hello, World!
How does it work? Proxy.newProxyInstance creates an object that implements the interface, and all method calls go to InvocationHandler.invoke, where you can run any cross-cutting code before/after delegating.
4. Real-world scenarios and limitations
Where is reflection used in practice?
- JUnit — finds methods with @Test and invokes them via reflection.
- Spring — creates beans, injects dependencies, scans annotations, generates proxies.
- Jackson/Gson — serialize/deserialize, reading even private fields.
- Hibernate — builds ORM models from class structure, manages fields and proxy objects.
- Mockito — creates mocks and intercepts calls via proxies.
Why shouldn't you always use reflection?
- Performance. Generally slower than regular calls (mitigated by caching/bytecode generation).
- Security. Breaks encapsulation; access to private data.
- Java 9+ modularity. You may get InaccessibleObjectException without explicitly opening packages/modules.
5. Common pitfalls
Pitfall #1: Ignoring checked exceptions. Reflection methods throw NoSuchFieldException, IllegalAccessException, InvocationTargetException, etc. Handle them or wrap them in your own exceptions.
Pitfall #2: setAccessible(true) does not always work. On Java 9+ in modular applications you may get InaccessibleObjectException. You need JVM/module parameters (--add-opens) or public APIs.
Pitfall #3: Cyclic dependencies in DI. With naive recursion (A depends on B, and B on A) you'll get StackOverflowError. Real containers track the dependency graph and resolve cycles with special techniques.
Pitfall #4: Incomplete object serialization. Reference fields serialize as ClassName@hash if there is no recursive traversal. Correct serialization requires handling nested objects/collections and protection against cycles.
Pitfall #5: Performance loss. Frequent reflective operations (in loops, on hot paths) become a bottleneck. Use caching of Field/Method, bytecode generation, MethodHandle or annotation processing.
Pitfall #6: Breaking encapsulation. Mutating private fields via reflection leads to fragility and hard-to-track bugs. Prefer public contracts.
GO TO FULL VERSION