Annotations provide metadata about program elements — classes, methods, fields, and parameters — without directly affecting program semantics. They are widely used by frameworks like Spring, JUnit, and JPA.

Built-in Annotations

Annotation Purpose
@Override Marks a method as overriding a superclass method
@Deprecated Marks an element as obsolete
@SuppressWarnings Suppresses compiler warnings
@FunctionalInterface Marks an interface as having exactly one abstract method
@SafeVarargs Asserts that a varargs method does not perform unsafe operations
  @Override
public String toString() {
    return "MyClass";
}

@Deprecated(since = "17", forRemoval = true)
public void oldMethod() { }
  

Defining Custom Annotations

  @Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Author {
    String name();
    String date() default "unknown";
}
  

Usage:

  @Author(name = "Alice", date = "2024-01-15")
public class Article {
    @Author(name = "Bob")
    public void publish() { }
}
  

Meta-Annotations

Annotations that apply to other annotations:

Meta-annotation Purpose
@Retention How long the annotation is kept (SOURCE, CLASS, RUNTIME)
@Target Where the annotation can be applied
@Documented Include in generated Javadoc
@Inherited Subclasses inherit the annotation
@Repeatable Allows multiple instances of the same annotation

Retention Policies

  • SOURCE — Discarded by the compiler (e.g., @Override)
  • CLASS — Stored in .class files but not available at runtime (default)
  • RUNTIME — Available at runtime via reflection (used by frameworks)

Reading Annotations at Runtime

  public class AnnotationReader {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Article.class;

        if (clazz.isAnnotationPresent(Author.class)) {
            Author author = clazz.getAnnotation(Author.class);
            System.out.println("Author: " + author.name());
        }

        for (Method method : clazz.getDeclaredMethods()) {
            if (method.isAnnotationPresent(Author.class)) {
                Author a = method.getAnnotation(Author.class);
                System.out.println(method.getName() + " by " + a.name());
            }
        }
    }
}
  

Marker, Single-Value, and Full Annotations

  // Marker — no elements
public @interface Test { }

// Single-value — shorthand when only one element named "value"
public @interface Version {
    int value();
}
@Version(2)  // equivalent to @Version(value = 2)

// Full — multiple elements
public @interface RequestMapping {
    String path();
    String method() default "GET";
}
  

Annotation Processing

At compile time, annotation processors (extending AbstractProcessor) can generate code or validate constraints. Frameworks like Lombok and MapStruct use this mechanism.

For most application developers, runtime reflection on @Retention(RUNTIME) annotations is the most common use case.

Best Practices

  • Use annotations to declare intent, not to replace good design
  • Prefer well-known framework annotations over inventing your own when possible
  • Always specify @Retention and @Target explicitly on custom annotations
  • Keep annotation elements simple — use primitives, String, Class, enums, or arrays of these