Reflection basics: Class<?>, getDeclaredFields, and reading annotations at runtime
Getting Class objects, inspecting fields and methods, and how frameworks use reflection under the hood.
What reflection is
Reflection is the ability of a program to examine and modify its own structure at runtime. You can discover the fields, methods, constructors, and annotations of a class without knowing them at compile time. This is how frameworks like Spring (dependency injection), JUnit (test discovery), and Jackson (JSON serialization) work.
Getting a Class object
Three ways to obtain a Class<?> object:
// 1 — from a live object
String s = "hello";
Class<?> c1 = s.getClass(); // java.lang.String
// 2 — from a class literal (compile-time — preferred when you know the type)
Class<String> c2 = String.class;
// 3 — from a name string (runtime lookup, throws ClassNotFoundException)
Class<?> c3 = Class.forName("java.lang.String");
Inspecting fields and methods
Class<?> cls = Employee.class;
// getDeclaredFields — fields declared in THIS class (incl. private), no inherited
for (Field f : cls.getDeclaredFields()) {
System.out.println(f.getName() + " : " + f.getType().getSimpleName());
}
// getMethods — ALL public methods including inherited ones from superclasses
for (Method m : cls.getMethods()) {
System.out.println(m.getName());
}
Key distinction: getDeclaredFields() returns this class's own fields including private, but excludes inherited. getFields() returns only public fields but includes inherited ones.
Reading annotations at runtime
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Audited { String by() default "system"; }
// ---
Method m = MyService.class.getMethod("processOrder");
if (m.isAnnotationPresent(Audited.class)) {
Audited a = m.getAnnotation(Audited.class);
System.out.println("Audited by: " + a.by());
}
isAnnotationPresent(Class) returns true only if the annotation has RUNTIME retention. getAnnotation(Class) returns the annotation instance, or null if absent.
How frameworks use this
- Spring: scans classes for @Component, @Autowired, @RequestMapping at startup — all require RUNTIME retention.
- JUnit 5: discovers @Test methods on test classes by reflecting on all methods and checking isAnnotationPresent(Test.class).
- Jackson: reads @JsonProperty on fields and methods to map between JSON keys and Java field names.
Reflection is powerful but slow — it bypasses compile-time type checking. Production code calls reflected methods infrequently; frameworks usually build an optimised call plan at startup and cache it.