2616 words
13 minutes
Java Reflection Mechanism

Detailed Explanation of Java Reflection Mechanism#

The Java reflection mechanism is one of the core foundations of backend development. It supports dynamically obtaining class information, calling object methods/operating properties at runtime, and is the core underlying technology of framework design (Spring/MyBatis). This article comprehensively explains the use and application of the reflection mechanism from definitions, core functions, API usage to practical cases.

1. What is the Reflection Mechanism#

The reflection mechanism refers to the ability, in the runtime state, to know all the properties and methods of any class, and to call any method and property of any object. This function of dynamically obtaining information and dynamically calling object methods is called the reflection mechanism of the Java language.

Reflection breaks the compile-time binding limit of Java, allowing the program to determine the class/object to operate only at runtime, realizing dynamic programming. It is also the key to achieving decoupling and dynamic configuration in Java frameworks.

2. What Can the Reflection Mechanism Do#

The reflection mechanism provides Java with dynamic operation capabilities at runtime, and its core functions include the following 5 points:

  • Determine the class to which any object belongs at runtime;
  • Construct an object of any class at runtime;
  • Determine all member variables and methods of any class at runtime;
  • Call any method of any object at runtime;
  • Implement dynamic proxy based on reflection (core principle of AOP).

Reflection is a runtime operation that skips some compile-time checks and has slightly lower performance than direct calls. At the same time, reflection can access private members, which may damage the encapsulation of the class, so permission control should be done well when using it.

3. Relevant APIs of the Reflection Mechanism#

The core APIs of Java reflection are all located in the java.lang.reflect package, and java.lang.Class is the entry class of reflection (all reflection operations need to obtain the Class object first). The following are the practical usages of commonly used reflection APIs, and all codes are accompanied by line numbers and running result explanations.

3.1 Obtain the Complete Package Name and Class Name Through an Object#

The fully qualified name (package name + class name) of the class to which the object belongs can be obtained through Object.getClass().getName(), which is the most basic reflection operation.

package net.xsoftlab.baike;
public class TestReflect {
public static void main(String[] args) throws Exception {
TestReflect testReflect = new TestReflect();
System.out.println(testReflect.getClass().getName());
// Running result: net.xsoftlab.baike.TestReflect
}
}

3.2 Instantiate a Class Object#

Class is the core entry of reflection. There are 3 ways to obtain a Class object, among which Class.forName("fully qualified name") is the most commonly used method (supports dynamic class loading).

package net.xsoftlab.baike;
public class TestReflect {
public static void main(String[] args) throws Exception {
Class<?> class1 = null;
Class<?> class2 = null;
Class<?> class3 = null;
// Method 1: Class.forName(fully qualified name) → Recommended, dynamically load class
class1 = Class.forName("net.xsoftlab.baike.TestReflect");
// Method 2: object.getClass()
class2 = new TestReflect().getClass();
// Method 3: ClassName.class → Determined at compile time, static loading
class3 = TestReflect.class;
// The three methods obtain the same Class object
System.out.println("Class name: " + class1.getName());
System.out.println("Class name: " + class2.getName());
System.out.println("Class name: " + class3.getName());
}
}

3.3 Obtain the Parent Class and Implemented Interfaces of an Object#

The parent Class object is obtained through Class.getSuperclass(), and all implemented interfaces are obtained through Class.getInterfaces() (returns a Class array).

package net.xsoftlab.baike;
import java.io.Serializable;
// Implement the Serializable interface, the default parent class is Object
public class TestReflect implements Serializable {
private static final long serialVersionUID = -2862585049955236662L;
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("net.xsoftlab.baike.TestReflect");
// Get parent class
Class<?> parentClass = clazz.getSuperclass();
System.out.println("The parent class of clazz is: " + parentClass.getName());
// Running result: The parent class of clazz is: java.lang.Object
// Get all implemented interfaces
Class<?> intes[] = clazz.getInterfaces();
System.out.println("The interfaces implemented by clazz are:");
for (int i = 0; i < intes.length; i++) {
System.out.println((i + 1) + ": " + intes[i].getName());
}
// Running result: 1: java.io.Serializable
}
}

3.4 Instantiate Class Objects & Obtain Constructors Through Reflection#

Reflection supports instantiating objects through no-argument constructors and parameterized constructors. You need to first obtain all public constructors through Class.getConstructors(), and then create objects through Constructor.newInstance().

