Skip to content

VOL 01 / CH 13 / LESSON 01

13.1 Annotations: Labeling Code ​

After learning about method overriding, interfaces, and generic warnings, this lesson can be completed in about 35 minutes. You will define annotations with explicit targets and retention policies, and determine whether they are read by the compiler, tools, or runtime programs.

1. An Annotation Describes a Declaration but Does Not Change Its Behavior ​

Annotations are metadata. They can be attached to classes, methods, fields, parameters, or any usage point of a type. The annotation itself does not perform logging, validation, or transactional operations. The actual reader might be:

  • the javac compiler;
  • Annotation processors at compile time;
  • IDEs or static analysis tools;
  • Runtime libraries that use reflection or class metadata.

Therefore, when you see an annotation, ask two questions: "Who reads it?" and "When is it read?"

2. Standard Annotations ​

2.1 @Override Checks Your Overriding Intent ​

Success example:

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

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

@Override is read by the compiler. It requires that the method actually overrides a method from a superclass or implements a method from a superinterface.

Compile the code below separately, and it should fail:

java
class Animal {
    public void speak() {}
}

class Dog extends Animal {
    @Override
    public void bark() {}
}
text
method does not override or implement a method from a supertype

When you don't write @Override, legitimate overrides will still dynamically dispatch; the problem is that spelling errors or parameter errors may silently turn into new methods. Adding the annotation lets the compiler check your intent for you.

2.2 @Deprecated Record Migration Paths ​

Removing an API while clients still use it breaks their code. Mark it as deprecated and identify its replacement first:

java
final class OldTool {
    /**
     * @deprecated Since 2.0, use {@link #newMethod()}.
     */
    @Deprecated(since = "2.0", forRemoval = true)
    public void oldMethod() {
        System.out.println("Old method");
    }

    public void newMethod() {
        System.out.println("New method");
    }
}

since and forRemoval require Java 9+. Here, "2.0" is the example library version. forRemoval = true announces intended future removal; it specifies no date and does not prevent calls. Save the example as OldTool.java and add Client.java:

java
public class Client {
    public static void main(String[] args) {
        new OldTool().oldMethod();
    }
}

Compile with deprecation and removal warnings enabled:

bash
javac --release 17 -encoding UTF-8 -Xlint:deprecation,removal Client.java OldTool.java

Annotations let the compiler recognize deprecated APIs, and the @deprecated tag in Javadoc tells readers what to replace it with.

2.3 @SuppressWarnings Only Wrap Verified Boundaries ​

Raw types and unchecked casts may cause heap pollution. Before suppressing warnings, you must prove the boundary contract:

java
import java.util.List;

final class LegacyAdapter {
    private LegacyAdapter() {}

    @SuppressWarnings("unchecked")
    static List<String> asNames(Object legacyValue) {
        // The upstream protocol has verified that each element in the list is String.
        return (List<String>) legacyValue;
    }
}

The contract must also prevent later writes of other element types through shared references. If that cannot be guaranteed throughout use, validate and copy the elements. Add this method to LegacyAdapter; it rejects null and non-string elements and returns a list unaffected by later changes to the source:

java
static List<String> checkedNames(Object value) {
    if (!(value instanceof List<?> source)) {
        throw new IllegalArgumentException("Expected a list");
    }
    List<String> result = new java.util.ArrayList<>();
    for (Object element : source) {
        if (!(element instanceof String name)) {
            throw new IllegalArgumentException("Expected a string element");
        }
        result.add(name);
    }
    return List.copyOf(result);
}

Keep the narrow suppression only when the old API cannot be changed, copying is unsuitable, and the contract is reliable.

Common keys:

ValueCommon Source
uncheckedunchecked cast or generic varargs
rawtypesusing raw generic types
deprecationcalling deprecated APIs
serialSerializable classes lack a serial version field
fallthroughswitch fallthrough

Warning keys depend on the compiler. Do not assume @SuppressWarnings("all") works everywhere; it is also separate from the javac option -Xlint:all. Use specific keys and verify with your compiler that only the intended warnings disappear.

2.4 @FunctionalInterface and @SafeVarargs ​

