Index

Annotations Syntax

Java Syntax

16.1 Built-in Annotations (@Override, @Deprecated, etc.)

Java annotations are metadata that provide information to the compiler or runtime environment. Built-in annotations serve as flags to communicate intent, affect compilation behavior, or help IDEs provide warnings or suggestions. This section focuses on three of the most widely used built-in annotations: @Override, @Deprecated, and @SuppressWarnings.

@Override

The @Override annotation indicates that a method is intended to override a method declared in a superclass or implement an interface method.

Example:

class Animal {
    void speak() {
        System.out.println("Animal speaks");
    }
}

class Dog extends Animal {
    @Override
    void speak() {
        System.out.println("Dog barks");
    }
}

If you accidentally misspell the method name or signature, the compiler will flag an error:

class Cat extends Animal {
    @Override
    void speek() { // Error: Method does not override a superclass method
        System.out.println("Cat meows");
    }
}

Why use @Override?

@Deprecated

The @Deprecated annotation marks a method, field, or class as outdated and signals that it should no longer be used.

Example:

class Calculator {
    @Deprecated
    int add(int a, int b) {
        return a + b;
    }

    int sum(int... numbers) {
        int result = 0;
        for (int num : numbers) {
            result += num;
        }
        return result;
    }
}

When add() is used, the compiler will issue a warning:

Calculator calc = new Calculator();
int result = calc.add(3, 4); // Warning: add(int, int) is deprecated

Enhancing with @Deprecated Javadoc:

/**
 * @deprecated Use {@link #sum(int...)} instead.
 */
@Deprecated
int add(int a, int b) {
    return a + b;
}

Why use @Deprecated?

@SuppressWarnings

The @SuppressWarnings annotation tells the compiler to ignore specific warnings for a code block, method, or class. It's useful for avoiding unnecessary or known warnings in special cases.

Common Values:

Example:

@SuppressWarnings("unchecked")
void rawListUsage() {
    List list = new ArrayList(); // Raw type warning suppressed
    list.add("Hello");
}

You can apply it to methods, classes, or local variables:

@SuppressWarnings({"unchecked", "deprecation"})
void mixedUsage() {
    List list = new ArrayList();
    list.add("Hello");

    Calculator calc = new Calculator();
    int result = calc.add(2, 3); // Deprecated method, warning suppressed
}

Why use @SuppressWarnings?

Reflection: How Built-in Annotations Improve Java Code

Built-in annotations serve three primary purposes:

  1. Correctness: @Override helps prevent bugs by ensuring methods are overridden as intended.

  2. Maintainability and API Evolution: @Deprecated signals safe migration paths and supports progressive changes without breaking legacy code.

  3. Control over Compiler Feedback: @SuppressWarnings allows developers to quiet compiler warnings selectively, enabling focus on real issues.

These annotations are not merely decorative—they guide the compiler, enhance documentation, and clarify design choices. IDEs like IntelliJ IDEA and Eclipse leverage annotations to provide better code suggestions, autocomplete, and error detection, making development faster and more robust.

Click to view full runnable Code

import java.util.ArrayList;
import java.util.List;

class Animal {
    void speak() {
        System.out.println("Animal speaks");
    }
}

class Dog extends Animal {
    @Override
    void speak() {
        System.out.println("Dog barks");
    }
}

class Calculator {
    /**
     * @deprecated Use {@link #sum(int...)} instead.
     */
    @Deprecated
    int add(int a, int b) {
        return a + b;
    }

    int sum(int... numbers) {
        int result = 0;
        for (int n : numbers) {
            result += n;
        }
        return result;
    }
}

public class Main {
    @SuppressWarnings({"unchecked", "deprecation"})
    public static void main(String[] args) {
        // @Override demonstration
        Animal a = new Dog();
        a.speak(); // Dog barks

        // @Deprecated usage
        Calculator calc = new Calculator();
        int result = calc.add(3, 4); // Deprecated method
        System.out.println("Deprecated add() result: " + result);

        // @SuppressWarnings usage
        List list = new ArrayList(); // Raw type
        list.add("Unchecked warning suppressed");

        System.out.println("Raw list first element: " + list.get(0));
    }
}

Summary

Annotation Purpose Typical Usage
@Override Ensure method correctly overrides parent Method overriding in subclasses
@Deprecated Warn about outdated or unsafe features Mark old methods or classes
@SuppressWarnings Suppress known compiler warnings Raw types, deprecated use, etc.