To access private constructors, you need to use Class.getDeclaredConstructors() and break the encapsulation through setAccessible(true).

package net.xsoftlab.baike;
import java.lang.reflect.Constructor;
public class TestReflect {
public static void main(String[] args) throws Exception {
Class<?> class1 = Class.forName("net.xsoftlab.baike.User");
// Method 1: Instantiate through no-argument constructor, then assign values using setter
User user = (User) class1.newInstance();
user.setAge(20);
user.setName("Rollen");
System.out.println(user);
// Running result: User [age=20, name=Rollen]
// Method 2: Get all constructors and instantiate through parameterized constructors
Constructor<?> cons[] = class1.getConstructors();
// Traverse and print the parameter types of all constructors
for (int i = 0; i < cons.length; i++) {
Class<?> clazzs[] = cons[i].getParameterTypes();
System.out.print("cons[" + i + "] (");
for (int j = 0; j < clazzs.length; j++) {
if (j == clazzs.length - 1)
System.out.print(clazzs[j].getName());
else
System.out.print(clazzs[j].getName() + ",");
}
System.out.println(")");
}
// Running result: cons[0] (java.lang.String) | cons[1] (int,java.lang.String) | cons[2] ()
// Instantiate through single-parameter constructor
user = (User) cons[0].newInstance("Rollen");
System.out.println(user); // User [age=0, name=Rollen]
// Instantiate through double-parameter constructor
user = (User) cons[1].newInstance(20, "Rollen");
System.out.println(user); // User [age=20, name=Rollen]
}
}
// Custom User class with multiple constructors
class User {
private int age;
private String name;
public User() {}
public User(String name) { this.name = name; }
public User(int age, String name) { this.age = age; this.name = name; }
// getter/setter & toString
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
@Override
public String toString() { return "User [age=" + age + ", name=" + name + "]"; }
}

3.5 Obtain All Properties of a Class#

Class.getDeclaredFields() is used to obtain all properties of the current class (including private ones), Class.getFields() is used to obtain only public properties (including public properties of parent classes/interfaces). Combined with java.lang.reflect.Modifier, the access modifiers (public/private/static, etc.) of the properties can be parsed.

package net.xsoftlab.baike;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
public class TestReflect implements Serializable {
private static final long serialVersionUID = -2862585049955236662L;
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("net.xsoftlab.baike.TestReflect");
System.out.println("===============Current Class Properties===============");
// Get all properties of the current class (including private ones)
Field[] field = clazz.getDeclaredFields();
for (int i = 0; i < field.length; i++) {
// Parse access modifiers
int mo = field[i].getModifiers();
String priv = Modifier.toString(mo);
// Get property type
Class<?> type = field[i].getType();
System.out.println(priv + " " + type.getName() + " " + field[i].getName() + ";");
}
System.out.println("==========Public Properties of Implemented Interfaces/Parent Class==========");
// Get only public properties (including parent classes/interfaces)
Field[] filed1 = clazz.getFields();
for (int j = 0; j < filed1.length; j++) {
int mo = filed1[j].getModifiers();
String priv = Modifier.toString(mo);
Class<?> type = filed1[j].getType();
System.out.println(priv + " " + type.getName() + " " + filed1[j].getName() + ";");
}
}
}

3.6 Obtain All Methods of a Class & Call Methods#

3.6.1 Obtain All Methods#

Class.getMethods() is used to obtain all public methods (including public methods of parent classes), and Class.getDeclaredMethods() is used to obtain all methods of the current class (including private ones). The return value, parameters, exceptions, modifiers and other information of the method can be parsed.

3.6.2 Call Methods#

Methods are dynamically called through Method.invoke(Object obj, Object... args). The first parameter is the instance object (pass null for static methods), and the subsequent parameters are the input parameters of the method.

package net.xsoftlab.baike;
import java.lang.reflect.Method;
public class TestReflect {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("net.xsoftlab.baike.TestReflect");
// Call the no-argument method reflect1
Method method = clazz.getMethod("reflect1");
method.invoke(clazz.newInstance());
// Running result: Java Reflection Mechanism - Calling a method of a class 1.
// Call the parameterized method reflect2 (int + String)
method = clazz.getMethod("reflect2", int.class, String.class);
method.invoke(clazz.newInstance(), 20, "Zhang San");
// Running result: Java Reflection Mechanism - Calling a method of a class 2. | age -> 20. name -> Zhang San
}
// No-argument test method
public void reflect1() {
System.out.println("Java Reflection Mechanism - Calling a method of a class 1.");
}
// Parameterized test method
public void reflect2(int age, String name) {
System.out.println("Java Reflection Mechanism - Calling a method of a class 2.");
System.out.println("age -> " + age + ". name -> " + name);
}
}

