Annotations

Metadata to Add Information to Your Code

← Back to Index

What Are Annotations?

Think of annotations like sticky notes or labels you put on your code:

What Is an Annotation?

An annotation is metadata (data about data) that you attach to your code. It doesn't change what your code does by itself, but it gives information to:

  • The Java compiler (to check for errors)
  • Build tools (like Maven or Gradle)
  • Frameworks (like Spring or Hibernate)
  • Your own code (via Reflection)

Basic Syntax:

@AnnotationName
public class MyClass {
    @AnnotationName
    private String field;

    @AnnotationName
    public void method() { }
}

Annotations start with @ and can be placed on classes, methods, fields, parameters, etc.

Built-In Annotations (Java Provides)

1. @Override - "I'm replacing a parent method"

class Animal {
    public void makeSound() {
        System.out.println("Some sound");
    }
}

class Dog extends Animal {
    @Override  // Tells compiler: "I'm overriding makeSound()"
    public void makeSound() {
        System.out.println("Woof!");
    }

    // Typo in method name? Compiler catches it!
    @Override
    public void makeSoud() {  // ❌ ERROR: Method doesn't override anything
        System.out.println("Woof!");
    }
}

Why use it? The compiler verifies you're actually overriding. Catches typos and signature mistakes!

2. @Deprecated - "Don't use this anymore"

public class Calculator {
    @Deprecated  // Old method, don't use!
    public int add(int a, int b) {
        return a + b;
    }

    // New improved method
    public long addNumbers(long a, long b) {
        return a + b;
    }
}

// Usage
Calculator calc = new Calculator();
calc.add(5, 10);  // ⚠️ Warning: 'add(int, int)' is deprecated

What happens? Compiler shows warnings, but code still works. It's a gentle nudge to use the new method.

3. @SuppressWarnings - "I know what I'm doing"

@SuppressWarnings("unchecked")  // Hide unchecked warnings
public void oldCode() {
    List list = new ArrayList();  // Raw type - normally warns
    list.add("Hello");
}

@SuppressWarnings({"unused", "deprecation"})  // Multiple warnings
public void legacyMethod() {
    int unusedVariable = 10;
    Date date = new Date(2024, 1, 1);  // Deprecated constructor
}

Common values: "unchecked", "deprecation", "unused", "rawtypes"

4. @FunctionalInterface - "This interface has exactly one method"

@FunctionalInterface
public interface Calculator {
    int calculate(int a, int b);  // One abstract method

    // Can have default/static methods
    default void print(int result) {
        System.out.println("Result: " + result);
    }
}

// Use with lambda
Calculator add = (a, b) -> a + b;
System.out.println(add.calculate(5, 3));  // 8

Purpose: Ensures interface can be used with lambdas. Compiler errors if you add a second abstract method.

5. @SafeVarargs - "Trust me, varargs is safe"

@SafeVarargs  // Suppresses heap pollution warning
public static  void printAll(T... items) {
    for (T item : items) {
        System.out.println(item);
    }
}

printAll("Hello", "World", "!");  // Works fine

Creating Custom Annotations

You can create your own annotations! Here's how:

Example 1: Simple Annotation

// Define annotation
public @interface Author {
    String name();
    String date();
}

// Use annotation
@Author(name = "Alice", date = "2024-01-15")
public class MyClass {
    // class code
}

Example 2: Annotation with Default Values

public @interface Test {
    String name() default "Test";
    int timeout() default 5000;  // milliseconds
    boolean enabled() default true;
}

// Usage - can omit defaults
@Test
public void testMethod1() { }

@Test(name = "Custom Test", timeout = 10000)
public void testMethod2() { }

@Test(enabled = false)  // This test is disabled
public void testMethod3() { }

Example 3: Single-Value Annotation

public @interface Version {
    String value();  // Special name "value"
}

// Can use shorthand syntax
@Version("1.0")  // Instead of @Version(value = "1.0")
public class Product { }

Meta-Annotations (Annotations on Annotations)

These annotations control how your custom annotations behave:

1. @Retention - "How long should this annotation live?"

import java.lang.annotation.*;

// Lives only in source code (discarded after compilation)
@Retention(RetentionPolicy.SOURCE)
public @interface MySourceAnnotation { }

// Lives in .class file but not at runtime
@Retention(RetentionPolicy.CLASS)
public @interface MyClassAnnotation { }

// Lives at runtime (can read with Reflection)
@Retention(RetentionPolicy.RUNTIME)  // ✅ Most common!
public @interface MyRuntimeAnnotation { }

Retention Policies:

  • SOURCE: Only in .java files (e.g., @Override)
  • CLASS: In .class files but JVM discards it (default)
  • RUNTIME: Available at runtime via Reflection (most useful!)

2. @Target - "Where can this annotation be used?"

import java.lang.annotation.*;

