View source on GitHub

Module 20 - Annotations

Table of contents
  1. Module 20 - Annotations
    1. Overview
    2. Defining Custom Annotations
      1. Element types
    3. Retention Policies
    4. Target
      1. TYPE_USE example
    5. Meta-Annotations
    6. @Repeatable
    7. @Inherited
    8. Built-in Java Annotations
      1. @Override
      2. @Deprecated
      3. @SuppressWarnings
      4. @FunctionalInterface
      5. @SafeVarargs
    9. Runtime Processing via Reflection
      1. Typical framework patterns
    10. Summary

Overview

Annotations are metadata attached to code elements (classes, methods, fields, parameters). They do not change program logic directly but are read by:

  • The compiler (@Override, @SuppressWarnings)
  • Frameworks at runtime via reflection (Spring, JUnit, JPA)
  • Build tools / annotation processors at compile time (Lombok, MapStruct)

Defining Custom Annotations

An annotation type is declared with @interface:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Author {
    String value();           // "value" is the conventional single-element name
    String date() default ""; // optional element with default
}

Usage:

@Author("Alice")              // short form - element named "value" can be omitted
@Author(value = "Bob", date = "2024-01-01")  // explicit form

Element types

Type Example default
String ""
int / long / boolean 0, 0L, false
Class<?> Void.class (sentinel for “not specified”)
enum MyEnum.DEFAULT
Annotation another annotation literal
T[] (array) {}

Retention Policies

@Retention(RetentionPolicy.SOURCE)   // stripped by javac; e.g. @Override
@Retention(RetentionPolicy.CLASS)    // stored in .class but not loaded (default)
@Retention(RetentionPolicy.RUNTIME)  // available via reflection at runtime

Only RUNTIME annotations can be read with reflection.


Target

Controls where an annotation may be placed:

@Target({
    ElementType.TYPE,           // class, interface, enum, record
    ElementType.FIELD,
    ElementType.METHOD,
    ElementType.PARAMETER,
    ElementType.CONSTRUCTOR,
    ElementType.LOCAL_VARIABLE,
    ElementType.ANNOTATION_TYPE,
    ElementType.PACKAGE,
    ElementType.TYPE_PARAMETER,  // <T extends ...>
    ElementType.TYPE_USE,        // any type usage
    ElementType.MODULE,
    ElementType.RECORD_COMPONENT
})

TYPE_USE example

TYPE_USE allows annotation on any type in the code:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
public @interface NonNull {}

// usage
@NonNull String name;
List<@NonNull String> items;
Object obj = (@NonNull Object) rawRef;

Meta-Annotations

Meta-annotation Effect
@Retention How long the annotation is kept
@Target Where it can be placed
@Documented Include in Javadoc
@Inherited Subclasses inherit superclass annotation
@Repeatable Same annotation may appear more than once

@Repeatable

To allow multiple occurrences of the same annotation, declare a container:

// Step 1 - the repeatable annotation
@Repeatable(Tags.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Tag {
    String value();
}

// Step 2 - the container annotation
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Tags {
    Tag[] value();
}

// Usage
@Tag("api")
@Tag("public")
public class UserService { ... }

// Reading (getAnnotationsByType unwraps the container automatically)
Tag[] tags = UserService.class.getAnnotationsByType(Tag.class);

@Inherited

When a class has an @Inherited annotation, subclasses automatically inherit it:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface Component {
    String name() default "";
}

@Component(name = "baseRepo")
class BaseRepository { ... }

class UserRepository extends BaseRepository { ... }  // no @Component declared

// But...
UserRepository.class.isAnnotationPresent(Component.class);  // true!

Note: @Inherited only applies to class-level annotations, not methods or fields.


Built-in Java Annotations

@Override

@Override
public String toString() { ... }  // compiler error if nothing to override

Without @Override, a typo in the method name silently creates a new method.

@Deprecated

@Deprecated(since = "2.0", forRemoval = true)
public String legacyFormat(int v) { ... }
  • since - version when deprecated
  • forRemoval = true - stronger signal; tools warn more aggressively

@SuppressWarnings

@SuppressWarnings("unchecked")    // suppress unchecked cast
@SuppressWarnings("deprecation")  // suppress deprecated API usage
@SuppressWarnings({"unchecked", "rawtypes"})  // multiple

Common names: "unchecked", "deprecation", "rawtypes", "unused", "serial".

@FunctionalInterface

@FunctionalInterface
public interface Transformer<T, R> {
    R transform(T input);   // exactly one abstract method - compiler enforces this
}

@SafeVarargs

Suppresses heap-pollution warning for generic varargs. Only valid on final, static, private methods or constructors:

@SafeVarargs
public static <T> List<T> listOf(T... items) {
    return List.of(items);   // safe: we don't write into the array
}

Runtime Processing via Reflection

// Class-level
Author a   = MyClass.class.getAnnotation(Author.class);
Tag[] tags = MyClass.class.getAnnotationsByType(Tag.class);  // handles @Repeatable
boolean b  = MyClass.class.isAnnotationPresent(Beta.class);
Annotation[] all = MyClass.class.getDeclaredAnnotations();

// Method-level
for (Method m : clazz.getDeclaredMethods()) {
    RequiresRoles rr = m.getAnnotation(RequiresRoles.class);
    if (rr != null) { /* enforce roles */ }
}

// Field-level
for (Field f : clazz.getDeclaredFields()) {
    if (f.isAnnotationPresent(Inject.class)) {
        f.setAccessible(true);
        f.set(obj, resolveInstance(f.getType()));
    }
}

Typical framework patterns

Pattern How annotations help
Dependency injection @Inject on fields → framework sets values via reflection
Access control @RequiresRoles on methods → interceptor checks before call
Validation @NonNull, @Min on fields → validator reads and checks
ORM @Table, @Column on classes/fields → maps to DB schema
Test runners @Test, @BeforeEach on methods → JUnit discovers and calls

Summary

Concept Annotation / API
Define annotation @interface with @Retention + @Target
Available at runtime RetentionPolicy.RUNTIME
Multiple occurrences @Repeatable + container annotation
Subclass inheritance @Inherited (class-level only)
Compiler check @Override, @FunctionalInterface
Suppress warnings @SuppressWarnings("unchecked")
Read at runtime getAnnotation(), getAnnotationsByType(), getDeclaredAnnotations()
Field injection field.setAccessible(true); field.set(obj, value)