Contents

Java ships with several annotations that the compiler itself understands. @Override is the most important: it tells the compiler to verify that the method actually overrides a superclass method, catching typos and signature mismatches at compile time rather than silently creating an unrelated overload. @Deprecated signals that an API element is scheduled for removal, causing the compiler to emit a warning at every call site so developers know to migrate. @SuppressWarnings silences specific named warnings when you intentionally accept the risk (e.g., raw-type casts during migration). Meta-annotations like @Retention and @Target control how annotation types themselves behave — they annotate the annotation declarations.

// @Override — tells compiler this method overrides a superclass method // Compilation error if no such method exists → catches typos class Animal { public String sound() { return "..."; } } class Dog extends Animal { @Override public String sound() { return "Woof"; } // compiler verifies this overrides } // @Deprecated — marks code as obsolete; compiler warns on use @Deprecated(since = "2.0", forRemoval = true) public void oldMethod() { /* use newMethod() instead */ } // @SuppressWarnings — suppress specific compiler warnings @SuppressWarnings("unchecked") List<String> list = (List<String>) getRawList(); // no unchecked warning @SuppressWarnings({"unchecked", "deprecation"}) // multiple warnings // @FunctionalInterface — ensure interface has exactly one abstract method @FunctionalInterface interface Transformer<T, R> { R transform(T input); // default methods allowed default Transformer<T, R> andLog() { return input -> { R result = transform(input); System.out.println(input + " → " + result); return result; };} } // @SafeVarargs — suppress heap pollution warnings for generic varargs @SafeVarargs static <T> List<T> listOf(T... elements) { return new ArrayList<>(Arrays.asList(elements)); }

Custom annotations are declared with the @interface keyword, which makes them a special kind of interface — the compiler and JVM treat annotation types as interfaces that extend java.lang.annotation.Annotation. Each method declaration inside the @interface body defines an annotation element (attribute). Elements can have default values declared with the default keyword; elements without defaults are required at every use site. The special element name value allows positional usage when it is the only element being set. Element types are restricted to primitives, String, Class, enum constants, other annotations, and arrays of those types — arbitrary objects are not allowed.

import java.lang.annotation.*; // @interface declares an annotation type // Elements (methods) define annotation attributes public @interface Route { String path(); // required attribute (no default) String method() default "GET"; // optional attribute with default boolean auth() default false; } // Using the annotation @Route(path = "/users", method = "GET") public class UserController { } @Route(path = "/admin", method = "POST", auth = true) public class AdminController { } // Annotation with array attribute public @interface Roles { String[] value(); // "value" is the magic name — can be used positionally } @Roles({"ADMIN", "MODERATOR"}) // with name class AdminPanel { } @Roles("USER") // single element shorthand when attribute is named "value" class UserDashboard { } // Annotation with Class and enum attributes public @interface Config { Class<?> handler(); LogLevel level() default LogLevel.INFO; String[] tags() default {}; } @Config(handler = DefaultHandler.class, level = LogLevel.DEBUG, tags = {"v2", "beta"}) class Service { }

@Retention controls how long an annotation survives after compilation. RetentionPolicy.SOURCE annotations are discarded by the compiler (used only by source-level tools like @Override checks); CLASS annotations are written into the .class file but stripped by the JVM at load time (the default, useful for bytecode instrumentation tools); RUNTIME annotations survive into the running JVM and are readable via reflection — this is mandatory for any annotation that frameworks like Spring, JUnit, or Jackson need to inspect at runtime. @Target restricts where an annotation can be applied using ElementType constants such as TYPE, METHOD, FIELD, and PARAMETER; omitting @Target makes the annotation applicable everywhere. @Inherited causes a type-level annotation to be visible on subclasses that do not re-declare it, while @Documented includes the annotation in generated Javadoc.

import java.lang.annotation.*; // @Retention — how long the annotation survives // SOURCE — discarded by compiler (only for source tools like @Override) // CLASS — in .class file but not loaded by JVM (default; used by bytecode tools) // RUNTIME — available via reflection at runtime (required for framework use) // @Target — where the annotation can be applied // TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, // ANNOTATION_TYPE, PACKAGE, TYPE_PARAMETER, TYPE_USE, RECORD_COMPONENT @Retention(RetentionPolicy.RUNTIME) // readable at runtime @Target({ElementType.METHOD, ElementType.TYPE}) // on classes and methods public @interface Benchmark { String description() default ""; int warmUpRuns() default 5; } // @Documented — include in Javadoc output @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface NotNull { String message() default "Field must not be null"; } // @Inherited — subclass inherits annotation from superclass // Only works on TYPE-targeted annotations @Inherited @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Auditable { } @Auditable class BaseEntity { } class User extends BaseEntity { } // User.class.isAnnotationPresent(Auditable.class) → true (inherited) // Practical full example @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Timed { String metric() default ""; // metric name for monitoring }