3.7 Operate Private Properties of a Class#

Reflection can break the encapsulation of a class and directly operate private properties. The core is to turn off access checks through Field.setAccessible(true), then assign values through Field.set(Object obj, Object value) and get values through Field.get(Object obj).

package net.xsoftlab.baike;
import java.lang.reflect.Field;
public class TestReflect {
// Private property
private String proprety = null;
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("net.xsoftlab.baike.TestReflect");
Object obj = clazz.newInstance();
// Get private property
Field field = clazz.getDeclaredField("proprety");
// Turn off access checks and break encapsulation
field.setAccessible(true);
// Assign value to private property
field.set(obj, "Java Reflection Mechanism");
// Get the value of the private property
System.out.println(field.get(obj));
// Running result: Java Reflection Mechanism
}
}

3.8 Dynamic Proxy of Reflection Mechanism#

Dynamic proxy is one of the core applications of reflection. Java’s native dynamic proxy is implemented based on java.lang.reflect.Proxy and java.lang.reflect.InvocationHandler, and only supports proxy for interfaces.

  1. Define the target interface; 2. Implement the real class of the target interface; 3. Implement InvocationHandler and rewrite the invoke method (proxy logic); 4. Create a proxy object through Proxy.newProxyInstance.
package net.xsoftlab.baike;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 1. Define the target interface
interface Subject {
public String say(String name, int age);
}
// 2. Implement the real class
class RealSubject implements Subject {
public String say(String name, int age) {
return name + " " + age;
}
}
// 3. Implement InvocationHandler and write proxy logic
class MyInvocationHandler implements InvocationHandler {
private Object obj = null;
// Bind the real object
public Object bind(Object obj) {
this.obj = obj;
// 4. Create proxy object: class loader + interface array + proxy handler
return Proxy.newProxyInstance(obj.getClass().getClassLoader(),
obj.getClass().getInterfaces(),
this);
}
// Rewrite invoke to execute the proxy method
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Execute the method of the real object
Object temp = method.invoke(this.obj, args);
return temp;
}
}
// Test class
public class TestReflect {
public static void main(String[] args) throws Exception {
MyInvocationHandler demo = new MyInvocationHandler();
Subject sub = (Subject) demo.bind(new RealSubject());
String info = sub.say("Rollen", 20);
System.out.println(info);
// Running result: Rollen 20
}
}

Extension: Explanation of Class Loaders#

There are three types of class loaders in Java, and dynamic proxy needs to specify the class loader of the real object:

  1. Bootstrap ClassLoader (implemented in C++, loads JDK core classes);
  2. Extension ClassLoader (loads extension classes under the JRE/lib/ext directory);
  3. AppClassLoader (loads classes under ClassPath, the default class loader).

4. Practical Examples of the Reflection Mechanism#

The reflection mechanism is widely used in actual development, especially in scenarios such as framework design, dynamic configuration, and generic breakthroughs. The following are 4 classic practical cases covering high-frequency scenarios in daily development.

4.1 Break Generic Restrictions: Store String in ArrayList of Integer#

Java’s generics are compile-time syntactic sugar and will be erased at runtime. Therefore, the type restriction of generics can be broken through reflection, and the add(Object) method of ArrayList can be directly called to add objects of any type.

package net.xsoftlab.baike;
import java.lang.reflect.Method;
import java.util.ArrayList;
public class TestReflect {
public static void main(String[] args) throws Exception {
// ArrayList with generic type Integer
ArrayList<Integer> list = new ArrayList<Integer>();
// Obtain the add method of ArrayList (parameter is Object) through reflection
Method method = list.getClass().getMethod("add", Object.class);
// Call the add method to add a String type object
method.invoke(list, "Java Reflection Mechanism Example.");
// Get and print the element
System.out.println(list.get(0));
// Running result: Java Reflection Mechanism Example.
}
}

4.2 Operate Arrays Through Reflection: Obtain/Modify Array Information & Dynamically Modify Array Size#

Java reflection provides the java.lang.reflect.Array class, which is specially used for operating arrays at runtime. It supports obtaining array type, length, modifying elements, and dynamically expanding/shrinking arrays.

