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 Objectpublic 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 constructorsclass 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.
- 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 interfaceinterface Subject { public String say(String name, int age);}// 2. Implement the real classclass RealSubject implements Subject { public String say(String name, int age) { return name + " " + age; }}// 3. Implement InvocationHandler and write proxy logicclass 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 classpublic 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:
- Bootstrap ClassLoader (implemented in C++, loads JDK core classes);
- Extension ClassLoader (loads extension classes under the JRE/lib/ext directory);
- 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 interfaceinterface Fruit { public abstract void eat();}// Specific product: Appleclass Apple implements Fruit { public void eat() { System.out.println("Eating apple"); }}// Specific product: Orangeclass Orange implements Fruit { public void eat() { System.out.println("Eating orange"); }}// Reflection factory class: no modification required, supports any Fruit subclassclass 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 classpublic 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
- The core of reflection is the
java.lang.Classclass, and all reflection operations need to obtain the Class object first; - Reflection supports dynamically operating classes/objects at runtime, and is the core technology of framework design, dynamic proxy, and generic erasure breakthrough;
- Reflection can break encapsulation, but it should be used with caution (damaging class encapsulation and slightly lower performance);
- 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”.