The Java reflection API exposes annotations through AnnotatedElement, which is implemented by Class, Method, Field, and Constructor. isAnnotationPresent(Class) is a fast boolean check, while getAnnotation(Class) returns the annotation instance or null. getAnnotations() returns all annotations including inherited ones; getDeclaredAnnotations() returns only those directly present on the element. To read parameter-level annotations, getParameterAnnotations() returns a two-dimensional array — one entry per parameter, each containing that parameter's annotations. All of this only works when the annotation has RetentionPolicy.RUNTIME; annotations with other retention policies will simply not appear in reflection results.

import java.lang.reflect.*; // Reading class-level annotations Class<?> cls = UserController.class; if (cls.isAnnotationPresent(Route.class)) { Route route = cls.getAnnotation(Route.class); System.out.println("Path: " + route.path()); System.out.println("Method: " + route.method()); System.out.println("Auth: " + route.auth()); } // Reading all annotations on a class for (Annotation ann : cls.getAnnotations()) { // includes inherited System.out.println(ann.annotationType().getSimpleName()); } for (Annotation ann : cls.getDeclaredAnnotations()) { // only directly present System.out.println(ann.annotationType().getSimpleName()); } // Reading method-level annotations for (Method method : cls.getDeclaredMethods()) { if (method.isAnnotationPresent(Timed.class)) { Timed timed = method.getAnnotation(Timed.class); String metric = timed.metric().isEmpty() ? method.getName() : timed.metric(); System.out.println("Timing: " + metric); } } // Reading parameter annotations Method m = cls.getMethod("createUser", String.class, int.class); Annotation[][] paramAnnotations = m.getParameterAnnotations(); for (int i = 0; i < paramAnnotations.length; i++) { for (Annotation ann : paramAnnotations[i]) { System.out.println("Param " + i + ": " + ann.annotationType().getSimpleName()); } } // Reading field annotations for (Field field : cls.getDeclaredFields()) { if (field.isAnnotationPresent(NotNull.class)) { NotNull nn = field.getAnnotation(NotNull.class); System.out.println(field.getName() + ": " + nn.message()); } } Only annotations with @Retention(RetentionPolicy.RUNTIME) are available via reflection. If you're writing a framework annotation that needs to be read at runtime (like Spring's @Component), RUNTIME retention is mandatory.

Prior to Java 8 you could not apply the same annotation type more than once to the same element — the workaround was to manually wrap multiple instances inside a container annotation. Java 8 made this transparent with @Repeatable: you still need a container annotation that holds an array of the repeatable type, but the compiler generates the container automatically when you apply the annotation more than once. Two rules to remember: the repeatable annotation must declare @Repeatable(ContainerType.class), and the container must have a value() element typed as an array of the repeatable annotation. At runtime, use getAnnotationsByType() instead of getAnnotation() — it transparently unwraps the container and returns individual instances whether the annotation was applied once or many times.

import java.lang.annotation.*; // Before Java 8: couldn't apply same annotation twice — workaround was a container // Java 8+: @Repeatable // Step 1: Create the container annotation @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Schedules { Schedule[] value(); // container holds array of @Schedule } // Step 2: Mark the repeatable annotation with its container @Repeatable(Schedules.class) // ← points to container @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Schedule { String cron(); String zone() default "UTC"; } // Step 3: Use multiple times on the same target @Schedule(cron = "0 0 9 * * MON-FRI") @Schedule(cron = "0 0 18 * * FRI", zone = "US/Eastern") public void generateReport() { /* ... */ } // Reading repeatable annotations at runtime Method m = ReportService.class.getMethod("generateReport"); // getAnnotationsByType works for both single and repeated uses Schedule[] schedules = m.getAnnotationsByType(Schedule.class); for (Schedule s : schedules) { System.out.println("Cron: " + s.cron() + " Zone: " + s.zone()); } // Alternatively, get the container Schedules container = m.getAnnotation(Schedules.class); if (container != null) { for (Schedule s : container.value()) { System.out.println(s.cron()); } }