skip to content

Generics & Annotations via Reflection

Reflection can recover declared generic type arguments through the retained Signature attribute, and can read annotations only when their retention is RUNTIME. The RUNTIME retention requirement is the detail interviewers most often test.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

Why does an annotation need RUNTIME retention to be readable via reflection, and what happens if it doesn't have it?

level: juniorimportance: must knowfreq 70%

answer

  1. SOURCE / CLASS / RUNTIME
  2. CLASS is the default
  3. reflection sees only RUNTIME
  4. getAnnotation returns null silently
  5. frameworks always use @Retention(RUNTIME)

basics

~10 s

Reflection can only see annotations marked to survive into the running program. If an annotation isn't kept at runtime, reflection methods like getAnnotation return nothing, even though the annotation was in the source.

solid answer

~40 s

Every annotation has a retention policy set with @Retention(RetentionPolicy.X). SOURCE means the compiler discards it (e.g. @Override). CLASS (the default) keeps it in the .class file but the JVM does not load it, so reflection can't see it. RUNTIME keeps it in the class file AND makes the JVM load it into metadata, so methods such as getAnnotation, getAnnotations, and isAnnotationPresent can read it. If you write a custom annotation and rely on reflection (validation, serialization, dependency injection), you must annotate it with @Retention(RetentionPolicy.RUNTIME); otherwise isAnnotationPresent returns false and getAnnotation returns null at run time, with no error at compile time. This silent failure is a common gotcha.

code

java · 18 lines
java
import java.lang.annotation.*;
import java.lang.reflect.Method;

@Retention(RetentionPolicy.RUNTIME)   // remove this line -> defaults to CLASS
@Target(ElementType.METHOD)
@interface Audited {}

class Service {
    @Audited void save() {}
}

public class Demo {
    public static void main(String[] args) throws Exception {
        Method m = Service.class.getDeclaredMethod("save");
        System.out.println(m.isAnnotationPresent(Audited.class));
        // true with @Retention(RUNTIME); false (silently) without it
    }
}

go deeper

for a junior

Knows reflection can read annotations and that the annotation must be 'kept at runtime' (RUNTIME) to be visible.

for a middle

Names all three RetentionPolicy values, knows CLASS is the default, and recalls the silent-null failure when RUNTIME is missing.

for a senior

Explains that reflection reads JVM-loaded class metadata, distinguishes @Retention from @Target, and can debug a 'framework ignores my annotation' issue back to a missing RUNTIME retention.

for a principal

Reasons about retention trade-offs (startup metadata cost, annotation processors vs reflection vs bytecode reading), and designs annotation contracts for libraries used across module boundaries.

## What is an annotation? An **annotation** in Java is a piece of metadata you attach to code (classes, methods, fields, parameters) with the `@` syntax, e.g. `@Override`, `@Deprecated`, or a custom one like `@JsonProperty`. Metadata means 'data about the code' — it does not run by itself; some other program reads it and acts on it. ## What is reflection? **Reflection** is Java's ability, at run time, to inspect and manipulate classes, methods, and fields that it discovers while the program is running, rather than at compile time. Through `java.lang.reflect` and `java.lang.Class` you can ask 'what methods does this class have?', 'what is the type of this field?', and 'what annotations are on this method?'. ## The retention policy When you declare a custom annotation, you can control **how long it survives** using a meta-annotation (an annotation on an annotation) called `@Retention`, which takes one `RetentionPolicy` value: - `SOURCE` — the annotation exists only in the source code; the compiler throws it away. Used by tools that run at compile time (lint, `@Override`, Lombok). It never reaches the `.class` file. - `CLASS` — **this is the default if you don't specify @Retention.** The annotation is written into the compiled `.class` file (the bytecode), but when the JVM loads the class it does **not** keep the annotation in the in-memory metadata. So bytecode tools can read it, but reflection cannot. - `RUNTIME` — the annotation is written into the `.class` file AND the JVM loads it into the runtime class metadata, making it visible to reflection. ## Why reflection needs RUNTIME Reflection reads the **in-memory metadata** that the JVM builds when it loads a class. Only `RUNTIME`-retained annotations are placed into that metadata. So the reflection accessor methods on `AnnotatedElement` (the interface implemented by `Class`, `Method`, `Field`, etc.) — `getAnnotation(Class)`, `getAnnotations()`, `getDeclaredAnnotations()`, `isAnnotationPresent(Class)` — can only ever return `SOURCE` (never present), `CLASS` (in the file but not loaded → invisible), or `RUNTIME` (visible) annotations. In practice that means: **only RUNTIME annotations are reflectively visible.** ## The silent-failure trap If you forget `@Retention(RetentionPolicy.RUNTIME)` on a custom annotation that a framework reads via reflection, the code still **compiles** and the annotation still **appears in source**, but at run time `isAnnotationPresent(...)` returns `false` and `getAnnotation(...)` returns `null`. Nothing throws — the feature just silently does nothing. This is why almost every annotation used by Spring, Jackson, JUnit, Hibernate validation, etc. is declared `@Retention(RUNTIME)`. ## Declaring it ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyMarker {} ``` Note `@Target` (another meta-annotation) restricts *where* the annotation may be placed; it is separate from retention but usually appears alongside it.

  • What is the default RetentionPolicy if you omit @Retention?
    CLASS — the annotation is in the .class file but is not loaded into runtime metadata, so reflection cannot read it.
  • Name a built-in annotation with SOURCE retention.
    @Override (and @SuppressWarnings). They are only meaningful to the compiler and are discarded after compilation.

saying these in an interview costs you the question

  • Thinking all annotations are visible to reflection by default
  • Believing the default retention is RUNTIME (it is CLASS)
  • Assuming a missing retention causes a compile error (it fails silently at run time)
  • Confusing @Retention (how long it lives) with @Target (where it can be placed)

context

open as a page

Given type erasure removes generic types at runtime, how can reflection still recover that a field is declared as List<String>?

level: middleimportance: must knowfreq 68%

basics

~20 s

Erasure removes generics from the values (objects) at runtime, but the generic types written in declarations (fields, method signatures, superclasses) are kept in the class file. Reflection reads them via getGenericType, and you inspect the result as a ParameterizedType to get String.

open as a page

Design a small reflection-based validator that reads a @NotBlank annotation on String fields. What must the annotation declare, and what reflection calls do you use?

level: middleimportance: should knowfreq 40%

basics

~10 s

Define @NotBlank with @Retention(RUNTIME) and @Target(FIELD). At runtime, loop over getDeclaredFields(), check isAnnotationPresent(NotBlank.class), call setAccessible(true), read the value with field.get(object), and fail if the String is null or blank.

open as a page

What is the difference between getAnnotations() and getDeclaredAnnotations(), and when does @Inherited matter?

level: seniorimportance: should knowfreq 48%

basics

~10 s

getDeclaredAnnotations() returns only annotations written directly on that element. getAnnotations() also includes inherited ones from a superclass — but only annotations themselves marked @Inherited, and only for class inheritance.

open as a page

How do you read a method or class's type variables and their bounds via reflection, and what do you get for `<T extends Number & Comparable<T>>`?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

Use getTypeParameters() on a Class or Method to get its declared type variables (like T). Each TypeVariable has getBounds(), which returns its upper bounds — for <T extends Number & Comparable<T>> you get [Number, Comparable<T>].

open as a page