When working with reflection in Java, you are not limited to accessing only public members. Reflection gives you the power to inspect and manipulate private fields, private methods and arrays at runtime — things that are normally hidden or restricted by the language.
In this section, we'll cover:
Accessing private fields and understanding why fields are usually private and final.
Working with arrays dynamically, even when you don't know their types at compile time.
Invoking methods reflectively using Method.invoke(), including private methods.
Finally, I’ll briefly talk about a small project idea — a validation framework built with reflection — to show a practical, real-world use case of these concepts.
Field are usually private , and preferably final
- The purpose of private field is for encapsulation
- Makes it easier to reason about who is reading / writing them
- The purpose of final fields is safety
- cannot be changed accidently
- state is exactly what we wanted when we constructed the object
- concurreny is way easier
Deep Reflection
Up to now, we have only used "shallow reflection"
Shallow reflective access is access at runtime via the core Reflection API (java.lang.reflect
) without using setAccessible.
only public elements in exported packages can be accessed.
Deep reflective access is access at runtime via the Core Reflection API, using the setAccessible()
method to break into non-public elements.
This allows access to any element—whether public or not, in any package, whether exported or not.
Note: Deep reflective access implies shallow reflective access.
📊 Table
Type of Reflective Access | Description |
---|---|
Shallow Reflective Access | Only uses public members, never violating encapsulation |
Deep Reflective Access | Uses setAccessible(true) to access non-public members, violating encapsulation |
When and Why Deep Reflection Is Used
1.broken encapsulation mainly for accessing fields
2.get hold of sun.misc.Unsafe
3.Serialization and similar mechanisms
Deep Reflection on Field
This is most common use case for deep reflection. field are usually private and preferably final.
The purpose of private fields is for encapsulation.
the purpose of final is safety
we have a Person class
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
create instance from Person and call it employee
public class PersonUseCase {
public static void main(String[] args) throws ReflectiveOperationException {
Person employee = new Person("jon" , 21);
}
}
after that, i want to change the age so use a setAccessible(true) for age
public class PersonUseCase {
public static void main(String[] args) throws ReflectiveOperationException {
Person employee = new Person("john" , 21);
Field fieldAge = Person.class.getDeclaredField("age");
fieldAge.setAccessible(true);
fieldAge.set(employee , 39);
}
}
Result
person: Person{name='john', age=39}
Changing final field Isn't that dangerous?
❌ Yes, it is.
Final fields are assumed to be constant and may be inlined by the compiler or JVM. Therefore, we should never modify a final field after it has been published.
Static final fields, in particular, are considered published as soon as the class is loaded. While it is technically possible to change a static final field using sun.misc.Unsafe
, doing so is highly discouraged and considered unsafe.
java.lang.invoke()
Reflection Suffers from “Amnesia”
- Reflection checks permissions every time you invoke a method. This adds overhead and can reduce performance.
Reflection Wraps All Exceptions
- Every reflective call wraps thrown exceptions, requiring an additional stack trace to be generated each time. This makes debugging harder and adds runtime cost.
🔍 MethodHandles.lookup()
are used to invoke methods and constructors in a more performant and flexible way compared to traditional reflection.
Before using MethodHandles, we need to identify ourselves
,this is similar to presenting an ID card.
Calling MethodHandles.lookup()
effectively declares who we are in terms of access control.
This method establishes the caller’s identity, allowing finer-grained access control.
The returned MethodHandles.Lookup object is scoped with access permissions based on the calling class.
The Lookup
object is used to find and access class members (methods, fields, constructors, etc.) using a variety of specialized methods:
findClass(String name)
– finds a nested class.findConstructor(Class, MethodType)
– finds a constructor.findGetter(Class, String, Class)
/ findStaticGetter(...) – reads fields.findSetter(Class, String, Class)
/ findStaticSetter(...) – writes to fields (not applicable to final fields).findVarHandle(Class, String, Class)
/ findStaticVarHandle(...) – returns a VarHandle to access fields.findSpecial(Class, String, MethodType, Class)
– used for special method invocations, like calling a super method.findStatic(Class, String, MethodType)
– finds a static method.findVirtual(Class, String, MethodType)
– finds a non-static (instance) method.
🛠️ Practical Example: MethodHandles.lookup() in Action
public class LookupDemo {
public static void main(String[] args) throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
//Usually we cannot, know our class in a static
//Context, since getClass() is not available
Class<?> whoAmI = lookup.lookupClass();
System.out.println("whoAmI: " + whoAmI);
MethodHandle constructorWithParameter = lookup.findConstructor(Person.class, MethodType.methodType(void.class, String.class, String.class, int.class));
Person person = (Person) constructorWithParameter.invoke("ted","doe",24);
MethodHandle setLastNameMethod = lookup.findVirtual(Person.class, "setLastName", MethodType.methodType(void.class, String.class));
MethodHandle constructor = lookup.findConstructor(Person.class, MethodType.methodType(void.class));
/**
* WrongMethodTypeException: expected ()person but found ()Object
* Object Person1 = constructor.invokeExact();
* setLastNameMethod.invokeExact( Person1,"ted");
*/
//work because i force constructor is Person
Person person1 = (Person) constructor.invokeExact();
setLastNameMethod.invokeExact( person1,"ted");
System.out.println(person1);
/**
* other solution is using invoke
* so return type doesn't matter
*/
Person person2 = (Person) constructor.invoke();
setLastNameMethod.invoke( person2,"mozby");
System.out.println(person2);
System.out.println(setLastNameMethod.toString());
}
}
🚀MethodHandles
are used to invoke methods and constructors in a more performant and flexible way compared to traditional reflection.
MethodHandle is similar to java.lang.reflect.Method
, but with key differences that improve performance and flexibility.
✅ No Boxing of Parameters (with @PolymorphicSignature)
- MethodHandle avoids parameter boxing thanks to @PolymorphicSignature, enabling more efficient method calls, especially for primitives.
✅ Direct Exception Throwing
- All invoke() methods throw Throwable directly — exceptions are not wrapped like in reflection.
- This means we must catch Throwable explicitly when using invoke.
✅ No Access Checks at Invocation Time
- Security/access checks are not repeated during method calls.
- Access control is enforced once when the MethodHandles.Lookup object is created.
✅ Return Type Matching
MethodHandle ensures that the return type matches the expected type.
This adds an additional layer of type safety, helping avoid accidental calls to the wrong method.
Top comments (1)
Great