Java Annotations Explained
With JSR-175, Java 5 gained a metadata facility, allowing us to annotate our code with decorative syntactic metadata.
“an·no·ta·tion | a-nə-ˈtā-shən
1: a note added by way of comment or explanation”
- Merriam-Webster
This metadata can be provided for types, fields, methods, parameters, constructors, local variables, type parameters, usage of types, and even other annotation types. It can be used at different steps of our code’s lifecycle by a wide arrangement of tools.
Table of Contents
Anatomy of Annotations
The basic definition of an annotation type is simple:
Let’s go through it line-by-line, everything will be explained in detail further down:
@Retention
- In which lifecycle of our code the annotation will be available.
@Target
- Where we can we use the annotation.
@Inherited
- If present, an annotated type will pass it on to any subtypes.
@Documented
- If present, documentation tools like
javadoc
can access it. @interface
- Marks an annotation type.
And the values of the annotation, optionally with a default value.
Basic usage
The simplest annotation use would be @MyAnnotation
at a compatible target site.
But annotations can have multiple values that might be required to be set if no default value is provided. The value name value()
is a special one. It can be used without a name if no other values are present.
@Retention
The typical lifecycle of our code is as follows:
The retention policy of annotations reflects these lifecycles and provides us with a way to specify the exact availability of metadata:
RetentionPolicy.SOURCE
Annotations are only available in the source. The compiler will discard the metadata, so neither the compiler nor runtime has access to it. This retention policy is useful for pre-compile tools, like annotation processors.RetentionPolicy.CLASS
The default retention policy. Annotations are visible to the compiler, and will be available in the class files, but not at runtime. Any post-compile byte-code tools might use the metadata.RetentionPolicy.RUNTIME
All metadata will be available at runtime.
Which retention policy we need for our custom annotations depends on our requirements.
The provided metadata might contain sensitive information on the inner workings of the annotated code. We should always choose the lowest retention possible for our code to still work.
@Target
Not every annotation makes sense on every available target. That’s why we can explicitly set the acceptable targets. The eight available targets are defined in java.lang.annotation.ElementType
:
ElementType.PACKAGE
Package declarations.ElementType.TYPE
Classes, interfaces, enum.ElementType.TYPE_PARAMETER
Generic type parameters. Available since Java 8.ElementType.TYPE_USE
Any usage of a type, like declarations, generic parameters, or casts. Available since Java 8.ElementType.ANNOTATION_TYPE
Annotation types.ElementType.CONSTRUCTOR
Constructor declaration.ElementType.FIELD
Fields and enum constants.ElementType.METHOD
Method declarations.ElementType.LOCAL_VARIABLE
Local variable declarations (not retained in class files or at runtime).
The @Target
annotation accepts an array of targets:
If @Target
is not specified, the annotation defaults to every available ElementType``, except
ElementType.TYPE_PARAMETER`.
@Inherited
Annotations are not inherited by default. By adding @Inherited
to an annotation type, we allow it to be inherited. This only applies to annotated type declarations, which will pass it down to their subtypes.
@Documented
Java default behavior for documentation is to ignore any annotation.
With @Documented
we can change this, making the metadata and its values accessible through documentation.
@Repeatable
Until Java 8, we could apply a specific annotation type only once on a target. With the help of the annotation @Repeatable
, we can now declare an annotation repeatable by providing an intermediate annotation:
Now we can use our annotation more than once:
Annotation Values
Being able to annotate our code and check if the annotation is present at different lifecycle events is great. But providing additional values besides the annotation type itself is even better. And even default values are supported.
Values are optional, separating annotations into two groups:
- Marker — No values.
The mere presence is the actual metadata.
Examples:
@Documented
,@Inherited
,@Override
. - Configuration — Values present, maybe with default values for less typing when used.
Examples:
@Target
,@Retention
.
The Java Language Specification (JLS) splits Configuration into Normal Annotation and Single Element Annotation. But in my opinion, the behavior of those two overlaps enough to be treated as (almost) equal.
Configuration annotations support multiple values. The allowed types are defined in the JLS 9.6.1:
- Primitive types
String
- The type
Class
orClass<T>
- Enum types
- Annotation types
- Array of any preceding type (single-dimension only)
Arrays are handled uniquely. If only a single value is provided when used, we can omit the curly braces.
Default values must be constant expressions, although null
is not acceptable. Arrays can return an empty array by using {}
as their default value.
Built-In Annotations
The JDK includes multiple annotations besides the ones we already encountered for creating annotation types themselves:
@Override
Indicates that a method overrides/replaces an inherited method. This information is not strictly necessary, but it helps to reduce mistakes. If we want to override a method but have a simple type in the signature, or the wrong argument type, that error might go unnoticed. But if we provide an@Override
annotation, the compiler makes sure we actually override a method, and not just accidentally add or overload it.@Deprecated
Another compile-only annotation. We can mark code as deprecated, and the compiler/IDE can access this information to tell us the code isn’t supposed to be used anymore. Since Java 9, this previous marker annotation becomes a configuration annotation. The valuesString since() default ""
andboolean forRemoval() default false
were added to provide even more info for compilers and IDE to work with.@FunctionalInterface
Since Java 8, we can mark interfaces to be single abstract method interfaces (SAM), so they can be used as lambdas. This marker annotation allows the compiler to ensure that an interface has precisely one single abstract method. f we add another abstract method, our code will no longer compile. This annotation enables the compiler-check, but isn’t strictly necessary. Any SAM is automatically a functional interface.@SafeVarargs
Another “trust me, I’m an engineer” marker annotation. Tells the compiler that we won’t do any unsafe operation when using varargs.@SuppressWarnings
A configuration annotation, accepts an array of warning names that should be disabled during compilation.
How to Access Annotations at Runtime
Adding metadata isn’t enough.
We also need to access it somehow.
Thanks to reflection, we can access it via the class
-object.
Check for annotation
Access metadata
Equivalent to boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
, we also have methods for accessing the actual annotation instance, providing us with access to its values.
Here are some of the methods available to different targets:
<A extends Annotation> A getAnnotation(Class<A> annotationClass)
Returns a specific annotation, if present, otherwisenull
.Annotation[] getAnnotations()
Returns all annotations on a given type.<A extends Annotation> A[] getAnnotationsByType(Class<A> annotationClass)
Returns all annotations of a given annotation type.
<T extends Annotation> T getAnnotation(Class<T> annotationClass
)
Returns a specific annotation, if present, otherwisenull
.Annotation[] getDeclaredAnnotations()
Returns all annotations on the method.Annotation[][] getParameterAnnotations()
Returns a two-dimensional array, containing the parameter annotations, in declaration order.
Use Cases
An excellent use case is Serialization. With annotations, a lot of additional metadata can be provided on how our data structures should be processed.
Jackson, a JSON Serialization framework, uses the @JsonProperty
annotation to provide every information necessary to modify the default Serialization process:
Another excellent use case is how [RESTEasy](https://resteasy.github.io/ uses annotations to describe REST endpoints, so no additional config is needed elsewhere:
This way, RestEASY can perform routing (@Path
), validates allowed HTTP methods (@POST
and @HEAD
), provides data extracted from the request (@FormParam
and @HeaderParam
), and uses a defined media type for the response (@Produces
).
All without any additional config-file or objects. The configuration is right there in the corresponding code.
Conclusion
Annotations are a great way to provide additional data, either for ourselves or third-party tools and libraries. But be aware of the additional costs of parsing, compiling, and lookup of annotations, especially at runtime.
Resources
- The Java Tutorials - Annotations (Oracle)
java.lang.annotation
package summary (JavaSE 8)- Creating a Custom Annotation (Baeldung)