CodeGym /Courses /JAVA 25 SELF /Security, limitations, and alternatives to reflection

Security, limitations, and alternatives to reflection

JAVA 25 SELF
Level 62 , Lesson 2
Available

1. Security: what makes reflection dangerous?

Reflection is like a lockpick for your program: it lets you get into places ordinary code should not reach. For example, with reflection you can read and modify private fields, call private methods, and even change the values of final fields (yes, even such tricks are possible, though not always without consequences).

Example: breaking encapsulation


import java.lang.reflect.Field;

public class Secret {
    private String secret = "There's a secret here!";

    public String getSecret() {
        return secret;
    }
}

public class ReflectionDemo {
    public static void main(String[] args) throws Exception {
        Secret s = new Secret();
        Field field = Secret.class.getDeclaredField("secret");
        field.setAccessible(true); // Opening the "door"
        field.set(s, "Hacked!");
        System.out.println(s.getSecret()); // Hacked!
    }
}

In normal circumstances a private field is protected, but reflection with setAccessible(true) breaks that protection. It is a superpower — and at the same time a huge responsibility.

SecurityManager and restrictions

Java used to have a SecurityManager mechanism that allowed restricting the use of reflection (for example, in applets or on a server). But in Java 17 SecurityManager was marked as deprecated for removal, and in Java 21 it was fully removed from the platform.

In modern JVMs, security is handled differently: via the module system (Java 9+) and strict access restrictions to internal classes.

Vulnerability example: mutating final fields

import java.lang.reflect.Field;

public class FinalDemo {
    private final int number = 42;

    public static void main(String[] args) throws Exception {
        FinalDemo obj = new FinalDemo();
        Field f = FinalDemo.class.getDeclaredField("number");
        f.setAccessible(true);
        f.set(obj, 99);
        System.out.println(obj.number); // 42 (!)
        System.out.println(f.get(obj)); // 99
    }
}

The value of the number field does not always change “as expected” — the compiler and the JVM can optimize operations on final fields, and the result can be... surprising! This proves once again that reflection is not a magic wand but more like a crowbar that sometimes works — and sometimes does not.

2. Limitations of reflection

Performance loss

Invoking methods and accessing fields via reflection is slower than ordinary calls. The JVM cannot optimize such calls as well as a direct method call or a field access. If you invoke a method via reflection inside a large loop or on a hot path — expect slowdowns.

public class PerfDemo {
    public void sayHello() {}

    public static void main(String[] args) throws Exception {
        PerfDemo obj = new PerfDemo();
        long start = System.nanoTime();
        for (int i = 0; i < 1_000_000; i++) {
            obj.sayHello();
        }
        long direct = System.nanoTime() - start;

        var method = PerfDemo.class.getMethod("sayHello");
        start = System.nanoTime();
        for (int i = 0; i < 1_000_000; i++) {
            method.invoke(obj);
        }
        long reflect = System.nanoTime() - start;

        System.out.printf("Direct call: %d µs\n", direct / 1000);
        System.out.printf("Via reflection: %d µs\n", reflect / 1000);
    }
}

Result: reflection is usually 10–100 times slower!

Loss of type safety

Reflection operates on objects of type Object and requires manual casting. Errors (for example, a wrong argument type) will show up only at runtime rather than at compile time. This increases the risk of “surprises” and bugs that are hard to find.

Exceptions and checked exceptions

Reflection loves to throw exceptions: NoSuchFieldException, IllegalAccessException, InvocationTargetException, and others. You have to catch them; otherwise, the program will just crash.

Module system limitations

With the introduction of modules in Java (the module system), access to internal classes and private members became restricted. If you try to access a private field of a class from another module, you will get an InaccessibleObjectException.

Example

// In a modular application:
Field f = SomeClass.class.getDeclaredField("secret");
f.setAccessible(true); // java.lang.reflect.InaccessibleObjectException!

To allow such access, you must explicitly open the package (for example, via JVM options: --add-opens), which is not always possible or safe.

3. Modern alternatives to reflection

Reflection is a tool to use only when you really cannot do without it. Fortunately, the Java language and its ecosystem evolve, bringing new features that let you avoid reflection in most cases.

Pattern Matching (Java 16+)

