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
javaccompiler; - 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:
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:
class Animal {
public void speak() {}
}
class Dog extends Animal {
@Override
public void bark() {}
}method does not override or implement a method from a supertypeWhen 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:
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:
public class Client {
public static void main(String[] args) {
new OldTool().oldMethod();
}
}Compile with deprecation and removal warnings enabled:
javac --release 17 -encoding UTF-8 -Xlint:deprecation,removal Client.java OldTool.javaAnnotations 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:
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:
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:
| Value | Common Source |
|---|---|
unchecked | unchecked cast or generic varargs |
rawtypes | using raw generic types |
deprecation | calling deprecated APIs |
serial | Serializable classes lack a serial version field |
fallthrough | switch 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
@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
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:
@interfacedeclares 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;defaultallows users to omit this element.
Usage:
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
ElementType | Location |
|---|---|
TYPE | Classes, interfaces, enums, records, annotation types |
METHOD | Methods |
FIELD | Fields, enum constants |
PARAMETER | Method or constructor parameters |
CONSTRUCTOR | Constructors |
ANNOTATION_TYPE | Annotation types |
TYPE_PARAMETER | Generic type parameter declarations, such as <@Marker T> |
TYPE_USE | Locations where a type appears, such as List<@NonNull String> |
RECORD_COMPONENT | Record 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
| Strategy | Retained Until | Readable by Core Reflection |
|---|---|---|
SOURCE | Source code stage | No |
CLASS | Class file; default value | No |
RUNTIME | JVM runtime | Yes |
@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
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
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.
@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
chapter13_annotation_root=$(mktemp -d)
mkdir -p "$chapter13_annotation_root/out"Save the following code as $chapter13_annotation_root/AnnotationDemo.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");
}
}javac --release 17 -Xlint:all -Werror \
-d "$chapter13_annotation_root/out" \
"$chapter13_annotation_root/AnnotationDemo.java"
java -ea -cp "$chapter13_annotation_root/out" AnnotationDemoTarget: METHOD
Level: INFO
Roles: [reader, auditor]
SOURCE visible at runtime: false
Annotation validation passedrm -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
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@interface MethodOnly {}
@MethodOnly
class WrongTarget {}Expected Diagnosis Includes:
annotation type not applicable to this kind of declarationInvalid Element Type
import java.util.List;
@interface InvalidElement {
List<String> values();
}Expected Diagnosis Includes:
invalid type for annotation type elementCompile 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
| Phenomenon | Check First |
|---|---|
| Cannot read annotations at runtime | Is it RUNTIME; is the query element correct |
| Compiler says target is not applicable | @Target Does it include the current location |
| Repeated annotations only read one | Is getAnnotationsByType used |
| Subclass does not receive annotations | Is it a class annotation; is @Inherited present; is the source an interface |
| Default value cannot compile | Does it use a permitted constant, class literal, enum constant, annotation, or array initializer |
| Annotations are added but no behavior | Does 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
- JLS 9.6: Annotation Interfaces: Rules for element types, default values, and meta-annotations.
- JLS 9.7: Annotations: Syntax and applicability for standard, marker, single-element, and repeat annotations.
java.lang.annotation:Target,Retention,Inherited, andRepeatable.AnnotatedElement: Contracts for reading annotations at runtime, including direct, inherited, and repeat annotations.
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