Definition
Built-in annotations fall into two groups:
- Code annotations: @Override, @Deprecated, @SuppressWarnings, @FunctionalInterface, @SafeVarargs.
- Meta-annotations (annotations on annotations): @Target (where it can be used), @Retention (how long it survives: SOURCE, CLASS, RUNTIME), @Documented, @Inherited, @Repeatable.
@Override is a stamp that says "this form replaces the head office version" – if it does not match, the stamp is rejected. @Deprecated is a "discontinued" label on an old banking product. @SuppressWarnings is the branch manager signing off on a known warning.
Understanding the Concepts
@Override
@Override is a compile-time instruction that tells the compiler to verify the annotated method actually overrides a method from a parent class or implements a method from an interface. If the method signature does not match any parent method, the compiler produces an error immediately, catching the bug before the code ever runs.
package com.bank.core;
import java.math.BigDecimal;
public class SavingsAccount extends Account {
@Override // compiler verifies this actually overrides a parent method
public BigDecimal calculateInterest() {
return balance.multiply(new BigDecimal("0.04"));
}
// @Override
// public BigDecimal calulateInterest() { } // COMPILE ERROR: typo, no such parent method
}
Always use @Override. It catches typos and accidental non-overrides at compile time. Without it, a typo silently creates a new method instead of overriding the parent, and polymorphism breaks.
@Deprecated
@Deprecated marks a method, class, or field as outdated. The compiler generates a warning when any code calls the deprecated element. IDEs display it with a strikethrough. Since Java 9, the annotation has two optional elements: since records the version where deprecation started, and forRemoval signals whether the element will be deleted in a future release.
package com.bank.service;
import java.math.BigDecimal;
public class TransferService {
@Deprecated(since = "2.0", forRemoval = true)
public void transferLegacy(String from, String to, BigDecimal amount) {
// Old implementation – use transfer() instead
}
public void transfer(Account from, Account to, BigDecimal amount) {
from.debit(amount);
to.credit(amount);
}
}
When forRemoval = true, the compiler issues a stronger warning. In a banking codebase, marking legacy transfer methods as deprecated helps teams migrate gradually without breaking existing batch jobs that still call the old API.
@SuppressWarnings
@SuppressWarnings tells the compiler to silence specific categories of warnings on the annotated element. Use it sparingly and only after confirming the warning is a false positive.
@SuppressWarnings("unchecked")
List<Account> accounts = (List<Account>) rawList; // suppress unchecked cast warning
@SuppressWarnings({"deprecation", "unused"})
public void legacyBatchProcess() {
transferLegacy("SAV2001", "CUR3001", new BigDecimal("50000"));
}
Common warning categories include unchecked (generic casts), deprecation (calling deprecated code), unused (unused variables), and rawtypes (raw generic types). Always add a comment explaining why the suppression is safe.
@FunctionalInterface
@FunctionalInterface ensures the interface has exactly one abstract method, making it usable as a lambda target. If someone adds a second abstract method, the compiler reports an error.
package com.bank.service;
import java.math.BigDecimal;
@FunctionalInterface // compiler ensures exactly one abstract method
public interface TransferValidator {
boolean isValid(Account from, Account to, BigDecimal amount);
// Adding another abstract method here causes a COMPILE ERROR
}
// Used as a lambda in the transfer pipeline
TransferValidator positiveAmount = (from, to, amount) -> amount.signum() > 0;
TransferValidator sufficientFunds = (from, to, amount) ->
from.getBalance().compareTo(amount) >= 0;
The interface can still have default methods and static methods. Only abstract methods count toward the single-method limit.
@SafeVarargs
@SafeVarargs suppresses heap pollution warnings when a method accepts generic varargs. Heap pollution means a variable of a parameterized type refers to an object that is not of that type.
package com.bank.util;
import java.util.Arrays;
import java.util.List;
public class AccountUtils {
@SafeVarargs // developer guarantees no heap pollution
public static <T> List<T> listOf(T... elements) {
return Arrays.asList(elements);
}
}
// Usage
List<String> accountIds = AccountUtils.listOf("SAV2001", "CUR3001", "LOAN5001");
You may only apply @SafeVarargs to methods that are static, final, or private (since Java 9), because those cannot be overridden.
Meta-annotations
Meta-annotations are annotations that you place on other annotations to control their behavior. Java provides five meta-annotations: @Target, @Retention, @Documented, @Inherited, and @Repeatable.
import java.lang.annotation.*;
@Target(ElementType.METHOD) // can only be placed on methods
@Retention(RetentionPolicy.RUNTIME) // available at runtime via reflection
@Documented // included in Javadoc
public @interface Audited {
String action() default "";
}
| Retention | Available at | Used by |
|---|---|---|
| SOURCE | Compile time only | @Override, @SuppressWarnings |
| CLASS | In .class file, not runtime | Default if unspecified |
| RUNTIME | Via reflection at runtime | Spring, JPA, JUnit |
@Target restricts where the annotation can appear: METHOD, FIELD, TYPE, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE, TYPE_PARAMETER (Java 8+), and TYPE_USE (Java 8+).
What happens if you use the wrong retention?
If you define a custom annotation with SOURCE retention but try to read it at runtime via reflection, the annotation is gone and reflection returns null. This is a common mistake when building custom framework features.
@Retention(RetentionPolicy.SOURCE) // only available at compile time
public @interface CompileOnly { }
// At runtime:
Method m = TransferService.class.getMethod("transfer");
m.isAnnotationPresent(CompileOnly.class); // false – the annotation was stripped
Always use RUNTIME retention when your code or a framework needs to discover the annotation via reflection.
What should a senior engineer know?
- @Repeatable (Java 8+) lets an annotation be used multiple times on the same element. It requires a container annotation to hold the repeated values.
- Spring annotations (@Component, @Transactional) use RUNTIME retention so Spring can read them via reflection during classpath scanning.
- Annotation processing (compile-time): Lombok, MapStruct, and Dagger generate code by processing annotations at compile time using JSR 269 processors.
- TYPE_USE annotations (Java 8+) can annotate any use of a type:
List<@NonNull String>,(@NonNull String) obj. This is the foundation for tools like the Checker Framework that catch nullability bugs at compile time.
Visual Overview
Built-in annotations:
@Override → compile check: actually overrides parent
@Deprecated → marks outdated code, compiler warns
@SuppressWarnings → silences specific warnings
@FunctionalInterface→ exactly one abstract method
@SafeVarargs → suppress generic varargs warning
Meta-annotations (on annotations):
@Target → where: METHOD, FIELD, TYPE, PARAMETER
@Retention → when: SOURCE, CLASS, RUNTIME
@Documented, @Inherited, @Repeatable
Key Takeaway
Java's built-in annotations provide compile-time safety and documentation. @Override catches overriding mistakes. @Deprecated marks outdated code. @FunctionalInterface enforces the lambda contract. Meta-annotations like @Target and @Retention control where and how long custom annotations survive.
One sentence to remember: @Override catches typos, @Deprecated warns users, @FunctionalInterface enforces one abstract method, and @Retention(RUNTIME) makes annotations readable by frameworks.
---