Pattern matching lets you elegantly check and extract values from objects without “poking around” in their internals via reflection.

// Pattern matching example for instanceof (Java 16+)
if (obj instanceof String s) {
    System.out.println("This is a string of length: " + s.length());
}

Sealed classes (Java 17+)

Sealed classes let you explicitly limit an inheritance hierarchy, which makes code analysis easier and reduces the need to “guess” structure via reflection.

public sealed class Shape permits Circle, Rectangle {}
public final class Circle extends Shape {}
public final class Rectangle extends Shape {}

Record classes (Java 16+)

record classes automatically generate constructors, getters, equals, hashCode, and toString. Thanks to this, serialization and comparison of objects become simpler and safer — reflection is often unnecessary.

public record Point(int x, int y) {}

Annotation Processing (APT)

Instead of analyzing annotations at runtime via reflection, you can use annotation processors at compile time (@SupportedAnnotationTypes, etc.) to generate the required code. This is faster and safer.

Using interfaces, factories, and DI

In many cases where reflection used to be employed to create objects by class name, it is much better to use interfaces, factories, or dependency injection containers (for example, Spring). This lets you build flexible and extensible systems without having to “break into” classes.

4. Best practices: how to use reflection without regrets

  • Use reflection only where it is truly indispensable. For example, when writing libraries, frameworks, plugins, or testing tools.
  • Minimize the scope of use. Do not make all fields and methods accessible via setAccessible(true) “just in case”.
  • Document your use of reflection. Anyone maintaining your code should know where and why you use this tool.
  • Handle all checked exceptions. Do not ignore them — otherwise bugs will surface at the worst possible moment.
  • Be careful with final fields, private and inner classes. Mutating them via reflection can lead to unstable application behavior.
  • Account for module system restrictions. If your application runs in a modular environment (Java 9+), plan access to internal class members in advance.
  • Do not use reflection for everyday tasks. In most cases you can rely on standard language features: interfaces, factories, design patterns.

5. Practice: accessing a private field in a modular application

Let’s try to access a private field of another class via reflection in a modular application and see what happens.

Code example

// module-info.java
module my.app {}

// SomeClass.java
package my.app;

public class SomeClass {
    private String secret = "Modular secret";
}

// Main.java
package my.app;

import java.lang.reflect.Field;

public class Main {
    public static void main(String[] args) throws Exception {
        SomeClass obj = new SomeClass();
        Field field = SomeClass.class.getDeclaredField("secret");
        field.setAccessible(true); // java.lang.reflect.InaccessibleObjectException!
        System.out.println(field.get(obj));
    }
}

What will happen?

On Java 17+ (and above) you will get an exception:

Exception in thread "main" java.lang.reflect.InaccessibleObjectException:
Unable to make field private java.lang.String my.app.SomeClass.secret accessible:
module my.app does not "opens my.app" to unnamed module

How to fix it?

Open the package for reflection explicitly (for example, via JVM options):

--add-opens my.app/my.app=ALL-UNNAMED

Or (better!) do not use reflection where you can do without it.

6. Typical mistakes and hazards when working with reflection

Mistake No. 1: Unjustified use of setAccessible(true).
Opening access to private fields is like breaking into your own apartment to get the keys from the refrigerator. Do it only if you really must and you understand the consequences.

Mistake No. 2: Ignoring checked exceptions.
Reflection loves to throw exceptions. If you do not handle them, the application may crash unexpectedly. Even if “it works on my machine” — it does not mean it will work for all users.

Mistake No. 3: Expecting reflection to behave the same everywhere.
The module system, JVM restrictions, different Java versions, and launch parameters can suddenly “break” your reflective code.

Mistake No. 4: Using reflection for routine tasks.
If interfaces, factories, or DI will do — do not use reflection. It increases complexity and reduces performance.

Mistake No. 5: Mutating final fields via reflection.
This can lead to unexpected and hard-to-reproduce bugs related to compiler and JVM optimizations.

1
Task
JAVA 25 SELF, level 62, lesson 2
Locked
Attempt to peek into a locked diary 🔒
Attempt to peek into a locked diary 🔒
1
Task
JAVA 25 SELF, level 62, lesson 2
Locked
Magic of Changing Hidden Numbers 🎩
Magic of Changing Hidden Numbers 🎩
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION