Built-in annotations like @Override and @Deprecated cover a narrow slice of what annotations can do. The real power shows up with custom annotations. A custom annotation marks methods, fields, parameters, or whole classes with information that frameworks, build tools, or your own code can read at compile time or at runtime. This chapter covers the mechanics: how to declare an annotation with @interface, what kinds of elements it can have, how default values work, what marker and single-element annotations look like, and how the five meta-annotations (@Target, @Retention, @Documented, @Inherited, @Repeatable) shape what an annotation can do.
@interfaceA custom annotation is declared with the @interface keyword. The keyword reads like a typo of interface, and that is not an accident. Annotations are a special kind of interface internally, which is why their elements look like abstract methods and why they can be queried through reflection in a way that resembles interface inspection.
The smallest possible annotation has no elements and carries no data; the name itself is the payload:
You apply it like any other annotation:
The program runs the same as if @Audit were not there. By default, an annotation with no extra configuration is a tag in the source code. It compiles into the class file, but no one reads it unless code is written that does. An annotation is metadata, and metadata is only useful when something consumes it.
The two pieces to understand: what goes inside the @interface declaration, and what goes on top of the @interface declaration (the meta-annotations). The first determines the data the annotation carries. The second determines how the annotation behaves.
An annotation can declare elements, which look like methods but behave like fields. Each element has a name, a return type, and an optional default value.
This says "every @ProductCategory annotation must carry one piece of data, a String, accessible under the name value." When applying the annotation, supply that data:
The element looks like value() because annotations are interfaces, and an element's getter is declared the same way as an abstract method. But syntactically, when applying the annotation, write value = "ELECTRONICS", not value() = "ELECTRONICS". The parentheses come back when something reads the annotation through reflection.
Not every Java type is allowed as an annotation element. The compiler restricts the set to types whose values are known at compile time and can be safely embedded in the class file. The allowed types are:
| Type category | Examples |
|---|---|
| Primitives | int, long, double, boolean, byte, short, float, char |
String | String name() |
Class | Class<?> targetType(), Class<? extends Validator> |
| Enums | OrderStatus status() where OrderStatus is an enum |
| Annotation types | @Min min() (another annotation) |
| Arrays of the above | String[] tags(), int[] discountTiers() |
That is the entire list. A List<String> element is not allowed, an arbitrary object element is not allowed, and a Map is not allowed. For multiple values, use an array.
Apply it like this:
A few details. Array values use brace syntax: {"a", "b"}. The formatter element accepts a Class literal, written as SomeType.class. The element values must all be compile-time constants. basePrice = computePrice() or tags = loadTagsFromConfig() are not allowed, because the value has to be baked into the class file at compile time.
The diagram captures the entire universe of legal element types. Anything outside that set is a compile error. For a List or Map, switch to an array, or model the structure as a nested annotation.
Annotation elements can have a default value, which makes them optional when applying the annotation.
With defaults in place, all three forms below are valid:
The first uses all three defaults. The second overrides only max and keeps the defaults for min and currency. The third overrides everything explicitly. If an element has no default and no value is supplied, the code does not compile.
The default value follows the same compile-time-constant rule as the value passed at the use site. default 0.0 is fine, default System.currentTimeMillis() is not. For arrays, the default uses brace syntax: String[] tags() default {}; for an empty array, or String[] tags() default {"default"}; for a one-element default.
A common convention: if an annotation has a few optional elements with sensible defaults, give them all defaults so the annotation can be applied with no arguments at all. That makes the API cleaner for the common case while still allowing customization when needed.
value() ShorthandIf an annotation has exactly one element and that element is named value, value = can be omitted when applying it. This is a syntactic shortcut, but it appears often.
Both Laptop and LaptopExplicit carry the same annotation. The shorthand only works when:
value.value = ... along with the other names.If value is the only element, shorthand works. If value is the only element supplied but the annotation has other optional elements, shorthand still works. But the moment a second element is supplied, the explicit form is required for all elements.
The shorthand is only available when the element's actual name is value. The discount annotation above declares percent, not value, so the shorthand does not apply. Rename the element to value to enable the abbreviated form, or use the explicit syntax.
A common example: @SuppressWarnings("unchecked"). The full form is @SuppressWarnings(value = {"unchecked"}), but because there is a value element of type String[], both value = and (when there is only one entry) the braces can be dropped.
When the element is an array and a single value is supplied, the braces are also optional. So @SuppressWarnings("unchecked") is the same as @SuppressWarnings(value = {"unchecked"}). Both shortcuts stack.
A marker annotation is one with no elements. It exists to tag something, with no extra data attached.
Apply it without parentheses, or with empty parentheses:
Both @Audit and @Audit() mean the same thing. The bare form without parentheses reads cleaner.
Why use a marker? The meaning is in the name. @Audit says "this method should be audited"; the consumer (an aspect-oriented framework, an annotation processor, a manual scan) reads the marker and acts. @Override, @FunctionalInterface, and JUnit's @Test are all markers in spirit, though @Test has optional elements with defaults so it is only a marker in practice when written bare.
If a marker annotation grows over time, elements can be added as long as they get defaults. Existing usages keep working because they fall back to the defaults. This is a common evolution path: start with a marker, add a value() later when a use case appears.
Arrays show up in two places: as an element type in the annotation declaration, and as the value passed when applying the annotation. A few syntactic details:
Any allowed element type can be made into an array.
The default for an array can be empty ({}), a single-element array ({"json"}), or a multi-element array ({"json", "xml", "csv"}).
When applying the annotation, array values use brace syntax:
For a single-element array, the braces are optional:
Both are legal and produce the same annotation. The compiler implicitly wraps the single value in a one-element array. This is a small ergonomic win when an annotation usually takes one tag but technically supports multiple.
An array element can hold other annotations. This is the pattern that powers things like Bean Validation's group-of-constraints style.
When applying @Validate, the array elements are themselves annotation usages:
A more compact pattern, often used for constraint groups, is to put the inner annotations directly in the outer one as a single combined array:
Because value() is the element name and there is only one element, the shorthand kicks in and the inner annotations go directly inside the parentheses, separated by commas. This is the syntactic pattern that @Repeatable automates.
Each annotation usage adds entries to the class file's constant pool and attribute table. Annotations are cheap individually, but on a class with hundreds of methods each carrying half a dozen annotations, the class file grows measurably. For library authors, this matters; for application code, it almost never does.
A meta-annotation is an annotation applied to another annotation declaration. The Java standard library defines five of them in java.lang.annotation, and together they control where the annotation can be used, how long it survives, whether it shows up in Javadoc, whether it is inherited by subclasses, and whether it can be applied more than once to the same element.
The diagram shows how the five meta-annotations attach to one custom annotation. Each one shapes a different dimension of the annotation's behavior. The rest of this section walks through each in turn.
@Target: Restricting Where the Annotation Applies@Target tells the compiler what kinds of program elements the annotation can be placed on. Without @Target, the annotation can go almost anywhere, which is usually not desired. A category annotation belongs on classes, not on local variables or parameters. A validation annotation belongs on fields and methods, not on package declarations.
The argument to @Target is one or more values from the ElementType enum:
ElementType | What it allows |
|---|---|
TYPE | Classes, interfaces, enums, annotation types |
FIELD | Instance and static fields, enum constants |
METHOD | Methods (not constructors) |
CONSTRUCTOR | Constructors |
PARAMETER | Method and constructor parameters |
LOCAL_VARIABLE | Local variables inside methods |
ANNOTATION_TYPE | Other annotation declarations (meta-annotations) |
PACKAGE | Package declarations (in package-info.java) |
TYPE_PARAMETER | Generic type parameters (Java 8+) |
TYPE_USE | Any use of a type (Java 8+) |
MODULE | Module declarations (Java 9+) |
RECORD_COMPONENT | Record components (Java 16+) |
A category annotation restricted to types:
Putting @ProductCategory on a method makes the compiler reject it:
For multiple targets, pass an array:
@MaxLength can now be applied to fields, methods, and parameters, but nothing else. A class-level usage would fail to compile.
The single-value form @Target(ElementType.TYPE) is also a shorthand for @Target({ElementType.TYPE}). Both work.
Choosing the wrong target set is a long-term maintenance trap. Too permissive (no @Target at all) means users will put the annotation in unexpected places, which leaks into annotation-processing logic. Too restrictive means a target has to be added later, which is a binary-compatible but source-incompatible change for callers who relied on the broader set. Pick the smallest reasonable set, and add to it deliberately.
@Retention: How Long the Annotation Lives@Retention says how long the annotation should be retained: in source code only, in the class file but not at runtime, or in the class file and visible to runtime reflection. The argument is one of three RetentionPolicy values.
RetentionPolicy | Where the annotation lives |
|---|---|
SOURCE | In the .java file only. Discarded by the compiler. |
CLASS | In the .class file. Not loaded into the JVM at runtime. (This is the default when no retention is specified.) |
RUNTIME | In the .class file and loaded by the JVM. Visible to reflection. |
The default is CLASS, which surprises readers who assume it is RUNTIME. Without @Retention, attempting to read the annotation reflectively returns nothing.
A category annotation read by reflection (for a custom inventory scanner, for example) needs RUNTIME:
An annotation that exists only to feed the compiler or an IDE (like @Override, which is SOURCE) does not need to live in the class file at all. An annotation read by an annotation processor only needs SOURCE or CLASS, depending on whether the processor runs against source or bytecode.
Rule of thumb: to read the annotation at runtime through reflection, use RUNTIME. Otherwise the default CLASS is fine.
@Documented: Include in JavadocBy default, annotations do not appear in the generated Javadoc of the elements they annotate. With @ProductCategory("ELECTRONICS") on a class and javadoc run, the annotation is not shown on the class's documentation page.
@Documented flips that. With @Documented on the annotation declaration, Javadoc includes it in the generated documentation for any element it is applied to.
This matters when the annotation is part of a public API. A @Deprecated method should show that in the docs. A category annotation that helps consumers of a library understand which products belong to which group should also be documented. An internal marker annotation used only by an annotation processor at build time probably should not be, because cluttering Javadoc with internal mechanics confuses readers.
@Documented has no elements; it is a marker meta-annotation. Either include it or do not.
@Inherited: Subclasses Inherit the AnnotationBy default, applying an annotation to a class does not automatically apply it to subclasses. With @Audit on Product and reflection on DigitalProduct extends Product, the reflection call for @Audit on DigitalProduct returns null.
@Inherited changes that for class-level annotations. When the annotation is present on a class and a subclass is queried for it, the subclass reports the annotation as if it had been directly applied.
Without @Inherited on the @Audit declaration, the second line would print false. With it, the subclass inherits the annotation.
Three caveats:
@Inherited only applies to class-level annotations. Method annotations are never inherited this way; reflection has its own (more complex) story for method-level annotations and interfaces.@Inherited is useful for marker-style annotations that describe a category of class (like @Audit for "audit every operation on this class and its subclasses"). It is less useful for value-carrying annotations where each subclass might want its own value.
@Repeatable: Applying an Annotation Multiple TimesBefore Java 8, the same annotation could not be applied more than once to the same element. For multiple values, an array was required. That worked, but the syntax was clunky: @Discounts({@Discount(10), @Discount(20)}) instead of the more natural @Discount(10) @Discount(20).
Java 8 introduced @Repeatable to make the natural form work. The mechanism is compile-time sugar: declare a container annotation that holds an array of the repeatable annotation, mark the repeatable one with @Repeatable(Container.class), and the compiler automatically wraps multiple usages into the container.
The full pattern for a @Discount annotation that can apply multiple times to a product:
Three things are happening:
Discounts is the container annotation. It has a single element named value of type Discount[]. The element must be named value and must be an array of the repeatable annotation. This is a strict requirement enforced by the compiler.Discount is the repeatable annotation. Its @Repeatable(Discounts.class) meta-annotation tells the compiler "if someone writes @Discount more than once on the same element, wrap them all into a @Discounts."@Target and @Retention must be at least as permissive as the repeatable annotation's. If @Discount is RUNTIME retention, the container must also be RUNTIME. If @Discount targets TYPE, the container must include TYPE.With the pair declared, @Discount can now be applied multiple times:
The reflective method getAnnotationsByType(Discount.class) understands the container pattern. It returns the array of Discount instances, automatically unwrapping the Discounts container if present. The older getAnnotation(Discount.class) returns null (because there is not a single @Discount on the class); the caller would have to call getAnnotation(Discounts.class) and walk its value() array. The point: the compiler-generated container is real, and old code that does not know about @Repeatable can still find it as a regular nested annotation.
The diagram traces the lifecycle. Multiple @Discount usages are written, the compiler sees the @Repeatable meta-annotation and wraps them in @Discounts, the class file holds the container, and reflection can unwrap it transparently.
The container can still be applied directly:
Applying the container directly and applying the repeatable annotation twice produce the same final class file structure. Pick whichever reads better in context. The repeated form is usually cleaner for two or three occurrences; the explicit container is better for a long list, especially when the annotation is generated from another tool.
A meta-annotation is itself an annotation, and the same rules apply to it. @Target is declared with @Target(ElementType.ANNOTATION_TYPE), @Retention with @Retention(RetentionPolicy.RUNTIME), and so on. The chain is visible in the JDK source for @Override:
@Override is a marker annotation (no elements). It targets methods. Its retention is SOURCE, meaning the compiler uses it for checks and then discards it; nothing about @Override survives into the class file. This is why reflection on a method will never report whether the developer wrote @Override on it.
Compare to @SuppressWarnings:
It targets almost everywhere a developer might want to suppress a warning. Its retention is also SOURCE, because the warning suppression is a compiler-time concern. And it has a String[] value() element, which is why @SuppressWarnings("unchecked") works (using both the value shorthand and the single-element array shorthand).
The pattern is consistent. Every annotation in the JDK uses meta-annotations to declare its own behavior. A custom annotation does the same: stacking the relevant meta-annotations and filling in the options.
A complete example that combines everything in this chapter: a @Reviewable annotation that marks a class as supporting reviews, with multiple @Rating annotations attached to capture different review buckets, and a @PriceRange annotation on the class to record the expected price band.
This single file uses every pattern in the chapter:
@Reviewable is a documented, runtime-visible marker annotation targeting types.@Ratings is the container annotation for @Rating. It has the required value() element of type Rating[].@Rating is repeatable (via @Repeatable(Ratings.class)) and has two elements with no defaults.@PriceRange has three elements, all with defaults, so it can be applied bare or with any subset overridden.main method reads the annotations through reflection. The key calls are isAnnotationPresent for the marker, getAnnotation for a single annotation, and getAnnotationsByType for the repeatable one.The example also shows the natural division of labor. The annotation declarations describe what data can be attached and where. The applications of those annotations describe a specific class. The reflection code consumes the metadata at runtime. None of those three layers cares about the others' implementation; they communicate through the annotation type.
10 quizzes