Using these annotations is not just good practice—it's essential for writing clear, maintainable, and forward-compatible Java code. As you develop more complex systems, understanding and applying annotations effectively will help you build more reliable software.

Index

16.2 Defining Custom Annotations

In Java, annotations are not limited to built-in ones like @Override or @Deprecated. Developers can define their own annotations to describe metadata for classes, methods, fields, parameters, and more. These custom annotations enable meta-programming, allowing tools, libraries, and frameworks to behave dynamically based on annotated elements.

Declaring a Custom Annotation

A custom annotation is defined using the @interface keyword. The syntax resembles that of an interface, but it does not contain methods in the traditional sense—rather, it defines elements that act like configuration parameters.

Basic Syntax:

public @interface MyAnnotation {
    String value(); // an element with no default value
}

This annotation can be applied like so:

@MyAnnotation("Hello")
public class Greeting {
    // ...
}

In this case, the value element is specified directly. If the annotation has only one element named value, Java allows shorthand usage by omitting the name.

Multiple Elements and Default Values

Annotations can include multiple elements, and elements can have default values.

Example:

public @interface Author {
    String name();
    String date();
    int version() default 1; // optional element with default
}

Usage:

@Author(name = "Jane Doe", date = "2025-06-20")
public class Document {
    // ...
}

Here, version is omitted because it uses the default value of 1. You can include it explicitly if needed:

@Author(name = "Jane Doe", date = "2025-06-20", version = 2)

Supported Element Types

The types allowed in annotation elements are limited:

Invalid types like List<String> or Map<String, String> are not permitted.

Custom Annotation Example: Validation Metadata

Suppose you're building a validation tool and want to annotate fields that must not be null.

public @interface NotNull {
    String message() default "This field cannot be null.";
}

You can use this annotation on fields:

public class User {
    @NotNull
    private String name;

    @NotNull(message = "Email is required.")
    private String email;
}

Although Java itself won't enforce this constraint, you can write a tool or framework to read the annotation using reflection and perform validation.

Reading Custom Annotations with Reflection

You can retrieve annotation metadata at runtime using the java.lang.reflect API.

Example:

import java.lang.reflect.Field;

public class Validator {
    public static void validate(Object obj) throws Exception {
        Class<?> clazz = obj.getClass();
        for (Field field : clazz.getDeclaredFields()) {
            if (field.isAnnotationPresent(NotNull.class)) {
                field.setAccessible(true);
                Object value = field.get(obj);
                if (value == null) {
                    NotNull annotation = field.getAnnotation(NotNull.class);
                    throw new Exception(annotation.message());
                }
            }
        }
    }
}
Click to view full runnable Code

import java.lang.annotation.*;
import java.lang.reflect.Field;

// Define the custom annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface NotNull {
    String message() default "This field cannot be null.";
}

// Class using the annotation
class User {
    @NotNull
    private String name;

    @NotNull(message = "Email is required.")
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
}

// Validator that uses reflection to check @NotNull fields
class Validator {
    public static void validate(Object obj) throws Exception {
        Class<?> clazz = obj.getClass();
        for (Field field : clazz.getDeclaredFields()) {
            if (field.isAnnotationPresent(NotNull.class)) {
                field.setAccessible(true);
                Object value = field.get(obj);
                if (value == null) {
                    NotNull annotation = field.getAnnotation(NotNull.class);
                    throw new Exception(annotation.message());
                }
            }
        }
    }
}

// Main class to test it all
public class CustomAnnotationDemo {
    public static void main(String[] args) {
        try {
            User user1 = new User("Alice", "alice@example.com");
            Validator.validate(user1);  // Should pass

            User user2 = new User("Bob", null);
            Validator.validate(user2);  // Should throw an exception
        } catch (Exception e) {
            System.out.println("Validation error: " + e.getMessage());
        }
    }
}

Why Use Custom Annotations?

Custom annotations are a key enabler of declarative programming—where developers express what should happen, not how.

Benefits:

Real-World Use Cases

All of these are implemented as custom annotations that are processed at compile-time or runtime.

Summary Table

Feature Description
@interface Declares a new annotation type
Elements Configuration values (must return specific supported types)
Default values Optional elements can have default values
Usage Apply to class, method, field, etc. using @YourAnnotation
Accessing annotations Use reflection (Class.getDeclaredFields(), etc.)

Final Reflection

