1. What reflection is
Reflection in Java is a mechanism that allows a program to inspect — and even modify — its own structure at runtime. It’s as if you could look inside yourself and find out: “What fields and methods do I have? What constructor do I have? What class am I anyway?” — and even invoke a private method on yourself.
Formal definition:
Reflection is an API that lets you obtain information about classes, interfaces, fields, methods, and constructors at runtime, and also work with them.
Why use reflection?
- Frameworks: Spring, Hibernate, and JUnit use reflection for “magic”: automatically creating objects, injecting dependencies, invoking methods by name and by annotations.
- Serialization: Turning objects into JSON/XML and back — you need to discover fields and be able to access them.
- Testing: Automatically finding and invoking methods with annotations (for example, @Test).
- Dynamic class loading: Loading a class by name at runtime (plugins, drivers).
- IDEs and code analysis: Autocompletion, inspections, refactoring.
Real‑world example
public class Person {
private String name;
private int age;
public void sayHello() {
System.out.println("Hello, my name is " + name);
}
}
With reflection we can, at runtime, discover that the Person class has fields name and age, as well as a sayHello method. Moreover, you can change the value of a private field or invoke a method by its name.
A bit of humor
You could say reflection is like in “The Matrix”: you realise that all your code is data that can be inspected and changed on the fly. Just don’t forget the red pill of security!
2. The Class class: the heart of reflection
In Java, every object and every type at runtime is associated with a special object of type Class. This object is the “meta‑information” about the type. The java.lang.Class<T> class is the central entry point into the world of reflection.
How do you obtain a Class object?
There are several ways:
- Via .class
The most straightforward and safe way if the class is known at compile time:Class<Person> personClass = Person.class; - Via an object: getClass()
If you already have an instance:
Here, clazz is the Class object that describes the actual class of the object p.Person p = new Person(); Class<?> clazz = p.getClass(); - Via a string: Class.forName()
If the class name is known only at runtime:
Suitable for loading plugins, drivers, and other dynamic use cases.Class<?> clazz = Class.forName("com.example.Person");
Example: Getting the class name
Person p = new Person();
Class<?> clazz = p.getClass();
System.out.println(clazz.getName()); // Will print: Person or com.example.Person
Table: Main ways to obtain a Class object
| How to obtain | When to use | Example |
|---|---|---|
|
If the class is known at compile time | |
|
If you have an object | |
|
If the class name is known at runtime | |
Diagram: An object and its Class
+-------------------+
| Person p |
+-------------------+
|
v
+-------------------+
| p.getClass() |
+-------------------+
|
v
+-------------------+
| Class<Person> |
+-------------------+
Type checking via Class
Sometimes you need to know whether an object belongs to a specific class:
if (p.getClass() == Person.class) {
System.out.println("This is definitely a Person!");
}
Or, taking inheritance into account — the usual instanceof operator:
if (p instanceof Person) {
// works as usual
}
3. When reflection is needed — and when it isn’t
With great power comes great responsibility. Reflection is a powerful tool, but use it only when it’s truly necessary.
Where reflection is essential
- Frameworks and libraries: Spring, Hibernate, JUnit, Jackson — automation, “magic,” and less “manual” code.
- Serialization: You need to discover an object’s fields to turn it into JSON/XML.
- Plugins and extensions: Classes are loaded dynamically and their structure is not known beforehand.
Why you shouldn’t overuse reflection
- Loss of type safety: Errors surface at runtime.
- Maintenance complexity: Code becomes less obvious.
- Performance: Slower than direct calls.
- Security: You can bypass encapsulation and expose private data.
Example: When reflection isn’t needed
p.sayHello();
Example: When reflection is a must‑have
You’re writing a mini‑framework for testing: the user marks methods with the @Test annotation, and the program must find and invoke them automatically — there’s no way to do this without reflection.
4. Demo: getting to know Class
Example: Printing the class name and its superclass
public class ReflectionDemo {
public static void main(String[] args) {
String s = "Hello, reflection!";
Class<?> clazz = s.getClass();
System.out.println("Class name: " + clazz.getName());
System.out.println("Simple class name: " + clazz.getSimpleName());
System.out.println("Package: " + clazz.getPackageName());
System.out.println("Superclass: " + clazz.getSuperclass().getName());
}
}
Result:
Class name: java.lang.String
Simple class name: String
Package: java.lang
Superclass: java.lang.Object
Example: Getting a Class for primitive types
Class<Integer> intClass = int.class;
System.out.println(intClass.getName()); // int
Class<?> doubleClass = double.class;
System.out.println(doubleClass.getName()); // double
Example: Getting a Class for an array
int[] arr = new int[10];
Class<?> arrClass = arr.getClass();
System.out.println(arrClass.getName()); // [I (JVM-specific format for arrays)
Example: Checking type membership
if (arrClass.isArray()) {
System.out.println("It's an array!");
}
5. Practice: mini program “What class is it?”
Let’s write a program that takes a fully qualified class name (for example, "java.util.ArrayList") and prints basic information about it.
import java.util.Scanner;
public class ClassInfoPrinter {
public static void main(String[] args) throws Exception {
Scanner scanner = new Scanner(System.in);
System.out.print("Enter the fully qualified class name: ");
String className = scanner.nextLine();
Class<?> clazz = Class.forName(className);
System.out.println("Class name: " + clazz.getName());
System.out.println("Package: " + clazz.getPackageName());
System.out.println("Superclass: " + clazz.getSuperclass().getName());
Class<?>[] interfaces = clazz.getInterfaces();
System.out.print("Implements interfaces: ");
for (Class<?> i : interfaces) {
System.out.print(i.getName() + " ");
}
System.out.println();
}
}
Sample run:
Enter the fully qualified class name: java.util.ArrayList
Class name: java.util.ArrayList
Package: java.util
Superclass: java.util.AbstractList
Implements interfaces: java.util.List java.util.RandomAccess java.lang.Cloneable java.io.Serializable
Interesting facts about Class and reflection
- Every loaded type in the JVM has one shared representative — a Class object. All instances of the same type share the same Class<T>.
- You can determine the kind of type: isInterface(), isEnum(), isArray(), isPrimitive(), and others.
- If you specify an incorrect name in Class.forName, you’ll get a ClassNotFoundException.
- With reflection you can create objects without knowing the class at compile time — we’ll talk about this in the next lecture.
6. Common mistakes when first learning reflection
Mistake #1: Expecting reflection to be fast.
In reality, reflection is slower than ordinary method calls and field access. Don’t use it for every little thing.
Mistake #2: Trying to get a Class for a non‑existent class.
If you misspell the name, you’ll get a ClassNotFoundException. Handle this exception.
Mistake #3: Confusing a Class object with an instance.
A Class object describes the structure of a type; it is not an instance of that type.
Mistake #4: Using reflection when it isn’t necessary.
If you can manage with ordinary code, do so. Reflection is intended for dynamics, frameworks, plugins, and similar tasks.
Mistake #5: Careless handling of private fields and methods.
Reflection allows you to bypass encapsulation, which can lead to errors and security issues, especially in large projects.
GO TO FULL VERSION