Java Repeatable Annotations: Syntax and Usage
java repeatable annotation: Learn how to declare and read repeatable annotations in Java, including container annotation requirements and reflection usage.
In Java, an annotation type is normally limited to one occurrence per declaration. The java repeatable annotation feature, introduced in Java 8, allows the same annotation to appear multiple times on a single element. This is useful when a single element needs multiple values for the same annotation type, such as multiple test tags or multiple validation rules. Without repeatable annotations, developers had to resort to arrays of annotations or nested containers, which made the code verbose and harder to read.
Declaring a Repeatable Annotation
To make an annotation repeatable, you must first define the annotation itself and then define a container annotation that holds an array of the repeatable annotation. The repeatable annotation is annotated with @Repeatable, and the container annotation must have a value() method that returns an array of the repeatable annotation type.
Here is a minimal example:
import java.lang.annotation.Repeatable; @Repeatable(Tags.class) public @interface Tag { String value(); } public @interface Tags { Tag[] value(); }
In this example, @Tag is the repeatable annotation, and @Tags is the container. The @Repeatable annotation takes the container class as its argument. The container must be declared with a value() method that returns an array of the repeatable annotation type. Both annotations must have the same target and retention policy, or the compiler will reject the declaration.
Once declared, you can apply @Tag multiple times to the same element:
@Tag("api") @Tag("performance") public class Service { // ... }
Without the container, the compiler would complain about a duplicate annotation. With the container, the compiler automatically wraps the repeated annotations in an instance of @Tags.
Reading Repeatable Annotations via Reflection
When you access annotations at runtime, you have two options: getAnnotations() and getAnnotationsByType(). The former returns the container annotation as a single object, while the latter flattens the container and returns the individual repeatable annotations.
Consider the following code:
import java.lang.reflect.Method; public class AnnotationReader { public static void main(String[] args) throws Exception { Tag[] tags = Service.class.getAnnotationsByType(Tag.class); for (Tag tag : tags) { System.out.println(tag.value()); } } }
getAnnotationsByType() is the preferred method for reading repeatable annotations because it hides the container implementation. It works for both repeatable annotations and non-repeatable annotations, returning a single-element array when the annotation is not repeated. This method is available since Java 8 and is the standard way to retrieve repeated annotations.
If you use getAnnotation(Tag.class) directly, it will return null because the actual stored annotation is the container @Tags, not @Tag. To access the individual values, you must call getAnnotation(Tags.class) and then iterate over the array:
Tags container = Service.class.getAnnotation(Tags.class); if (container != null) { for (Tag tag : container.value()) { System.out.println(tag.value()); } }
This approach is more verbose and exposes the container type, which is an implementation detail. In most cases, getAnnotationsByType() is the better choice.
Common Mistakes with Container Annotations
Several pitfalls can cause compilation errors or unexpected runtime behavior when working with repeatable annotations.
One common mistake is forgetting to annotate the container with the same retention and target as the repeatable annotation. For example, if @Tag has @Retention(RetentionPolicy.RUNTIME) but @Tags has @Retention(RetentionPolicy.CLASS), the compiler will produce an error. Both must match exactly.
Another mistake is declaring the container's value() method with a return type that is not an array of the repeatable annotation. The method must be named value and return an array of the exact repeatable type. Any other signature will cause a compilation error.
A third issue is using the container annotation directly in code. You should never manually apply @Tags in your source code. The container is meant to be generated by the compiler when it encounters multiple @Tag annotations. If you manually use @Tags, you risk confusing the reflection logic and may end up with nested containers.
Finally, remember that the repeatable annotation and its container must be declared in the same compilation unit. If they are in different files, the compiler will not recognize the relationship, and you will get an error.
Runtime Behavior and Reflection Overhead
At runtime, the JVM stores repeated annotations as a single container annotation. This means that when you call getAnnotations(), you will see the container, not the individual annotations. The getAnnotationsByType() method performs the flattening internally, but it still has to read the container and then expand its array.
This indirection has a small performance cost. For most applications, the overhead is negligible because reflection is rarely a hot path. However, if you are scanning a large number of classes or methods, the repeated array allocation and iteration can add up. If performance is critical, consider caching the results of getAnnotationsByType() or using compile-time annotation processing instead of runtime reflection.
Another runtime consideration is that the order of repeated annotations is preserved. The JVM stores them in the order they appear in the source code, and getAnnotationsByType() returns them in that same order. This is useful when the order carries meaning, such as a sequence of validation steps.
Compatibility and Maintainability Considerations
Repeatable annotations were introduced in Java 8. If your codebase must run on older Java versions, you cannot use this feature directly. You would need to fall back to a single annotation with an array of values, or use a container annotation manually. When upgrading, be aware that libraries that consume annotations may not be updated to handle repeatable annotations, so you should test compatibility.
From a maintainability perspective, repeatable annotations make the source code cleaner when multiple instances of the same annotation are needed. For example, instead of writing:
@Tags({@Tag("api"), @Tag("performance")})
you can write:
@Tag("api") @Tag("performance")
The latter is more readable and easier to edit. However, if you need to handle a variable number of values that are only known at runtime, a single annotation with an array might be more appropriate because you can construct it programmatically.
When to Use Repeatable Annotations
Repeatable annotations are ideal when the number of instances is fixed at compile time and each instance carries a distinct value. Common use cases include:
- Multiple test tags or categories on a test method.
- Multiple validation rules on a field.
- Multiple security roles for an endpoint.
- Multiple configuration flags for a component.
They are less suitable when the values are dynamic or when you need to pass them through a configuration file. In those cases, a regular annotation with an array or a separate data structure is more flexible.
Another consideration is tooling support. Some annotation processors and frameworks may not handle repeatable annotations correctly, especially if they were written before Java 8. Before adopting repeatable annotations, verify that your build tools and libraries can process them.
When you do use them, always prefer getAnnotationsByType() over manual container handling. This keeps your code independent of the container implementation and makes it easier to change the annotation design later. If you ever need to remove repeatability, you can change the annotation to a single array without breaking the callers that use getAnnotationsByType().
Repeatable annotations are a language feature that simplifies metadata declaration. They are not a performance optimization, nor do they change the semantics of annotations. They are purely a syntactic convenience that reduces boilerplate and improves readability. By following the container requirements and using the right reflection methods, you can integrate them cleanly into your codebase.