Custom annotations are a powerful design tool in Java. While they don't enforce behavior on their own, they enable you to express intent declaratively and build tooling around it. This is especially powerful in large systems or frameworks, where consistent behavior can be encoded through simple annotations rather than boilerplate logic.

By learning how to define and apply your own annotations, you open the door to building cleaner, more expressive APIs and participating in the modern Java ecosystem of annotation-driven development.

Index

16.3 Applying Annotations

In Java, annotations provide metadata that describe how a program should behave, rather than implementing the behavior directly. Applying annotations to various elements—classes, methods, fields, parameters, local variables—makes code more expressive, declarative, and tool-friendly. This section explores how annotations are applied and interpreted in different contexts.

Applying Annotations: Basic Syntax

Annotations are placed directly above the element they apply to, using the @AnnotationName syntax.

Example 1: Annotating a Class

@Deprecated
public class OldService {
    // ...
}

Here, @Deprecated marks the entire class as outdated. IDEs and compilers will usually show a warning when this class is used.

Annotating Methods

Annotations are commonly used with methods, especially for overriding, testing, and lifecycle control.

Example 2: Using @Override on a method

class Animal {
    void makeSound() {
        System.out.println("Generic animal sound");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Woof!");
    }
}

The @Override annotation instructs the compiler to ensure that makeSound() actually overrides a method from the superclass. If it doesn't, a compile-time error occurs.

Annotating Fields

Field annotations are often used in frameworks like Spring, Hibernate, or validation libraries.

Example 3: Applying an annotation to a field

public class User {
    @NotNull
    private String username;

    @Size(min = 8)
    private String password;
}

These annotations act as metadata. Tools can use them to automatically validate the field values at runtime.

Annotating Parameters

Method parameters can also be annotated, often for validation or documentation purposes.

Example 4: Annotating a parameter

public void registerUser(@NotNull String email) {
    // ...
}

Here, a tool or library could detect the @NotNull annotation and reject null values automatically.

Annotating Local Variables

Since Java 8, even local variables can be annotated.

Example 5: Local variable annotation

public void process() {
    @SuppressWarnings("unused")
    int temp = 42;
}

This usage is less common but can be helpful for controlling compiler behavior in specific scopes.

Multiple Annotations on a Single Element

You can apply more than one annotation to a single element by stacking them.

Example 6: Multiple annotations

@Deprecated
@SuppressWarnings("unchecked")
public void legacyMethod() {
    // ...
}

The order of annotations generally doesn't matter, unless one annotation depends on another (rare). Some annotations, like @Repeatable ones, can be applied multiple times in certain ways (covered later).

Annotation Placement: Quick Reference

Target Example
Class @Deprecated class Foo {}
Method @Override void bar() {}
Field @NotNull String name;
Parameter void setName(@NotNull String name)
Local Variable @SuppressWarnings String s = "";

Annotation with Parameters

Annotations can include values to customize their behavior.

Example 7: Passing values

@Test(timeout = 1000)
public void testPerformance() {
    // ...
}

Here, the @Test annotation from JUnit includes a timeout parameter.

Click to view full runnable Code

import java.util.ArrayList;
import java.util.List;

// Custom stubs to simulate behavior
@interface NotNull {}
@interface Size {
    int min();
}

public class AnnotationDemo {

    @Deprecated
    static class OldService {
        void doSomething() {
            System.out.println("OldService logic");
        }
    }

    static class Animal {
        void makeSound() {
            System.out.println("Generic animal sound");
        }
    }

    static class Dog extends Animal {
        @Override
        void makeSound() {
            System.out.println("Woof!");
        }
    }

    static class User {
        @NotNull
        String username;

        @Size(min = 8)
        String password;
    }

    public void registerUser(@NotNull String email) {
        System.out.println("Registering: " + email);
    }

    public void process() {
        @SuppressWarnings("unused")
        int temp = 42;
    }

    @Deprecated
    @SuppressWarnings("unchecked")
    public void legacyMethod() {
        List list = new ArrayList(); // raw type
        list.add("legacy");
        System.out.println("Legacy method ran: " + list.get(0));
    }

    public static void main(String[] args) {
        OldService old = new OldService(); // Will show deprecation warning
        old.doSomething();

        Animal a = new Dog();
        a.makeSound();

        AnnotationDemo demo = new AnnotationDemo();
        demo.registerUser("user@example.com");
        demo.legacyMethod();
    }
}

Annotations and Declarative Programming

Annotations shift the focus from how to do something to what should be done. Instead of manually wiring services or validating input, annotations allow developers to declare their intent.

