Back to Blog
Java

Java Annotation Declaration: Syntax and Usage

java annotation declaration: Learn how to declare custom annotations in Java, set retention and target, define elements, and process them with reflection.

JavaAnnotationsReflectionCustom AnnotationsAnnotation Processing
Diagram showing a Java annotation declaration with retention and target attributes.

When you write @Override or @Deprecated, you are using a built-in annotation. The same mechanism that defines those annotations is available to you: you can declare your own annotation types. A java annotation declaration is a special interface that starts with @interface, and it becomes part of the type system. Once declared, the annotation can be applied to classes, methods, fields, parameters, and other program elements, depending on its @Target. This article explains how to write a custom annotation, configure its lifecycle, define elements, and read it at runtime with reflection.

The Basic Syntax of an Annotation Declaration

An annotation type is declared with the @interface keyword. The name follows the same conventions as a class or interface name, and the body contains method declarations that represent annotation elements. Here is a minimal declaration:

public @interface Author { String name(); }

This declares an annotation named Author with a single element name. To use it, you supply a value for that element:

@Author(name = "Ada Lovelace") public class AnalyticalEngine { }

If an element has a default value, you can omit it at the use site. The annotation declaration itself does not contain any logic; it only defines the shape of the metadata. The actual behavior comes from code that reads the annotation, typically through reflection or an annotation processor.

Setting Retention and Target

Two meta-annotations control how the annotation behaves: @Retention and @Target. @Retention determines how long the annotation is available. @Target restricts where the annotation can be applied.

The @Retention policy takes one of three values from java.lang.annotation.RetentionPolicy:

PolicyAvailability
SOURCEDiscarded by the compiler; not present in bytecode.
CLASSRecorded in the class file but not loaded into the JVM at runtime.
RUNTIMEPresent in the class file and readable via reflection.

If you need to read the annotation at runtime, you must set RUNTIME. For compile-time processing with an annotation processor, SOURCE or CLASS is sufficient and avoids runtime overhead.

The @Target meta-annotation accepts an array of ElementType constants. Common targets include TYPE (classes and interfaces), METHOD, FIELD, PARAMETER, and CONSTRUCTOR. If you do not specify @Target, the annotation can be applied to any element except a type parameter. Restricting the target makes the annotation's purpose explicit and prevents misuse. For example, an annotation intended for methods should not be accidentally placed on a field.

Defining Elements and Default Values

Annotation elements are declared as methods without parameters or throws clauses. The return type must be a primitive type, String, Class, an enum, another annotation, or an array of any of these. You cannot use a Double object or a custom class as an element type.

import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RequiresPermission { String value(); int level() default 1; String[] roles() default {}; }

Here value() is a special element name. When an annotation has a single element named value, you can omit the element name at the use site:

@RequiresPermission("admin") public void deleteUser() { }

This is equivalent to @RequiresPermission(value = "admin", level = 1, roles = {}). Default values are evaluated at compile time and must be constant expressions. Arrays are written with braces, and a single-element array can be written without braces in the annotation use.

Reading Annotations with Reflection

To make an annotation useful at runtime, you need to retrieve it. The java.lang.reflect.AnnotatedElement interface provides methods such as getAnnotation, getAnnotationsByType, and isAnnotationPresent. Here is a complete example:

import java.lang.annotation.*; import java.lang.reflect.Method; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface RequiresPermission { String value(); } public class Service { @RequiresPermission("admin") public void deleteUser() { } } public class Main { public static void main(String[] args) throws Exception { Method method = Service.class.getMethod("deleteUser"); RequiresPermission permission = method.getAnnotation(RequiresPermission.class); if (permission != null) { System.out.println("Required permission: " + permission.value()); } } }

The annotation is only visible if its retention policy is RUNTIME. If you forget @Retention(RetentionPolicy.RUNTIME), getAnnotation returns null. This is a common source of confusion when developers first test custom annotations.

Common Pitfalls and Runtime Behavior

Several details of annotation declarations cause subtle bugs. The @Target restriction is enforced at compile time; placing the annotation on an invalid element produces a compile error. However, if you use an annotation without @Target, it can appear on any element, which can make the code harder to reason about.

Another pitfall is element type restrictions. You cannot use Integer or List<String> as an element type. The compiler enforces this, so you will see an error during compilation. For more complex data, you must encode it as a String or an array of primitives.

When reading annotations, remember that getAnnotation returns null if the annotation is not present. If the annotation is repeatable, you need getAnnotationsByType. Repeatable annotations require a container annotation, which adds complexity. For most use cases, a single annotation is sufficient.

Maintainability and Compatibility Considerations

Declaring a custom annotation creates a public API contract. Changing the annotation's elements later can break code that uses it. Adding a new element with a default value is backward-compatible, but removing an element or changing its type is not. If the annotation is part of a library, treat it with the same care as any other public interface.

Retention policy also affects operational behavior. RUNTIME annotations are visible to reflection, which can be useful for frameworks that inspect classes at startup. However, they add a small amount of metadata to the class file and require reflection calls to read. For compile-time processing, CLASS or SOURCE avoids that runtime lookup entirely. Choose the retention policy based on when the annotation must be consumed, not on a default.

Finally, consider whether an annotation is the right tool. Annotations are metadata, not behavior. If the logic depends on the annotation's presence, the code that reads it must be tested separately. Keeping the annotation declaration simple and its processing logic separate improves maintainability.

java annotation declaration: Practical Usage and Code Exampl | RYUSLOG DEV