What Are Annotations?
Think of annotations like sticky notes or labels you put on your code:
- 📝 A sticky note on a method: "@Override - Remember to implement this properly!"
- ⚠️ A warning label: "@Deprecated - Don't use this anymore!"
- 🏷️ A tag on a class: "@Entity - This is a database table!"
What Is an Annotation?
An annotation is metadata (data about data) that you attach to your code. It doesn't change what your code does by itself, but it gives information to:
- The Java compiler (to check for errors)
- Build tools (like Maven or Gradle)
- Frameworks (like Spring or Hibernate)
- Your own code (via Reflection)
Basic Syntax:
@AnnotationName
public class MyClass {
@AnnotationName
private String field;
@AnnotationName
public void method() { }
}
Annotations start with @ and can be placed on classes, methods, fields, parameters, etc.
Built-In Annotations (Java Provides)
1. @Override - "I'm replacing a parent method"
class Animal {
public void makeSound() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
@Override // Tells compiler: "I'm overriding makeSound()"
public void makeSound() {
System.out.println("Woof!");
}
// Typo in method name? Compiler catches it!
@Override
public void makeSoud() { // ❌ ERROR: Method doesn't override anything
System.out.println("Woof!");
}
}
Why use it? The compiler verifies you're actually overriding. Catches typos and signature mistakes!
2. @Deprecated - "Don't use this anymore"
public class Calculator {
@Deprecated // Old method, don't use!
public int add(int a, int b) {
return a + b;
}
// New improved method
public long addNumbers(long a, long b) {
return a + b;
}
}
// Usage
Calculator calc = new Calculator();
calc.add(5, 10); // ⚠️ Warning: 'add(int, int)' is deprecated
What happens? Compiler shows warnings, but code still works. It's a gentle nudge to use the new method.
3. @SuppressWarnings - "I know what I'm doing"
@SuppressWarnings("unchecked") // Hide unchecked warnings
public void oldCode() {
List list = new ArrayList(); // Raw type - normally warns
list.add("Hello");
}
@SuppressWarnings({"unused", "deprecation"}) // Multiple warnings
public void legacyMethod() {
int unusedVariable = 10;
Date date = new Date(2024, 1, 1); // Deprecated constructor
}
Common values: "unchecked", "deprecation", "unused", "rawtypes"
4. @FunctionalInterface - "This interface has exactly one method"
@FunctionalInterface
public interface Calculator {
int calculate(int a, int b); // One abstract method
// Can have default/static methods
default void print(int result) {
System.out.println("Result: " + result);
}
}
// Use with lambda
Calculator add = (a, b) -> a + b;
System.out.println(add.calculate(5, 3)); // 8
Purpose: Ensures interface can be used with lambdas. Compiler errors if you add a second abstract method.
5. @SafeVarargs - "Trust me, varargs is safe"
@SafeVarargs // Suppresses heap pollution warning
public static void printAll(T... items) {
for (T item : items) {
System.out.println(item);
}
}
printAll("Hello", "World", "!"); // Works fine
Creating Custom Annotations
You can create your own annotations! Here's how:
Example 1: Simple Annotation
// Define annotation
public @interface Author {
String name();
String date();
}
// Use annotation
@Author(name = "Alice", date = "2024-01-15")
public class MyClass {
// class code
}
Example 2: Annotation with Default Values
public @interface Test {
String name() default "Test";
int timeout() default 5000; // milliseconds
boolean enabled() default true;
}
// Usage - can omit defaults
@Test
public void testMethod1() { }
@Test(name = "Custom Test", timeout = 10000)
public void testMethod2() { }
@Test(enabled = false) // This test is disabled
public void testMethod3() { }
Example 3: Single-Value Annotation
public @interface Version {
String value(); // Special name "value"
}
// Can use shorthand syntax
@Version("1.0") // Instead of @Version(value = "1.0")
public class Product { }
Meta-Annotations (Annotations on Annotations)
These annotations control how your custom annotations behave:
1. @Retention - "How long should this annotation live?"
import java.lang.annotation.*;
// Lives only in source code (discarded after compilation)
@Retention(RetentionPolicy.SOURCE)
public @interface MySourceAnnotation { }
// Lives in .class file but not at runtime
@Retention(RetentionPolicy.CLASS)
public @interface MyClassAnnotation { }
// Lives at runtime (can read with Reflection)
@Retention(RetentionPolicy.RUNTIME) // ✅ Most common!
public @interface MyRuntimeAnnotation { }
Retention Policies:
- SOURCE: Only in .java files (e.g., @Override)
- CLASS: In .class files but JVM discards it (default)
- RUNTIME: Available at runtime via Reflection (most useful!)
2. @Target - "Where can this annotation be used?"
import java.lang.annotation.*;
// Can only be used on methods
@Target(ElementType.METHOD)
public @interface Test { }
// Can be used on classes and interfaces
@Target({ElementType.TYPE})
public @interface Entity { }
// Can be used on fields
@Target(ElementType.FIELD)
public @interface Column { }
// Can be used anywhere!
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
public @interface Custom { }
Common ElementTypes:
TYPE- Classes, interfaces, enumsMETHOD- MethodsFIELD- Fields/attributesPARAMETER- Method parametersCONSTRUCTOR- ConstructorsLOCAL_VARIABLE- Local variablesANNOTATION_TYPE- Other annotations
3. @Documented - "Show in JavaDoc"
@Documented // Will appear in generated documentation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Important { }
4. @Inherited - "Subclasses inherit this"
@Inherited // Subclasses automatically get this annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Auditable { }
@Auditable
class Parent { }
class Child extends Parent { } // Child is also @Auditable!
Real-World Example: Custom Annotation
Let's create a complete example with annotation processing:
import java.lang.annotation.*;
// 1. Define annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Performance {
String description() default "Performance test";
int iterations() default 1000;
}
// 2. Use annotation
public class Calculator {
@Performance(description = "Testing addition", iterations = 10000)
public int add(int a, int b) {
return a + b;
}
@Performance(iterations = 5000)
public int multiply(int a, int b) {
return a * b;
}
public int subtract(int a, int b) {
return a - b; // No annotation
}
}
// 3. Process annotation with Reflection
import java.lang.reflect.Method;
public class PerformanceTester {
public static void testClass(Class> clazz) throws Exception {
Object obj = clazz.getDeclaredConstructor().newInstance();
for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(Performance.class)) {
Performance perf = method.getAnnotation(Performance.class);
System.out.println("Testing: " + perf.description());
System.out.println("Iterations: " + perf.iterations());
long start = System.nanoTime();
for (int i = 0; i < perf.iterations(); i++) {
method.invoke(obj, 5, 3);
}
long end = System.nanoTime();
System.out.println("Time: " + (end - start) / 1_000_000 + " ms");
System.out.println("---");
}
}
}
public static void main(String[] args) throws Exception {
testClass(Calculator.class);
}
}
Output:
Testing: Testing addition
Iterations: 10000
Time: 2 ms
---
Testing: Performance test
Iterations: 5000
Time: 1 ms
---
What happened?
- We created @Performance annotation with description and iterations
- We applied it to some methods (not all)
- We used Reflection to find methods with @Performance
- We read annotation values and ran performance tests
- Methods without @Performance were skipped
Framework Examples (How Others Use Annotations)
Spring Framework
@RestController // This is a REST API controller
@RequestMapping("/api/users")
public class UserController {
@Autowired // Inject dependency automatically
private UserService userService;
@GetMapping("/{id}") // Handle GET /api/users/123
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
@PostMapping // Handle POST /api/users
public User createUser(@RequestBody User user) {
return userService.save(user);
}
}
Spring reads these annotations and automatically configures routing, dependency injection, and more!
JUnit Testing
public class CalculatorTest {
@BeforeEach // Runs before each test
public void setUp() {
System.out.println("Setting up test...");
}
@Test // This is a test method
public void testAddition() {
assertEquals(5, 2 + 3);
}
@Test
@Disabled("Not ready yet") // Skip this test
public void testDivision() {
assertEquals(2, 4 / 2);
}
@AfterEach // Runs after each test
public void tearDown() {
System.out.println("Cleaning up...");
}
}
Hibernate/JPA
@Entity // This class maps to a database table
@Table(name = "users")
public class User {
@Id // Primary key
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_name", length = 50, nullable = false)
private String username;
@Column(unique = true)
private String email;
@OneToMany(mappedBy = "user") // One user has many orders
private List orders;
// getters/setters
}
Hibernate reads these annotations and creates database tables, handles relationships, etc.
Lombok (Code Generation)
@Data // Generates getters, setters, toString, equals, hashCode
@NoArgsConstructor // Generates no-argument constructor
@AllArgsConstructor // Generates constructor with all fields
public class Product {
private Long id;
private String name;
private double price;
}
// You get ALL this for free (at compile time):
// - Getters: getId(), getName(), getPrice()
// - Setters: setId(), setName(), setPrice()
// - toString(), equals(), hashCode()
// - new Product() constructor
// - new Product(id, name, price) constructor
Reading Annotations with Reflection
Here's how to check and read annotations at runtime:
import java.lang.reflect.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@interface Info {
String author();
int version() default 1;
}
@Info(author = "Alice", version = 2)
class MyClass {
@Info(author = "Bob")
private String field;
@Info(author = "Charlie", version = 3)
public void method() { }
}
public class AnnotationReader {
public static void main(String[] args) throws Exception {
Class> clazz = MyClass.class;
// Check if class has annotation
if (clazz.isAnnotationPresent(Info.class)) {
Info info = clazz.getAnnotation(Info.class);
System.out.println("Class author: " + info.author());
System.out.println("Class version: " + info.version());
}
// Read field annotations
Field field = clazz.getDeclaredField("field");
if (field.isAnnotationPresent(Info.class)) {
Info info = field.getAnnotation(Info.class);
System.out.println("Field author: " + info.author());
}
// Read method annotations
Method method = clazz.getMethod("method");
Info info = method.getAnnotation(Info.class);
System.out.println("Method author: " + info.author());
System.out.println("Method version: " + info.version());
// Get all annotations on a class
Annotation[] annotations = clazz.getAnnotations();
for (Annotation ann : annotations) {
System.out.println("Found: " + ann);
}
}
}
Output:
Class author: Alice
Class version: 2
Field author: Bob
Method author: Charlie
Method version: 3
Found: @Info(author="Alice", version=2)
Key methods:
isAnnotationPresent(Class)- Check if annotation existsgetAnnotation(Class)- Get specific annotationgetAnnotations()- Get all annotationsgetDeclaredAnnotations()- Get annotations (excluding inherited)
Repeating Annotations (Java 8+)
You can apply the same annotation multiple times:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(Schedules.class) // Enable repeating
public @interface Schedule {
String day();
String time();
}
// Container annotation (required for @Repeatable)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Schedules {
Schedule[] value();
}
// Usage - apply multiple times!
public class TaskRunner {
@Schedule(day = "Monday", time = "9:00 AM")
@Schedule(day = "Wednesday", time = "2:00 PM")
@Schedule(day = "Friday", time = "4:00 PM")
public void runBackup() {
System.out.println("Running backup...");
}
}
// Reading
Method method = TaskRunner.class.getMethod("runBackup");
Schedule[] schedules = method.getAnnotationsByType(Schedule.class);
for (Schedule s : schedules) {
System.out.println(s.day() + " at " + s.time());
}
Output:
Monday at 9:00 AM
Wednesday at 2:00 PM
Friday at 4:00 PM
Annotation Processing (Compile-Time)
Advanced: Process annotations during compilation to generate code!
How It Works:
- You create an annotation processor (extends AbstractProcessor)
- During compilation, processor scans for annotations
- Processor generates new Java files or resources
- Generated code is compiled along with your code
This is how Lombok, MapStruct, and other code generators work!
// Simple example (actual processors are more complex)
@SupportedAnnotationTypes("com.example.Builder")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class BuilderProcessor extends AbstractProcessor {
@Override
public boolean process(Set extends TypeElement> annotations,
RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(Builder.class)) {
// Generate builder class code
String builderCode = generateBuilder(element);
// Write to file
writeJavaFile(builderCode);
}
return true;
}
}
Best Practices
✅ DO:
- Use @Override always - Catches typos and signature mistakes
- Make annotations RUNTIME if using Reflection - Otherwise can't read them
- Provide default values - Makes annotations easier to use
- Specify @Target precisely - Prevents misuse
- Document your annotations - Explain what they do and how to use them
- Keep annotations simple - Don't add too many parameters
- Use meaningful names - @Test is better than @T
❌ DON'T:
- Don't put logic in annotations - They're metadata, not code
- Don't abuse @SuppressWarnings - Fix warnings instead of hiding them
- Don't create annotations for everything - Only when truly needed
- Don't forget @Retention - Default is CLASS (not RUNTIME!)
- Don't make annotations too complex - Keep them simple and focused
Common Use Cases
1. Validation
public class User {
@NotNull
@Size(min = 3, max = 50)
private String username;
@Email
private String email;
@Min(18)
@Max(150)
private int age;
}
2. Dependency Injection
@Component
public class UserService {
@Autowired
private UserRepository repository;
@PostConstruct
public void init() {
// Runs after construction
}
}
3. Testing
@RunWith(MockitoJUnitRunner.class)
public class ServiceTest {
@Mock
private Repository repository;
@InjectMocks
private Service service;
@Test
public void testMethod() {
// test code
}
}
4. REST APIs
@RestController
public class API {
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return findUser(id);
}
@PostMapping("/users")
@ResponseStatus(HttpStatus.CREATED)
public User create(@RequestBody User user) {
return save(user);
}
}
Summary
- Annotations are metadata (@Something) attached to code elements
- Built-in: @Override, @Deprecated, @SuppressWarnings, @FunctionalInterface
- Custom: Use
public @interface Name { }to create your own - @Retention: Controls lifetime (SOURCE, CLASS, RUNTIME)
- @Target: Controls where annotation can be used
- Frameworks: Spring, Hibernate, JUnit use annotations extensively
- Reflection: Read annotations at runtime with
getAnnotation() - Processors: Can generate code at compile-time
- Purpose: Reduce boilerplate, configure frameworks, validate code