Java Annotations: A Comprehensive Guide
A focused guide to Java Annotations. Definition, creation, and usage through practical examples. Covers built-in annotations, custom annotation design, reflection techniques, and best practices.
Introduction to Java Annotations
Java Annotations are metadata tags that provide additional information about a program. They start with @, followed by the annotation name, and can be placed above:
- Classes
- Interfaces
- Methods
- Method parameters
- Fields
- Local variables
Built-in Java Annotations
Java provides several built-in annotations:
@Override: Indicates that a method is intended to override a method in a superclass@Deprecated: Marks code as obsolete or no longer recommended@SuppressWarnings: Suppresses specific compiler warnings@Contended: Used for preventing false sharing in concurrent programming
Purpose of Creating Custom Annotations
Custom annotations serve multiple purposes:
- Passive Documentation: Provide additional context and metadata about code
- Source Code Processing: Supply input for Java source code processors
- Runtime Reflection: Enable runtime access to annotation information via Java Reflection
Annotation Types and Purposes
Java Annotations can be used for three main types of instructions:
- Compiler Instructions: Provide guidance to the compiler
- Build-time Instructions: Assist in build processes and code generation
- Runtime Instructions: Enable runtime behavior modification
Creating a Custom Annotation
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PerformanceLog {
String value() default "";
int sampleRate() default 100;
}
The two meta-annotations doing the work:
@Retention(RUNTIME): keep this annotation in the class file and make it available via reflection at runtime@Target(METHOD): restrict this annotation to method declarations only
Using Reflection to Read Annotations
Method method = service.getClass().getMethod("processOrder");
PerformanceLog log = method.getAnnotation(PerformanceLog.class);
if (log != null) {
System.out.printf("Sampling at %d%% (tag: %s)%n",
log.sampleRate(), log.value());
}
This is the bridge between declarative metadata and runtime behaviour. Frameworks like Spring, Jackson, JUnit, and JPA are largely built on top of this pattern.
Best Practices
- Prefer composition over inheritance for behaviour. Annotations are great for declaring the behaviour.
- Don’t overuse runtime reflection. It’s slower than direct calls. Cache the annotation lookups when used in hot paths.
- Document custom annotations like APIs. Others will use them based on the contract, not the implementation.
- Avoid magical annotations that hide major side effects. If an
@TransactionalAndAlsoSendsEmailannotation exists, split it.
When Annotations Are the Wrong Tool
If the metadata changes at runtime, you don’t want annotations. You want configuration. Annotations are compile-time constants. Use them for static facts about code; use config/DI for behaviour that needs to vary by environment.