// Can only be used on methods
@Target(ElementType.METHOD)
public @interface Test { }

// Can be used on classes and interfaces
@Target({ElementType.TYPE})
public @interface Entity { }

// Can be used on fields
@Target(ElementType.FIELD)
public @interface Column { }

// Can be used anywhere!
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
public @interface Custom { }

Common ElementTypes:

3. @Documented - "Show in JavaDoc"

@Documented  // Will appear in generated documentation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Important { }

4. @Inherited - "Subclasses inherit this"

@Inherited  // Subclasses automatically get this annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Auditable { }

@Auditable
class Parent { }

class Child extends Parent { }  // Child is also @Auditable!

Real-World Example: Custom Annotation

Let's create a complete example with annotation processing:

import java.lang.annotation.*;

// 1. Define annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Performance {
    String description() default "Performance test";
    int iterations() default 1000;
}

// 2. Use annotation
public class Calculator {
    @Performance(description = "Testing addition", iterations = 10000)
    public int add(int a, int b) {
        return a + b;
    }

    @Performance(iterations = 5000)
    public int multiply(int a, int b) {
        return a * b;
    }

    public int subtract(int a, int b) {
        return a - b;  // No annotation
    }
}

// 3. Process annotation with Reflection
import java.lang.reflect.Method;

public class PerformanceTester {
    public static void testClass(Class clazz) throws Exception {
        Object obj = clazz.getDeclaredConstructor().newInstance();

        for (Method method : clazz.getDeclaredMethods()) {
            if (method.isAnnotationPresent(Performance.class)) {
                Performance perf = method.getAnnotation(Performance.class);

                System.out.println("Testing: " + perf.description());
                System.out.println("Iterations: " + perf.iterations());

                long start = System.nanoTime();
                for (int i = 0; i < perf.iterations(); i++) {
                    method.invoke(obj, 5, 3);
                }
                long end = System.nanoTime();

                System.out.println("Time: " + (end - start) / 1_000_000 + " ms");
                System.out.println("---");
            }
        }
    }

    public static void main(String[] args) throws Exception {
        testClass(Calculator.class);
    }
}

Output:

Testing: Testing addition
Iterations: 10000
Time: 2 ms
---
Testing: Performance test
Iterations: 5000
Time: 1 ms
---

What happened?

  1. We created @Performance annotation with description and iterations
  2. We applied it to some methods (not all)
  3. We used Reflection to find methods with @Performance
  4. We read annotation values and ran performance tests
  5. Methods without @Performance were skipped

Framework Examples (How Others Use Annotations)

Spring Framework

@RestController  // This is a REST API controller
@RequestMapping("/api/users")
public class UserController {

    @Autowired  // Inject dependency automatically
    private UserService userService;

    @GetMapping("/{id}")  // Handle GET /api/users/123
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }

    @PostMapping  // Handle POST /api/users
    public User createUser(@RequestBody User user) {
        return userService.save(user);
    }
}

Spring reads these annotations and automatically configures routing, dependency injection, and more!

JUnit Testing

public class CalculatorTest {

    @BeforeEach  // Runs before each test
    public void setUp() {
        System.out.println("Setting up test...");
    }

    @Test  // This is a test method
    public void testAddition() {
        assertEquals(5, 2 + 3);
    }

    @Test
    @Disabled("Not ready yet")  // Skip this test
    public void testDivision() {
        assertEquals(2, 4 / 2);
    }

    @AfterEach  // Runs after each test
    public void tearDown() {
        System.out.println("Cleaning up...");
    }
}

Hibernate/JPA

@Entity  // This class maps to a database table
@Table(name = "users")
public class User {

    @Id  // Primary key
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "user_name", length = 50, nullable = false)
    private String username;

    @Column(unique = true)
    private String email;

    @OneToMany(mappedBy = "user")  // One user has many orders
    private List orders;

    // getters/setters
}

Hibernate reads these annotations and creates database tables, handles relationships, etc.

Lombok (Code Generation)

@Data  // Generates getters, setters, toString, equals, hashCode
@NoArgsConstructor  // Generates no-argument constructor
@AllArgsConstructor  // Generates constructor with all fields
public class Product {
    private Long id;
    private String name;
    private double price;
}

// You get ALL this for free (at compile time):
// - Getters: getId(), getName(), getPrice()
// - Setters: setId(), setName(), setPrice()
// - toString(), equals(), hashCode()
// - new Product() constructor
// - new Product(id, name, price) constructor

Reading Annotations with Reflection

Here's how to check and read annotations at runtime:

import java.lang.reflect.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@interface Info {
    String author();
    int version() default 1;
}

@Info(author = "Alice", version = 2)
class MyClass {
    @Info(author = "Bob")
    private String field;

