Java 51: Annotations & Reflection Basics
easy⏱ 5 mincoursejava
Declaring annotations & retention
@interface Test {} declares an annotation. @Retention decides its lifespan: SOURCE (compiler only, e.g. @Override), CLASS (in the .class file, default, not at runtime), or RUNTIME (readable via reflection — required for frameworks). @Target restricts where it can go (METHOD, TYPE, FIELD). Annotations can have elements with defaults: @Test(timeout = 1000).
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Test { long timeout() default 0; }
class MyTests {
@Test void addsCorrectly() { /* ... */ }
}
Reflection: reading classes at runtime
Class<?> c = obj.getClass() is the entry point. c.getDeclaredMethods(), getDeclaredFields(), getConstructors() enumerate members. method.isAnnotationPresent(Test.class) checks for an annotation; method.getAnnotation(Test.class) reads its elements. method.invoke(instance, args...) calls it dynamically. This is how a test runner finds and runs @Test methods without you registering them.
for (Method m : MyTests.class.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
m.invoke(new MyTests()); // dynamic call
}
}
The costs of reflection
Reflection is slower than direct calls (no JIT inlining, security checks), bypasses compile-time type checks (errors surface at runtime as InvocationTargetException), and setAccessible(true) breaks encapsulation. Use it for framework-level wiring, not hot paths. Modern alternatives — MethodHandle, annotation processors (compile-time codegen), and records — reduce the need for runtime reflection.
Build a reflective test runner
Mark methods with a @Test-style flag (model the RUNTIME annotation as registered metadata). Write runTests(instance) that discovers every test method, invokes it, catches thrown assertions, and tallies pass/fail. Include one passing and one failing test. Print a JUnit-style summary: Tests run: N, Failures: M and each test's status.
Prefer annotation processors for performance
Runtime reflection scanning has a startup cost (Spring's classpath scan). Annotation processors (javax.annotation.processing) run at compile time and generate plain code — Dagger, Micronaut, and Lombok use this for zero-reflection, faster-startup wiring. If you control the build, compile-time codegen beats runtime reflection.
Quiz: why @Retention(RUNTIME)?
A custom annotation isn't visible to your framework's reflection — why? It probably lacks @Retention(RUNTIME). The default retention is CLASS, which keeps the annotation in the bytecode but discards it before runtime, so isAnnotationPresent returns false. Any annotation a framework reads reflectively must be @Retention(RetentionPolicy.RUNTIME).