Benefits:

Final Reflection

Applying annotations correctly is a foundational skill in modern Java development. From basic compiler hints (@Override) to complex framework configurations (@Autowired, @Entity), annotations make your code more readable, robust, and framework-friendly.

When used thoughtfully, annotations cleanly encode metadata that improves program structure and behavior—without cluttering logic with extra code. This is especially powerful in large projects, where declarative style can lead to cleaner APIs and reduced boilerplate.

Index

16.4 Retention and Target Policies

Annotations in Java are powerful tools that enhance code with metadata. However, for them to be truly useful, Java provides two important meta-annotations from the java.lang.annotation package: @Retention and @Target. These meta-annotations define how annotations behave—specifically, where they can be applied and how long they are retained in the compiled code.

@Retention: Controlling Annotation Lifetime

The @Retention annotation specifies how long an annotation is retained in the program. It takes a value from the RetentionPolicy enum:

RetentionPolicy Options:

  1. SOURCE

    • The annotation exists only in the source code.
    • It is discarded by the compiler and not included in the .class file.
    • Common use case: documentation tools like @SuppressWarnings.
  2. CLASS

    • The annotation is stored in the compiled .class file but is not available at runtime via reflection.
    • This is the default retention policy if none is specified.
  3. RUNTIME

    • The annotation is retained in the .class file and available at runtime through reflection.
    • Required for annotations used by frameworks or libraries that analyze classes dynamically (e.g., Spring, JUnit).

Example: Defining a Runtime-Retained Annotation

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecution {
}

This @LogExecution annotation will be available during runtime, which is necessary if we want to scan classes with reflection to detect its presence.

@Target: Controlling Where Annotations Can Be Applied

The @Target meta-annotation limits where your custom annotation can legally appear. It uses constants from the ElementType enum.

Common ElementType Values:

Example: Annotation for Methods Only

import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Audit {
}

In this example, @Audit can only be applied to methods. Trying to use it on a class or a field would cause a compilation error.

Combining @Retention and @Target

Typically, you'll specify both @Retention and @Target when defining a custom annotation, especially if you expect it to be used by frameworks or tooling.

Example: Logging Annotation for Methods

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log {
    String value() default "INFO";
}

This annotation can now be used like this:

public class UserService {

    @Log("DEBUG")
    public void createUser() {
        // Business logic
    }
}

And later, a framework can reflectively inspect this annotation:

Method m = UserService.class.getMethod("createUser");
if (m.isAnnotationPresent(Log.class)) {
    Log log = m.getAnnotation(Log.class);
    System.out.println("Log level: " + log.value());
}

Multiple Targets: Allowing More Flexibility

You can allow annotations on multiple types by passing an array to @Target.

@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
public @interface Timed {
}

This allows @Timed to be applied to both methods and constructors.

Click to view full runnable Code

import java.lang.annotation.*;
import java.lang.reflect.Method;

// Define the annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Log {
    String value() default "INFO";
}

// Use the annotation
class UserService {

    @Log("DEBUG")
    public void createUser() {
        System.out.println("User created.");
    }

    @Log
    public void deleteUser() {
        System.out.println("User deleted.");
    }

    public void updateUser() {
        System.out.println("User updated.");
    }
}

// Main class to inspect and use the annotation
public class AnnotationDemo {
    public static void main(String[] args) throws Exception {
        UserService service = new UserService();
        Method[] methods = UserService.class.getDeclaredMethods();

        for (Method m : methods) {
            if (m.isAnnotationPresent(Log.class)) {
                Log log = m.getAnnotation(Log.class);
                System.out.println("Method: " + m.getName() + ", Log Level: " + log.value());
                m.invoke(service); // Call the method
            }
        }
    }
}

Reflection and RetentionPolicy.RUNTIME

If you plan to use reflection to detect annotations at runtime—like in dependency injection frameworks, testing tools, or ORM systems—you must declare the annotation with RetentionPolicy.RUNTIME. Otherwise, the JVM will not retain the annotation, and your reflection code won't find it.

Final Reflection

The @Retention and @Target meta-annotations are not just technical details—they are essential to the proper use and design of custom annotations. They give you:

Used wisely, these meta-annotations enforce good design patterns and ensure that your annotations are applied in meaningful, predictable ways. Whether you're building a lightweight library, a testing tool, or a full-fledged framework, these policies help manage annotation behavior efficiently—keeping your metadata robust, safe, and appropriately scoped.

Index