    @Info(author = "Charlie", version = 3)
    public void method() { }
}

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

        // Check if class has annotation
        if (clazz.isAnnotationPresent(Info.class)) {
            Info info = clazz.getAnnotation(Info.class);
            System.out.println("Class author: " + info.author());
            System.out.println("Class version: " + info.version());
        }

        // Read field annotations
        Field field = clazz.getDeclaredField("field");
        if (field.isAnnotationPresent(Info.class)) {
            Info info = field.getAnnotation(Info.class);
            System.out.println("Field author: " + info.author());
        }

        // Read method annotations
        Method method = clazz.getMethod("method");
        Info info = method.getAnnotation(Info.class);
        System.out.println("Method author: " + info.author());
        System.out.println("Method version: " + info.version());

        // Get all annotations on a class
        Annotation[] annotations = clazz.getAnnotations();
        for (Annotation ann : annotations) {
            System.out.println("Found: " + ann);
        }
    }
}

Output:

Class author: Alice
Class version: 2
Field author: Bob
Method author: Charlie
Method version: 3
Found: @Info(author="Alice", version=2)

Key methods:

Repeating Annotations (Java 8+)

You can apply the same annotation multiple times:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(Schedules.class)  // Enable repeating
public @interface Schedule {
    String day();
    String time();
}

// Container annotation (required for @Repeatable)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Schedules {
    Schedule[] value();
}

// Usage - apply multiple times!
public class TaskRunner {
    @Schedule(day = "Monday", time = "9:00 AM")
    @Schedule(day = "Wednesday", time = "2:00 PM")
    @Schedule(day = "Friday", time = "4:00 PM")
    public void runBackup() {
        System.out.println("Running backup...");
    }
}

// Reading
Method method = TaskRunner.class.getMethod("runBackup");
Schedule[] schedules = method.getAnnotationsByType(Schedule.class);
for (Schedule s : schedules) {
    System.out.println(s.day() + " at " + s.time());
}

Output:

Monday at 9:00 AM
Wednesday at 2:00 PM
Friday at 4:00 PM

Annotation Processing (Compile-Time)

Advanced: Process annotations during compilation to generate code!

How It Works:

  1. You create an annotation processor (extends AbstractProcessor)
  2. During compilation, processor scans for annotations
  3. Processor generates new Java files or resources
  4. Generated code is compiled along with your code

This is how Lombok, MapStruct, and other code generators work!

// Simple example (actual processors are more complex)
@SupportedAnnotationTypes("com.example.Builder")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class BuilderProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set annotations,
                          RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(Builder.class)) {
            // Generate builder class code
            String builderCode = generateBuilder(element);
            // Write to file
            writeJavaFile(builderCode);
        }
        return true;
    }
}

Best Practices

✅ DO:

  • Use @Override always - Catches typos and signature mistakes
  • Make annotations RUNTIME if using Reflection - Otherwise can't read them
  • Provide default values - Makes annotations easier to use
  • Specify @Target precisely - Prevents misuse
  • Document your annotations - Explain what they do and how to use them
  • Keep annotations simple - Don't add too many parameters
  • Use meaningful names - @Test is better than @T

❌ DON'T:

  • Don't put logic in annotations - They're metadata, not code
  • Don't abuse @SuppressWarnings - Fix warnings instead of hiding them
  • Don't create annotations for everything - Only when truly needed
  • Don't forget @Retention - Default is CLASS (not RUNTIME!)
  • Don't make annotations too complex - Keep them simple and focused

Common Use Cases

1. Validation

public class User {
    @NotNull
    @Size(min = 3, max = 50)
    private String username;

    @Email
    private String email;

    @Min(18)
    @Max(150)
    private int age;
}

2. Dependency Injection

@Component
public class UserService {
    @Autowired
    private UserRepository repository;

    @PostConstruct
    public void init() {
        // Runs after construction
    }
}

3. Testing

@RunWith(MockitoJUnitRunner.class)
public class ServiceTest {
    @Mock
    private Repository repository;

    @InjectMocks
    private Service service;

    @Test
    public void testMethod() {
        // test code
    }
}

4. REST APIs

@RestController
public class API {
    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        return findUser(id);
    }

    @PostMapping("/users")
    @ResponseStatus(HttpStatus.CREATED)
    public User create(@RequestBody User user) {
        return save(user);
    }
}

Summary

  • Annotations are metadata (@Something) attached to code elements
  • Built-in: @Override, @Deprecated, @SuppressWarnings, @FunctionalInterface
  • Custom: Use public @interface Name { } to create your own
  • @Retention: Controls lifetime (SOURCE, CLASS, RUNTIME)
  • @Target: Controls where annotation can be used
  • Frameworks: Spring, Hibernate, JUnit use annotations extensively
  • Reflection: Read annotations at runtime with getAnnotation()
  • Processors: Can generate code at compile-time
  • Purpose: Reduce boilerplate, configure frameworks, validate code