4.2.1 Obtain and Modify Basic Information of Arrays#

package net.xsoftlab.baike;
import java.lang.reflect.Array;
public class TestReflect {
public static void main(String[] args) throws Exception {
int[] temp = { 1, 2, 3, 4, 5 };
// Get the component type of the array (array element type)
Class<?> demo = temp.getClass().getComponentType();
System.out.println("Array type: " + demo.getName()); // int
System.out.println("Array length: " + Array.getLength(temp)); // 5
System.out.println("The first element of the array: " + Array.get(temp, 0)); // 1
// Modify the element at the specified position of the array
Array.set(temp, 0, 100);
System.out.println("The first element of the array after modification: " + Array.get(temp, 0)); // 100
}
}

4.2.2 Dynamically Modify Array Size#

package net.xsoftlab.baike;
import java.lang.reflect.Array;
public class TestReflect {
public static void main(String[] args) throws Exception {
int[] temp = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// Expand the array to 15 bits
int[] newTemp = (int[]) arrayInc(temp, 15);
print(newTemp);
String[] atr = { "a", "b", "c" };
// Expand the string array to 8 bits
String[] str1 = (String[]) arrayInc(atr, 8);
print(str1);
}
// Universal array expansion method: supports arrays of any type
public static Object arrayInc(Object obj, int len) {
// Get array element type
Class<?> arr = obj.getClass().getComponentType();
// Create a new array of specified length
Object newArr = Array.newInstance(arr, len);
// Copy the content of the original array to the new array
int co = Array.getLength(obj);
System.arraycopy(obj, 0, newArr, 0, co);
return newArr;
}
// Universal array printing method
public static void print(Object obj) {
Class<?> c = obj.getClass();
if (!c.isArray()) {
return;
}
System.out.println("Array length: " + Array.getLength(obj));
for (int i = 0; i < Array.getLength(obj); i++) {
System.out.print(Array.get(obj, i) + " ");
}
System.out.println();
}
}

4.3 Optimize Factory Pattern with Reflection: Realize Non-intrusive Subclass Expansion#

The problem of the traditional simple factory pattern is that the factory class must be modified when adding subclasses, which violates the open-closed principle; the factory pattern implemented through reflection can dynamically add any subclass without modifying the factory class, only by passing the fully qualified name of the class.

package net.xsoftlab.baike;
// Product interface
interface Fruit {
public abstract void eat();
}
// Specific product: Apple
class Apple implements Fruit {
public void eat() {
System.out.println("Eating apple");
}
}
// Specific product: Orange
class Orange implements Fruit {
public void eat() {
System.out.println("Eating orange");
}
}
// Reflection factory class: no modification required, supports any Fruit subclass
class Factory {
public static Fruit getInstance(String className) {
Fruit f = null;
try {
// Dynamically instantiate objects through reflection
f = (Fruit) Class.forName(className).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return f;
}
}
// Test class
public class TestReflect {
public static void main(String[] args) throws Exception {
// Instantiate Apple: pass the fully qualified name
Fruit f = Factory.getInstance("net.xsoftlab.baike.Apple");
if (f != null) {
f.eat(); // Running result: Eating apple
}
// When adding a new Banana subclass, you only need to implement the Fruit interface without modifying the Factory
// Fruit f2 = Factory.getInstance("net.xsoftlab.baike.Banana");
}
}

In actual development, the fully qualified name of the class can be configured in the properties configuration file, and the class name can be dynamically obtained by reading the configuration file to achieve complete dynamic configuration. The core principle of Spring’s Bean factory is based on this.

Summary#

  1. The core of reflection is the java.lang.Class class, and all reflection operations need to obtain the Class object first;
  2. Reflection supports dynamically operating classes/objects at runtime, and is the core technology of framework design, dynamic proxy, and generic erasure breakthrough;
  3. Reflection can break encapsulation, but it should be used with caution (damaging class encapsulation and slightly lower performance);
  4. Classic applications of reflection: Spring IOC/DI, MyBatis mapping, dynamic proxy (AOP), factory pattern optimization.

Reflection is a necessary foundation for Java backend development. Understanding the principle and use of reflection can help you truly understand the underlying implementation of mainstream frameworks and achieve the leap from “using frameworks” to “understanding frameworks”.

Java Reflection Mechanism
https://fuwari.vercel.app/posts/learning/java-reflection-mechanism/
Author
Zero02
Published at
2026-03-30
License
CC BY-NC-SA 4.0