java
@FunctionalInterface
interface BookFilter {
    boolean accept(String isbn);
}

The compiler will reject the second abstract method, making the lambda's target contract more robust. Public methods of Object are not counted toward the number of abstract methods in a functional interface.

@SafeVarargs indicates that the method author confirms that the implementation of generic varargs does not compromise type safety. The compiler cannot fully prove this promise. Array leaks, writing incorrect elements, or unsafe forwarding can still cause heap pollution.

3. Custom Annotations ​

3.1 Minimal Declaration ​

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
    String level() default "INFO";
    boolean includeArgs() default false;
}

Syntax breakdown:

  • @interface declares an annotation type;
  • @Target(METHOD) limits the methods that can be annotated;
  • @Retention(RUNTIME) allows core reflection to read it at runtime;
  • level() is an annotation element that must have a return type;
  • default allows users to omit this element.

Usage:

java
final class OrderService {
    @Loggable(level = "DEBUG")
    public void placeOrder(String userId, String itemId) {
        System.out.println("Ordering:" + userId + "/" + itemId);
    }

    @Loggable
    public void queryOrder(String orderId) {
        System.out.println("Query order:" + orderId);
    }
}

This code does not include automatic logging. The next lesson will cover readers.

3.2 @Target Quick Reference ​

ElementTypeLocation
TYPEClasses, interfaces, enums, records, annotation types
METHODMethods
FIELDFields, enum constants
PARAMETERMethod or constructor parameters
CONSTRUCTORConstructors
ANNOTATION_TYPEAnnotation types
TYPE_PARAMETERGeneric type parameter declarations, such as <@Marker T>
TYPE_USELocations where a type appears, such as List<@NonNull String>
RECORD_COMPONENTRecord components

Without @Target, an annotation is applicable to declarations other than type parameters; this does not enable arbitrary type-use positions. For framework annotations, specify the minimal target. Errors will appear earlier during compilation.

3.3 @Retention Quick Reference ​

StrategyRetained UntilReadable by Core Reflection
SOURCESource code stageNo
CLASSClass file; default valueNo
RUNTIMEJVM runtimeYes

@Override and @SuppressWarnings use SOURCE, because the compiler is sufficient. Annotations that need to be read at runtime typically use RUNTIME. CLASS metadata can still be read by bytecode tools, and it's not correct to write "reflection invisible" as "information disappears from the class file."

3.4 @Inherited Has a Very Narrow Scope ​

java
import java.lang.annotation.*;

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Audited {}

When subclasses call getAnnotation(Audited.class), they can search up the parent class. The following content will not be automatically inherited:

  • Annotations on parent class methods, fields, or parameters;
  • Class-level annotations on parent interfaces;
  • Constructor annotations.

An annotation is not copied to an overriding method. If the subclass does not override the method, however, getMethod may return the parent declaration itself, whose annotations remain readable. A framework that merges interface, method-level, or composed annotations must define its own search rules.

3.5 @Repeatable Use a Container to Store Repeated Values ​

java
import java.lang.annotation.*;

