DEV Community

Milad sadeghi
Milad sadeghi

Posted on

Working with Private Members and Method Invocation in Java Reflection

Working with Private Members and Method Invocation in Java Reflection

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;
    }

}

Enter fullscreen mode Exit fullscreen mode

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);
 }
}

Enter fullscreen mode Exit fullscreen mode

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);
 }
}

Enter fullscreen mode Exit fullscreen mode

Result

person: Person{name='john', age=39}
Enter fullscreen mode Exit fullscreen mode

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());


    }
}


Enter fullscreen mode Exit fullscreen mode

🚀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)

Collapse
 
f_gh_086bef25c2df8660aa70 profile image
F gh

Great