Understanding Java Reflection: Class, ClassLoader, and Dynamic Operations
The Class Class
When an object looks into a mirror, it can see information about itself: field names, methods, constructors, and implemented interfaces. For every class, the JRE keeps an immutable Class object that contains all relevant information about that particular class.
- A Class object can only be created by the system
- There is only one Class instance for each class in the JVM
- Every class instance remembers which Class instance generated it
What is a Class?
Class is a class that encapsulates information about the class it represents.
public class ReflectionTest {
@Test
public void testClass() {
Class<?> classInfo = null;
}
}
// Class definition
public final class Class<T> implements java.io.Serializable,
java.lang.reflect.GenericDeclaration,
java.lang.reflect.Type,
java.lang.reflect.AnnotatedElement {
// ...
}
Note that lowercase class represents a class type, while uppercase Class refers to the class name itself.
What Does the Class Class Encapsulate?
Class is a class that encapsulates information about the class corresponding to an object. A typical class contains properties, methods, and constructors. For example, Person, Order, and Book are different classes. We need a class to describe classes—this is what Class does. It should have class name, properties, methods, and constructors. Class is the class that describes other classes.
Class objects are the result of an object looking into a mirror. Through reflection, you can see what properties, methods, constructors, and interfaces a class has.
Consider defining a Person class and accessing its class metadata:
public class ReflectionTest {
@Test
public void testClass() {
Class<?> classInfo = null;
// Get Class object
classInfo = Person.class;
System.out.println();
}
}
At a breakpoint, you can inspect the information contained in the Class object.
Similarly, field information is accessible:
public class ReflectionTest {
@Test
public void testClass() {
Class<?> classInfo = null;
classInfo = Person.class;
Field[] fields = classInfo.getDeclaredFields();
System.out.println();
}
}
You can examine the contents of the fields array to see all declared fields.
Why would you need to look in the mirror?
- The object might be passed from external code
- There might be no object available, only a fully qualified class name
Reflection allows you to obtain information about a class even without an instance.
Three Ways to Obtain a Class Object
1. Using the class literal: ClassName.class
2. Using the object: objectReference.getClass()
3. Using the fully qualified name: Class.forName("fully.qualified.ClassName")
public class ReflectionTest {
@Test
public void testClass() throws ClassNotFoundException {
Class<?> classInfo = null;
// 1. Using class literal
classInfo = Person.class;
// 2. Using object reference
// Useful when an object is passed in but its type is unknown
Person person = new Person();
classInfo = person.getClass();
// This pattern is more useful with Object references
Object obj = new Person();
classInfo = obj.getClass();
// 3. Using fully qualified name (may throw exception)
// Common in framework development where configuration files contain class names
String className = "com.example.model.Person";
classInfo = Class.forName(className);
// String class examples
classInfo = String.class;
classInfo = "testString".getClass();
classInfo = Class.forName("java.lang.String");
}
}
Common Class Methods
| Method | Description |
|---|---|
static Class forName(String name) |
Returns the Class object for the specified class name |
Object newInstance() |
Creates a new instance using the default constructor |
Object newInstance(Object[] args) |
Creates a new instance using a specific constructor |
String getName() |
Returns the name of the class, interface, array class, primitive type, or void |
Class getSuperClass() |
Returns the Class object of the superclass |
Class[] getInterfaces() |
Returns the interfaces implemented by this class |
ClassLoader getClassLoader() |
Returns the class loader for this class |
The newInstance() Method
public void testNewInstance() throws ClassNotFoundException,
InstantiationException, IllegalAccessException {
String className = "com.example.model.Person";
Class<?> classInfo = Class.forName(className);
Object instance = classInfo.newInstance();
System.out.println(instance);
}
// Output: com.example.model.Person@2866bb78
This creates a Person instance. Since Person has two constructors, which one is called?
The method invokes the no-argument constructor. Therefore, when defining a class with a parameterized constructor, you should also define a no-argument constructor for use with reflection.
Best practice: When a class declares a parameterized constructor, always declare a no-argument constructor aswell.
ClassLoader
Class loaders are responsible for loading classes into the JVM. The JVM specification defines two types of class loaders: bootstrap class loader and user-defined class loaders. The JVM creates three class loaders during initialization in a hierarchical structure.
public class ReflectionTest {
@Test
public void testClassLoader() throws ClassNotFoundException {
// 1. Get the system class loader
ClassLoader loader = ClassLoader.getSystemClassLoader();
System.out.println(loader);
// 2. Get parent loader (extension class loader)
loader = loader.getParent();
System.out.println(loader);
// 3. Get extension class loader's parent (bootstrap, not accessible)
loader = loader.getParent();
System.out.println(loader);
// 4. Determine which class loader loaded this class
loader = Class.forName("com.example.ReflectionTest")
.getClassLoader();
System.out.println(loader);
// 5. Determine which class loader loaded Object
loader = Class.forName("java.lang.Object")
.getClassLoader();
System.out.println(loader);
}
}
// Output:
// sun.misc.Launcher$AppClassLoader@5ffdfb42
// sun.misc.Launcher$ExtClassLoader@1b7adb4a
// null
// sun.misc.Launcher$AppClassLoader@5ffdfb42
// null
Loading Resources with ClassLoader
The system class loader can load classes from the project's src directory. If resource files are also placed under src, they can be loaded using the ClassLoader.
Use getResourceAsStream to obtain an InputStream for files in the classpath:
public class ReflectionTest {
@Test
public void testClassLoader() throws IOException {
// Load from src root
InputStream input1 = this.getClass()
.getClassLoader()
.getResourceAsStream("config.txt");
// Load from package directory - use full path
InputStream input2 = this.getClass()
.getClassLoader()
.getResourceAsStream("com/example/resources/data.txt");
}
}
Reflection
Overview
Reflection is the key feature that makes Java a dynamic language. The reflection mechanism allows programs to obtain any class's internal information at runtime through the Reflection API, and directly manipulate any object's internal properties and methods.
Java reflection provides the following capabilities:
- Create instances of any class at runtime
- Access any class's member variables and methods at runtime
- Invoke any object's methods at runtime
- Generate dynamic proxies
Class is a class that describes other classes. It encapsulates:
Methodobjects describing methodsFieldobjects describing fieldsConstructorobjects describing constructors
Working with Methods
public class ReflectionTest {
@Test
public void testMethod() throws Exception {
Class<?> classInfo = Class.forName("com.example.model.Person");
// 1. Get all methods
// 1.1 Get all public methods including inherited ones
// Cannot access private methods, retrieves inherited methods
Method[] allMethods = classInfo.getMethods();
for (Method method : allMethods) {
System.out.print(" " + method.getName());
}
System.out.println();
// 1.2 Get all declared methods including private ones
// Retrieves only methods declared in the current class
allMethods = classInfo.getDeclaredMethods();
for (Method method : allMethods) {
System.out.print(" " + method.getName());
}
System.out.println();
// 1.3 Get specific method
// Requires method name and parameter types
// For: public void setName(String name)
Method method = classInfo.getDeclaredMethod("setName", String.class);
System.out.println(method);
// For: public void setAge(int age)
method = classInfo.getDeclaredMethod("setAge", Integer.class);
System.out.println(method);
// Note: If parameter is int primitive, use int.class not Integer.class
// 2. Invoke method
// First parameter: object to invoke on
// Remaining parameters: arguments to pass
Object instance = classInfo.newInstance();
method.invoke(instance, "John");
// Private methods require setAccessible(true) before invocation
}
}
Key methods:
/**
* @param name the name of the method
* @param parameterTypes the list of parameter types
* @return the Method object matching the specified signature
*/
public Method getMethod(String name, Class<?>... parameterTypes) {
// ...
}
/**
* @param obj the object to invoke the method on
* @param args the arguments for the method call
* @return the result of method invocation
*/
public Object invoke(Object obj, Object... args) {
// ...
}
Custom Utility Methods
Create utility methods that accept an object and method name as parameters to execute methods dynamically.
Consider a Person class with this method:
public void test(String name, Integer age) {
System.out.println("Method called successfully");
}
Utility Method 1: Execute method given object and method name
/**
* Executes a method on the given object
* @param obj the object to invoke the method on
* @param methodName the method name to invoke
* @param args arguments to pass to the method
* @return the result of the method invocation
*/
public Object invoke(Object obj, String methodName, Object... args)
throws Exception {
// 1. Get Method object
// Convert argument types to Class types for method lookup
Class<?>[] paramTypes = new Class<?>[args.length];
for (int i = 0; i < args.length; i++) {
paramTypes[i] = args[i].getClass();
}
// getDeclaredMethod cannot access parent methods
// getMethod cannot access private methods
Method method = obj.getClass().getDeclaredMethod(methodName, paramTypes);
// 2. Execute method
// 3. Return result
return method.invoke(obj, args);
}
Usage:
@Test
public void testInvoke() throws Exception {
Object obj = new Person();
invoke(obj, "test", "Alice", 25);
}
Utility Method 2: Execute method given fully qualified class name
/**
* Executes a method given the fully qualified class name
* @param className fully qualified class name
* @param methodName the method name to invoke
* @param args arguments to pass
* @return the result of the method invocation
*/
public Object invoke(String className, String methodName, Object... args) {
Object obj = null;
try {
obj = Class.forName(className).newInstance();
return invoke(obj, methodName, args);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
Usage:
@Test
public void testInvoke() throws Exception {
invoke("com.example.model.Person", "test", "Bob", 30);
}
Using standard library classes:
@Test
public void testInvoke() throws Exception {
Object result = invoke("java.text.SimpleDateFormat", "format",
new Date());
System.out.println(result);
}
The main benefit of reflection is configurability and loose coupling. You only need the class name and method name—no class instance required. By placing these in a configuration file, you can execute methods based on configuration at runtime.
Accessing Parent Class Methods
The getDeclaredMethod approach retrieves methods from the current class only, including private methods, but cannot access inherited methods. Use getMethod to access public inherited methods.
To access parent class methods:
public class ReflectionTest {
@Test
public void testGetSuperClass() throws Exception {
String className = "com.example.model.Student";
Class<?> classInfo = Class.forName(className);
Class<?> superClassInfo = classInfo.getSuperclass();
System.out.println(superClassInfo);
}
}
// Output: class com.example.model.Person
Utility Method: Access private methods in current or parent classes
/**
* Executes a method that may be private or inherited
* @param obj an instance of the class
* @param methodName the method name
* @param args arguments to pass
* @return the result of the method invocation
*/
public Object invokeExtended(Object obj, String methodName, Object... args) {
Class<?>[] paramTypes = new Class<?>[args.length];
for (int i = 0; i < args.length; i++) {
paramTypes[i] = args[i].getClass();
}
try {
Method method = findMethod(obj.getClass(), methodName, paramTypes);
method.setAccessible(true);
return method.invoke(obj, args);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* Finds a method in the class hierarchy, including private methods
* Searches from the given class up to Object if necessary
* @param clazz the class to search in
* @param methodName the method name
* @param parameterTypes the parameter types
* @return the Method object or null if not found
*/
public Method findMethod(Class<?> clazz, String methodName,
Class<?>... parameterTypes) {
for (; clazz != Object.class; clazz = clazz.getSuperclass()) {
try {
Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
return method;
} catch (Exception e) {
// Continue searching in parent class
}
}
return null;
}
Working with Fields
@Test
public void testField() throws Exception {
String className = "com.example.model.Person";
Class<?> classInfo = Class.forName(className);
// 1. Get fields
// 1.1 Get all declared fields (public and private, not inherited)
Field[] allFields = classInfo.getDeclaredFields();
for (Field field : allFields) {
System.out.print(" " + field.getName());
}
System.out.println();
// 1.2 Get specific field
Field nameField = classInfo.getDeclaredField("name");
System.out.println(nameField.getName());
Person person = new Person("ABC", 12);
// 2. Use fields
// 2.1 Get field value
Object value = nameField.get(person);
System.out.println(value);
// 2.2 Set field value
nameField.set(person, "DEF");
System.out.println(person.getName());
// 2.3 Private fields require setAccessible(true)
Field ageField = classInfo.getDeclaredField("age");
ageField.setAccessible(true);
System.out.println(ageField.get(person));
}
Accessing inherited private fields:
/**
* Creates an instance of className and assigns value to fieldName
* fieldName may be private in the class or its parent
*/
public void testClassField() throws Exception {
String className = "com.example.model.Student";
String fieldName = "age";
Object fieldValue = 20;
Class<?> classInfo = Class.forName(className);
Field field = locateField(classInfo, fieldName);
Object instance = classInfo.newInstance();
assignFieldValue(instance, field, fieldValue);
Object retrievedValue = retrieveFieldValue(instance, field);
}
public Object retrieveFieldValue(Object obj, Field field) throws Exception {
field.setAccessible(true);
return field.get(obj);
}
public void assignFieldValue(Object obj, Field field, Object val)
throws Exception {
field.setAccessible(true);
field.set(obj, val);
}
public Field locateField(Class<?> clazz, String fieldName) throws Exception {
Field field = null;
for (Class<?> current = clazz; current != Object.class;
current = current.getSuperclass()) {
try {
field = current.getDeclaredField(fieldName);
} catch (Exception e) {
// Continue searching in parent
}
}
return field;
}
Working with Constructors
@Test
public void testConstructor() throws Exception {
String className = "com.example.model.Person";
Class<Person> classInfo = (Class<Person>) Class.forName(className);
// 1. Get Constructor objects
// 1.1 Get all constructors
Constructor<Person>[] allConstructors =
(Constructor<Person>[]) Class.forName(className).getConstructors();
for (Constructor<Person> ctor : allConstructors) {
System.out.println(ctor);
}
// 1.2 Get specific constructor with parameter types
Constructor<Person> ctor = classInfo.getConstructor(String.class, int.class);
System.out.println(ctor);
// 2. Create instance using constructor
Object instance = ctor.newInstance("John", 28);
}
Working with Annotations
Define a custom annotation:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
public @interface AgeValidator {
int min();
int max();
}
Apply the annotation to a method:
@AgeValidator(min = 18, max = 35)
public void setAge(int age) {
this.age = age;
}
Without reflection, annotations are invisible during normal usage:
@Test
public void testAnnotation() throws Exception {
Person person = new Person();
person.setAge(10);
}
Annotations can only be accessed through reflection:
/**
* Annotation and Reflection:
* Get annotation using:
* getAnnotation(Class<T>)
* getDeclaredAnnotations()
*/
@Test
public void testAnnotation() throws Exception {
String className = "com.example.model.Person";
Class<?> classInfo = Class.forName(className);
Object instance = classInfo.newInstance();
Method method = classInfo.getDeclaredMethod("setAge", int.class);
int ageValue = 6;
Annotation annotation = method.getAnnotation(AgeValidator.class);
if (annotation != null) {
if (annotation instanceof AgeValidator) {
AgeValidator validator = (AgeValidator) annotation;
if (ageValue < validator.min() || ageValue > validator.max()) {
throw new RuntimeException("Age is invalid");
}
}
}
method.invoke(instance, 20);
System.out.println(instance);
}
For validation logic that accesses annotations, the object and method must be created through reflection.
Reflection and Generics
Defining a Generic Class
public class DAO<T> {
T get(Integer id) {
return null;
}
void save(T entity) {
}
}
Create a subclass extending the generic class:
public class PersonDAO extends DAO<Person> {
}
Here, the generic parameter T in the parent class receives Person as its actual type argument.
Alternatively, preserve the generic parameter:
public class PersonDAO<T> extends DAO<T> {
}
Testing the inheritance:
@Test
public void testGeneric() throws Exception {
PersonDAO<Person> dao = new PersonDAO<Person>();
Person entity = new Person();
// Calling parent's save method passes Person as T
dao.save(entity);
// The get method returns type T, which should be Person
Person result = dao.get(1);
System.out.print(result);
}
The challenge: The get method returns T, but when T becomes Person, we need to create a Person object dynamically. The solution involves clazz.newInstance(), so we need a Class object representing T.
Add a field to store the Class object for T:
public class DAO<T> {
private Class<T> entityClass;
T get(Integer id) {
return null;
}
}
How to obtain this Class object?
public DAO() {
// 1. Reference the current instance
System.out.println("DAO Constructor...");
System.out.println(this); // com.example.PersonDAO@66588ec0
System.out.println(this.getClass()); // class com.example.PersonDAO
// 2. Get parent class
Class<?> parentClass = this.getClass().getSuperclass();
System.out.println(parentClass); // class com.example.DAO
// Only the type name is available, not the generic parameter
// 3. Get parent class with generic type
Type genericParent = this.getClass().getGenericSuperclass();
System.out.println(genericParent); // com.example.DAO<com.example.Person>
// 4. Extract the actual generic parameter
// Type is an empty interface; use its subinterface ParameterizedType
if (genericParent instanceof ParameterizedType) {
ParameterizedType paramType = (ParameterizedType) genericParent;
Type[] typeArgs = paramType.getActualTypeArguments();
System.out.println(Arrays.asList(typeArgs)); // [class com.example.Person]
if (typeArgs != null && typeArgs.length > 0) {
Type typeArg = typeArgs[0];
System.out.println(typeArg); // class com.example.Person
if (typeArg instanceof Class) {
entityClass = (Class<T>) typeArg;
}
}
}
}
Utility Method: Extract Generic Type from Parent Class
public class ReflectionTest {
/**
* Gets the generic parameter type declared in the parent class
* Example: public EmployeeDao extends BaseDao<Employee, String>
* @param clazz the child class's Class object
* @param index the index of the generic parameter (0-based)
* @return the Class object for the generic parameter
*/
@SuppressWarnings("unchecked")
public Class<?> getParentGenericType(Class<?> clazz, int index) {
Type parentType = clazz.getGenericSuperclass();
if (!(parentType instanceof ParameterizedType)) {
return null;
}
ParameterizedType paramType = (ParameterizedType) parentType;
Type[] args = paramType.getActualTypeArguments();
if (args == null || index < 0 || index > args.length - 1) {
return null;
}
Type arg = args[index];
if (arg instanceof Class) {
return (Class<?>) arg;
}
return null;
}
@SuppressWarnings("unchecked")
public Class<?> getFirstGenericType(Class<?> clazz) {
return getParentGenericType(clazz, 0);
}
@Test
public void testGetParentGenericType() {
Class<?> clazz = PersonDAO.class;
Class<?> typeArg = getParentGenericType(clazz, 0);
System.out.println(typeArg);
// Output: class com.example.Person
}
}
Summary
-
Class: A class that describes other classes. It encapsulates
Method(describing methods),Field(describing fields), andConstructor(describing constructors). -
Obtaining a Class object:
ClassName.classobjectReference.getClass()Class.forName("fully.qualified.ClassName")
-
Working with Methods:
- Getting methods:
getDeclaredMethods()returns all declared methods;getDeclaredMethod(name, parameterTypes)returns a specific method - Invoking methods: Private methods require
setAccessible(true)first; usemethod.invoke(obj, args)
- Getting methods:
-
Working with Fields:
- Getting fields:
getField(fieldName)orgetDeclaredField(fieldName) - Getting field values: Call
setAccessible(true)for private fields, thenfield.get(obj) - Setting field values:
field.set(obj, value)
- Getting fields:
-
Constructors and Annotations: Understand how to use
Constructorfor object creation and access annotations via reflection -
Reflection and Generics:
getGenericSuperclass()retrieves the parent class with type parametersParameterizedTypeis a subinterface ofType- Call
getActualTypeArguments()to obtain the array of generic parameters