@Repeatable(Roles.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Role {
    String value();
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Roles {
    Role[] value();
}

@Role("reader")
@Role("auditor")
final class CatalogService {}

When reading repeated annotations, use getAnnotationsByType(Role.class), rather than call getAnnotation(Role.class) alone.

3.6 Element Type Restrictions ​

Legal element types include primitive types, String, Class, enums, annotations, and one-dimensional arrays of these types.

java
@interface AuthorInfo {
    String name();
    int version() default 1;
    Class<?> relatedType() default Void.class;
    String[] reviewers() default {};
}

You cannot use List<String>, any business objects, two-dimensional arrays, or null. Default values must be determinable at compile time.

4. Complete Experiment: Targets, Defaults, Duplicates, and Retention Policies ​

bash
chapter13_annotation_root=$(mktemp -d)
mkdir -p "$chapter13_annotation_root/out"

Save the following code as $chapter13_annotation_root/AnnotationDemo.java:

java
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.Arrays;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Loggable {
    String level() default "INFO";
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Roles.class)
@interface Role {
    String value();
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Roles {
    Role[] value();
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
@interface SourceMarker {}

@Role("reader")
@Role("auditor")
final class CatalogService {
    @Loggable
    @SourceMarker
    public void search() {}
}

public class AnnotationDemo {
    public static void main(String[] args) throws NoSuchMethodException {
        Target target = Loggable.class.getAnnotation(Target.class);
        Method method = CatalogService.class.getMethod("search");
        Loggable loggable = method.getAnnotation(Loggable.class);
        String[] roles = Arrays.stream(CatalogService.class.getAnnotationsByType(Role.class))
                .map(Role::value)
                .toArray(String[]::new);
        boolean sourceVisible = method.isAnnotationPresent(SourceMarker.class);

        System.out.println("Target: " + target.value()[0]);
        System.out.println("Level: " + loggable.level());
        System.out.println("Roles: " + Arrays.toString(roles));
        System.out.println("SOURCE visible at runtime: " + sourceVisible);

        if (!Arrays.equals(target.value(), new ElementType[]{ElementType.METHOD})
                || !"INFO".equals(loggable.level())) {
            throw new AssertionError("Target or default value mismatch");
        }
        if (!Arrays.equals(roles, new String[]{"reader", "auditor"})) {
            throw new AssertionError("Duplicate annotation order error");
        }
        if (sourceVisible) {
            throw new AssertionError("SOURCE annotations should not be visible at runtime");
        }
        System.out.println("Annotation validation passed");
    }
}
bash
javac --release 17 -Xlint:all -Werror \
  -d "$chapter13_annotation_root/out" \
  "$chapter13_annotation_root/AnnotationDemo.java"
java -ea -cp "$chapter13_annotation_root/out" AnnotationDemo
text
Target: METHOD
Level: INFO
Roles: [reader, auditor]
SOURCE visible at runtime: false
Annotation validation passed
bash
rm -rf -- "$chapter13_annotation_root"

This program reads the annotation via reflection. Remember the shape of the API for the next lesson when we break down Class and Method.

5. Compilation Failures Are Also Tests ​

Annotation Placed in Wrong Location ​

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

@Target(ElementType.METHOD)
@interface MethodOnly {}

@MethodOnly
class WrongTarget {}

Expected Diagnosis Includes:

text
annotation type not applicable to this kind of declaration

Invalid Element Type ​

java
import java.util.List;

@interface InvalidElement {
    List<String> values();
}

Expected Diagnosis Includes:

text
invalid type for annotation type element

Compile the failure cases into a separate file. Do not remove the negative experiments that demonstrate boundaries to make the tutorial "all green."

6. Common Roadblocks ​

PhenomenonCheck First
Cannot read annotations at runtimeIs it RUNTIME; is the query element correct
Compiler says target is not applicable@Target Does it include the current location
Repeated annotations only read oneIs getAnnotationsByType used
Subclass does not receive annotationsIs it a class annotation; is @Inherited present; is the source an interface
Default value cannot compileDoes it use a permitted constant, class literal, enum constant, annotation, or array initializer
Annotations are added but no behaviorDoes the project have real readers or processors

7. Let Readers Prove That Annotations Are Valid ​

First, define only annotations that can be applied to types, @Owner(name, reviewers), and retain them until runtime. Then write a reflection code snippet to read them. Next, define a repeatable @Permission, and verify that getAnnotationsByType can retrieve all permissions in the order they were declared.

Then, prepare three use cases for @Inherited: parent class, child class, and interface. The child class inherits class-level annotations from the parent, but this does not mean that interface or method annotations will automatically merge. Write the actual results as assertions, which is more accurate than saying "annotations can be inherited."

Finally, retain two separate compilation failures: one where the METHOD annotation is applied to a class, and another where the annotation element returns List<String>. Fix each by adjusting @Target and changing the element to a valid one-dimensional array. Use positive examples to validate the reading behavior and negative examples to validate the declaration boundary.

Reviewing the Official Rules for Java 17 ​

Next Step: Let the Program Read the Labels ​

The annotations are attached to declarations. The next step is to read that metadata and locate matching members under an explicit rule. Java provides those runtime inspection capabilities through the reflection API.

Continue learning about reflection: The Program Looks in the Mirror

Built with VitePress | Software Systems Atlas