Back to Blog
Java

Java Annotation Syntax: Declaring and Using Annotations

java annotation syntax: Learn the exact syntax for declaring and using Java annotations, including retention, target, and reflection-based processing.

JavaAnnotationsReflectionMetadataSyntax
Diagram showing Java annotation syntax elements such as @interface, retention, target, and reflection processing.

Java annotations are a form of metadata that you attach to code elements such as classes, methods, fields, and parameters. The java annotation syntax is compact but has several moving parts: the @interface declaration, annotation elements, retention policies, and target restrictions. This article walks through each part with concrete examples and explains how the syntax behaves at compile time and runtime.

Declaring an Annotation Type

An annotation type is declared with the @interface keyword. The name follows the same rules as a class name, and the body contains element declarations that look like method signatures without parameters or throws clauses.

public @interface Author { String name(); String date() default "unknown"; }

Here name() is a required element, and date() has a default value. When you use the annotation, you must supply values for all elements that do not have defaults. The syntax for supplying values is element = value, separated by commas.

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

If an annotation has only one element, you can omit the element name and use the value directly, but only if the element is named value. This is a common convention for annotations that take a single configuration parameter.

public @interface Version { String value(); } @Version("1.0") public class Release { }

The @interface declaration itself is compiled like a normal interface, but it implicitly extends java.lang.annotation.Annotation. You cannot explicitly extend another annotation type.

Using Annotations on Code Elements

Annotations can be placed before the declaration of a type, method, field, parameter, local variable, package, or module, depending on the @Target meta-annotation. Without a @Target, the annotation is applicable to most declaration contexts, but not to type parameters or type uses.

@Target(ElementType.METHOD) public @interface LogExecution { }

With this target, the annotation can only be used on methods. Trying to use it on a class produces a compile-time error. The ElementType enum defines the valid targets: TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE, TYPE_PARAMETER, TYPE_USE, and MODULE.

For example, to allow an annotation on both classes and methods, you pass an array:

@Target({ElementType.TYPE, ElementType.METHOD}) public @interface Auditable { }

The syntax for using an annotation is straightforward: place @AnnotationName before the declaration. If the annotation has elements, include them in parentheses. For example:

@Auditable public class OrderService { @Auditable public void placeOrder(Order order) { } }

Annotations with no elements are called marker annotations. They are often used to trigger processing by a framework or a compile-time tool.

Configuring Retention and Target

Retention controls how long the annotation is available. The @Retention meta-annotation takes a RetentionPolicy value: SOURCE, CLASS, or RUNTIME.

  • SOURCE: discarded by the compiler, not present in the class file.
  • CLASS: stored in the class file but not visible at runtime via reflection. This is the default.
  • RUNTIME: stored in the class file and visible via reflection.
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface JsonField { String name() default ""; }

If you need to read an annotation at runtime with reflection, the retention must be RUNTIME. Many frameworks such as Spring and Jackson rely on runtime-retained annotations.

Target and retention are themselves annotations, so they are declared with @interface and applied with the usual syntax. The combination of @Retention and @Target determines where and when your annotation is visible.

Annotation Elements and Default Values

Annotation elements can have primitive types, String, Class, enums, other annotations, or arrays of these types. They cannot be generic types or arbitrary objects. The return type of the element method defines the allowed type.

public enum Priority { LOW, MEDIUM, HIGH } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface TaskInfo { Priority priority() default Priority.LOW; String assignee() default "unassigned"; Class<?> handler() default Object.class; String[] tags() default {}; }

When using the annotation, you can provide values for some elements and rely on defaults for the rest. Array elements are supplied with brace syntax:

@TaskInfo(priority = Priority.HIGH, tags = {"backend", "urgent"}) public void processPayment() { }

The default value must be a compile-time constant. For a Class element, the default is typically a class literal. For an enum, it must be one of the enum constants. For an array, you use the empty array syntax {}.

Processing Annotations with Reflection

Runtime-retained annotations can be read using the reflection API. The java.lang.reflect.AnnotatedElement interface provides methods such as getAnnotation, getAnnotations, and isAnnotationPresent. Classes, methods, fields, and other declaration types implement this interface.

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RateLimit { int maxRequests() default 10; } public class ApiEndpoint { @RateLimit(maxRequests = 5) public void fetchData() { } }

To read this annotation at runtime:

Method method = ApiEndpoint.class.getMethod("fetchData"); if (method.isAnnotationPresent(RateLimit.class)) { RateLimit limit = method.getAnnotation(RateLimit.class); System.out.println("Max requests: " + limit.maxRequests()); }

The annotation instance returned by getAnnotation is a proxy that implements the annotation interface. Calling the element methods returns the values you supplied, or the defaults if you omitted them.

Reflection-based processing is common in frameworks that need to inspect classes without knowing them at compile time. However, reflection has a runtime cost, so you should cache the results of annotation lookups when the same class is processed repeatedly.

Common Pitfalls in Annotation Syntax

One frequent mistake is forgetting to set @Retention(RetentionPolicy.RUNTIME) and then wondering why getAnnotation returns null. The default retention is CLASS, which is not visible at runtime.

Another issue is using an annotation on a target that was not declared. For example, if @Target(ElementType.FIELD) is set, placing the annotation on a method causes a compile error. The error message is clear, but developers often overlook the target when reusing an annotation from a library.

Element types are also a source of confusion. You cannot use a wrapper type like Integer as an element type; you must use int. Similarly, you cannot use Object or a custom class as an element type. If you need a more complex value, consider using an enum or a Class reference.

Another subtlety is that annotations themselves cannot be extended. There is no inheritance for annotation types. If you need to reuse a set of elements, you must duplicate them or create a new annotation that contains the original annotation as an element.

Runtime Cost and Maintainability Considerations

Reading annotations via reflection is not free. Each call to getAnnotation may involve class loading and proxy creation. If you process a large number of objects, the overhead can become noticeable. A common pattern is to read the annotation once and store the extracted metadata in a cache.

private static final Map<Method, RateLimit> CACHE = new ConcurrentHashMap<>(); public static RateLimit getRateLimit(Method method) { return CACHE.computeIfAbsent(method, m -> m.getAnnotation(RateLimit.class)); }

This avoids repeated reflection calls for the same method. The same principle applies when annotations are used to define configuration or routing metadata: parse them once at startup rather than on every request.

Maintainability also depends on how you use annotations. Because annotations are compile-time constants, you cannot change their values at runtime. If you need dynamic behavior, use a configuration file or a database instead. Overusing annotations for things that are not static metadata can make the codebase harder to reason about, especially when the annotation processing logic is spread across multiple classes.

Another operational concern is compatibility. The @Target and @Retention meta-annotations were introduced in Java 5 and have been stable since. However, newer ElementType constants like MODULE and TYPE_USE require a recent Java version. If your library targets older Java versions, avoid using those targets.

Finally, remember that annotation processing tools like javac's -processor option and the javax.annotation.processing API operate on source or class files. They do not see runtime annotations unless the retention is CLASS or RUNTIME. If you are writing an annotation processor, you must consider the retention policy to decide whether the annotation is available during compilation or only at runtime.

java annotation syntax: Practical Usage and Code Examples | RYUSLOG DEV