Introduction to Java Reflection
Java reflection is a powerful feature that allows a program to inspect and manipulate classes, methods, and fields at runtime. This capability is particularly useful for frameworks, libraries, and tools that need to work with classes whose details are not known at compile time. The core of reflection lies in the java.lang.Class object, which acts as a blueprint for a class, providing access to its metadata and allowing dynamic instantiation and method invocation.
The Class Object
Every object in Java is an instance of a class, and the JVM maintains a unique Class object for each loaded class. This object contains all the information about the class's structure, including its fields, methods, constructors, and interfaces. The Class object is the entry point for all reflective operations.
Classis itself a class in thejava.langpackage.- Instances of
Classare created by the JVM, not by application code. - For a given class, only one
Classobject exists in a running JVM. - It represents the loaded bytecode of a single class file.
- Every object instance holds a reference to the
Classobject that created it. - Through a
Classobject, you can discover the complete structure of a loaded class. - It is the foundation of the entire Reflection API.
Core Reflection Classes
The reflecsion API is centered around a few key classes:
java.lang.Class: Represents a class or interface.java.lang.reflect.Constructor: Represents a class constructor.java.lang.reflect.Field: Represents a class field or attribute.java.lang.reflect.Method: Represents a class method.java.lang.reflect.Modifier: Provides static methods to decode class and member access modifiers.
Obtaining a Class Object
There are three primary ways to obtain a Class object:
- Using the
getClass()method on any object instance. - Using the
.classsyntax on a class name. - Using the static
Class.forName(String className)method.
// 1. Using the getClass() method
Product productInstance = new Product();
Class<?> productClass1 = productInstance.getClass();
System.out.println("Class name: " + productClass1.getName());
// 2. Using the .class syntax
Class<?> productClass2 = Product.class;
System.out.println("Classes are equal: " + (productClass1 == productClass2));
// 3. Using Class.forName() (most common for dynamic loading)
try {
Class<?> productClass3 = Class.forName("com.example.Product");
System.out.println("Classes are equal: " + (productClass2 == productClass3));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Note: A class will have only one Class object instance during the lifetime of a JVM.
Creating Class Instances
Once you have a Class object, you can create new instances of that class.
Using Class.newInstance()
This method is used to create an instance of a class using its no-argument constructor.
Class<?> stringClass = String.class;
Object newString = stringClass.newInstance(); // Deprecated in modern Java, but fundamental to reflection
Using Constructor.newInstance()
A more flexible approach is to get a specific Constructor object and then invoke it. This allows you to use constructors with arguments.
// Get the Class object for String
Class<?> stringClass = String.class;
// Retrieve the constructor that accepts a String argument
Constructor<?> constructor = stringClass.getConstructor(String.class);
// Create a new instance using the constructor
Object greeting = constructor.newInstance("Hello, World!");
Accessing and Invoking Constructors
The java.lang.reflect.Constructor class provides methods to access and use class constructors.
Retrieving Constructors
-
getConstructors(): Returns an array of all public constructors. -
getDeclaredConstructors(): Returns an array of all constructors (public, protected, and private). -
getConstructor(Class... parameterTypes): Returns a single public constructor matching the specified parameter types. -
getDeclaredConstructor(Class... parameterTypes): Returns a single constructor (of any visibility) matching the specified parameter types.
Invoking a Constructor
Once you have a Constructor object, you can create a new instance by calling its newInstance() method, passing the required arguments.
// 1. Load the Class object
Class<?> personClass = Class.forName("com.example.Person");
// 2. Get the desired constructor (e.g., a constructor with String and int parameters)
Constructor<?> personConstructor = personClass.getConstructor(String.class, int.class);
// 3. Invoke the constructor to create a new instance
Object person = personConstructor.newInstance("Alice", 30);
Java Class Loading Process
When a class is first used, the JVM initiates a three-step process to load and prepare it.
- Loading: The class loader reads the bytecode from the class file and creates a
Classobject in the method area. - Linking: This phase prepares the class for execution.
- Verification: Ensures the bytecode is valid and secure.
- Preparation: Allocates memory for class variables and initializes them to default values.
- Resolution: Replaces symbolic references in the constant pool with direct references.
- Initialization: The JVM executes the class's static initializers and initializes static variables. This is when the
clinitmethod is run.
Triggers for Class Initialization
Initialization occurs when a class is active referenced. Passive references generally do not trigger initialization.
Active References (Trigger Initialization)
- When the JVM starts, the class containing the
mainmethod is initialized. - Creating a new instance of a class (e.g.,
new MyClass()). - Accessing a static field or method (except for constants).
- Using reflection to access a class.
- Initializing a subclass, which triggers initialization of its superclass first.
Passive References (Do NOT Trigger Initialization)
- Accessing a static field on a class does not initialize the class if the field is a constant (e.g.,
public static final int VALUE = 10;). - Creating an array of a class type (e.g.,
MyClass[] array = new MyClass[10];). - Referencing a class's constant (as mentioned above).
ClassLoaders
The ClassLoader is responsible for locating and loading class files in to the JVM. Once a class is loaded, it is often cached by the class loader to improve performance. The standard Java class loaders maintain a cache of loaded classes, and these cached Class objects can be garbage collected when they are no longer reachable.