@Override
, @Deprecated
, etc.)Java annotations are metadata that provide information to the compiler or runtime environment. Built-in annotations serve as flags to communicate intent, affect compilation behavior, or help IDEs provide warnings or suggestions. This section focuses on three of the most widely used built-in annotations: @Override
, @Deprecated
, and @SuppressWarnings
.
@Override
The @Override
annotation indicates that a method is intended to override a method declared in a superclass or implement an interface method.
Example:
class Animal {
void speak() {
System.out.println("Animal speaks");
}
}
class Dog extends Animal {
@Override
void speak() {
System.out.println("Dog barks");
}
}
If you accidentally misspell the method name or signature, the compiler will flag an error:
class Cat extends Animal {
@Override
void speek() { // Error: Method does not override a superclass method
System.out.println("Cat meows");
}
}
Why use @Override
?
@Deprecated
The @Deprecated
annotation marks a method, field, or class as outdated and signals that it should no longer be used.
Example:
class Calculator {
@Deprecated
int add(int a, int b) {
return a + b;
}
int sum(int... numbers) {
int result = 0;
for (int num : numbers) {
result += num;
}
return result;
}
}
When add()
is used, the compiler will issue a warning:
Calculator calc = new Calculator();
int result = calc.add(3, 4); // Warning: add(int, int) is deprecated
Enhancing with @Deprecated
Javadoc:
/**
* @deprecated Use {@link #sum(int...)} instead.
*/
@Deprecated
int add(int a, int b) {
return a + b;
}
Why use @Deprecated
?
@SuppressWarnings
The @SuppressWarnings
annotation tells the compiler to ignore specific warnings for a code block, method, or class. It's useful for avoiding unnecessary or known warnings in special cases.
Common Values:
"unchecked"
— to suppress unchecked type operations (e.g., raw generics)"deprecation"
— to suppress warnings about deprecated APIs"rawtypes"
— to suppress warnings about using raw typesExample:
@SuppressWarnings("unchecked")
void rawListUsage() {
List list = new ArrayList(); // Raw type warning suppressed
list.add("Hello");
}
You can apply it to methods, classes, or local variables:
@SuppressWarnings({"unchecked", "deprecation"})
void mixedUsage() {
List list = new ArrayList();
list.add("Hello");
Calculator calc = new Calculator();
int result = calc.add(2, 3); // Deprecated method, warning suppressed
}
Why use @SuppressWarnings
?
Built-in annotations serve three primary purposes:
Correctness: @Override
helps prevent bugs by ensuring methods are overridden as intended.
Maintainability and API Evolution: @Deprecated
signals safe migration paths and supports progressive changes without breaking legacy code.
Control over Compiler Feedback: @SuppressWarnings
allows developers to quiet compiler warnings selectively, enabling focus on real issues.
These annotations are not merely decorative—they guide the compiler, enhance documentation, and clarify design choices. IDEs like IntelliJ IDEA and Eclipse leverage annotations to provide better code suggestions, autocomplete, and error detection, making development faster and more robust.
import java.util.ArrayList;
import java.util.List;
class Animal {
void speak() {
System.out.println("Animal speaks");
}
}
class Dog extends Animal {
@Override
void speak() {
System.out.println("Dog barks");
}
}
class Calculator {
/**
* @deprecated Use {@link #sum(int...)} instead.
*/
@Deprecated
int add(int a, int b) {
return a + b;
}
int sum(int... numbers) {
int result = 0;
for (int n : numbers) {
result += n;
}
return result;
}
}
public class Main {
@SuppressWarnings({"unchecked", "deprecation"})
public static void main(String[] args) {
// @Override demonstration
Animal a = new Dog();
a.speak(); // Dog barks
// @Deprecated usage
Calculator calc = new Calculator();
int result = calc.add(3, 4); // Deprecated method
System.out.println("Deprecated add() result: " + result);
// @SuppressWarnings usage
List list = new ArrayList(); // Raw type
list.add("Unchecked warning suppressed");
System.out.println("Raw list first element: " + list.get(0));
}
}
Annotation | Purpose | Typical Usage |
---|---|---|
@Override |
Ensure method correctly overrides parent | Method overriding in subclasses |
@Deprecated |
Warn about outdated or unsafe features | Mark old methods or classes |
@SuppressWarnings |
Suppress known compiler warnings | Raw types, deprecated use, etc. |
Using these annotations is not just good practice—it's essential for writing clear, maintainable, and forward-compatible Java code. As you develop more complex systems, understanding and applying annotations effectively will help you build more reliable software.
In Java, annotations are not limited to built-in ones like @Override
or @Deprecated
. Developers can define their own annotations to describe metadata for classes, methods, fields, parameters, and more. These custom annotations enable meta-programming, allowing tools, libraries, and frameworks to behave dynamically based on annotated elements.
A custom annotation is defined using the @interface
keyword. The syntax resembles that of an interface, but it does not contain methods in the traditional sense—rather, it defines elements that act like configuration parameters.
Basic Syntax:
public @interface MyAnnotation {
String value(); // an element with no default value
}
This annotation can be applied like so:
@MyAnnotation("Hello")
public class Greeting {
// ...
}
In this case, the value
element is specified directly. If the annotation has only one element named value
, Java allows shorthand usage by omitting the name.
Annotations can include multiple elements, and elements can have default values.
Example:
public @interface Author {
String name();
String date();
int version() default 1; // optional element with default
}
Usage:
@Author(name = "Jane Doe", date = "2025-06-20")
public class Document {
// ...
}
Here, version
is omitted because it uses the default value of 1
. You can include it explicitly if needed:
@Author(name = "Jane Doe", date = "2025-06-20", version = 2)
The types allowed in annotation elements are limited:
int
, double
, etc.)String
Class
or Class<?>
Invalid types like List<String>
or Map<String, String>
are not permitted.
Suppose you're building a validation tool and want to annotate fields that must not be null.
public @interface NotNull {
String message() default "This field cannot be null.";
}
You can use this annotation on fields:
public class User {
@NotNull
private String name;
@NotNull(message = "Email is required.")
private String email;
}
Although Java itself won't enforce this constraint, you can write a tool or framework to read the annotation using reflection and perform validation.
You can retrieve annotation metadata at runtime using the java.lang.reflect
API.
Example:
import java.lang.reflect.Field;
public class Validator {
public static void validate(Object obj) throws Exception {
Class<?> clazz = obj.getClass();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(NotNull.class)) {
field.setAccessible(true);
Object value = field.get(obj);
if (value == null) {
NotNull annotation = field.getAnnotation(NotNull.class);
throw new Exception(annotation.message());
}
}
}
}
}
import java.lang.annotation.*;
import java.lang.reflect.Field;
// Define the custom annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface NotNull {
String message() default "This field cannot be null.";
}
// Class using the annotation
class User {
@NotNull
private String name;
@NotNull(message = "Email is required.")
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
}
// Validator that uses reflection to check @NotNull fields
class Validator {
public static void validate(Object obj) throws Exception {
Class<?> clazz = obj.getClass();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(NotNull.class)) {
field.setAccessible(true);
Object value = field.get(obj);
if (value == null) {
NotNull annotation = field.getAnnotation(NotNull.class);
throw new Exception(annotation.message());
}
}
}
}
}
// Main class to test it all
public class CustomAnnotationDemo {
public static void main(String[] args) {
try {
User user1 = new User("Alice", "alice@example.com");
Validator.validate(user1); // Should pass
User user2 = new User("Bob", null);
Validator.validate(user2); // Should throw an exception
} catch (Exception e) {
System.out.println("Validation error: " + e.getMessage());
}
}
}
Custom annotations are a key enabler of declarative programming—where developers express what should happen, not how.
@Inject
, @Autowired
@Test
, @BeforeEach
@JsonProperty
, @XmlElement
@RolesAllowed
, @Secured
@NotNull
, @Min
, @Email
All of these are implemented as custom annotations that are processed at compile-time or runtime.
Feature | Description |
---|---|
@interface |
Declares a new annotation type |
Elements | Configuration values (must return specific supported types) |
Default values | Optional elements can have default values |
Usage | Apply to class, method, field, etc. using @YourAnnotation |
Accessing annotations | Use reflection (Class.getDeclaredFields() , etc.) |
Custom annotations are a powerful design tool in Java. While they don't enforce behavior on their own, they enable you to express intent declaratively and build tooling around it. This is especially powerful in large systems or frameworks, where consistent behavior can be encoded through simple annotations rather than boilerplate logic.
By learning how to define and apply your own annotations, you open the door to building cleaner, more expressive APIs and participating in the modern Java ecosystem of annotation-driven development.
In Java, annotations provide metadata that describe how a program should behave, rather than implementing the behavior directly. Applying annotations to various elements—classes, methods, fields, parameters, local variables—makes code more expressive, declarative, and tool-friendly. This section explores how annotations are applied and interpreted in different contexts.
Annotations are placed directly above the element they apply to, using the @AnnotationName
syntax.
@Deprecated
public class OldService {
// ...
}
Here, @Deprecated
marks the entire class as outdated. IDEs and compilers will usually show a warning when this class is used.
Annotations are commonly used with methods, especially for overriding, testing, and lifecycle control.
@Override
on a methodclass Animal {
void makeSound() {
System.out.println("Generic animal sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof!");
}
}
The @Override
annotation instructs the compiler to ensure that makeSound()
actually overrides a method from the superclass. If it doesn't, a compile-time error occurs.
Field annotations are often used in frameworks like Spring, Hibernate, or validation libraries.
public class User {
@NotNull
private String username;
@Size(min = 8)
private String password;
}
These annotations act as metadata. Tools can use them to automatically validate the field values at runtime.
Method parameters can also be annotated, often for validation or documentation purposes.
public void registerUser(@NotNull String email) {
// ...
}
Here, a tool or library could detect the @NotNull
annotation and reject null values automatically.
Since Java 8, even local variables can be annotated.
public void process() {
@SuppressWarnings("unused")
int temp = 42;
}
This usage is less common but can be helpful for controlling compiler behavior in specific scopes.
You can apply more than one annotation to a single element by stacking them.
@Deprecated
@SuppressWarnings("unchecked")
public void legacyMethod() {
// ...
}
The order of annotations generally doesn't matter, unless one annotation depends on another (rare). Some annotations, like @Repeatable
ones, can be applied multiple times in certain ways (covered later).
Target | Example |
---|---|
Class | @Deprecated class Foo {} |
Method | @Override void bar() {} |
Field | @NotNull String name; |
Parameter | void setName(@NotNull String name) |
Local Variable | @SuppressWarnings String s = ""; |
Annotations can include values to customize their behavior.
@Test(timeout = 1000)
public void testPerformance() {
// ...
}
Here, the @Test
annotation from JUnit includes a timeout
parameter.
import java.util.ArrayList;
import java.util.List;
// Custom stubs to simulate behavior
@interface NotNull {}
@interface Size {
int min();
}
public class AnnotationDemo {
@Deprecated
static class OldService {
void doSomething() {
System.out.println("OldService logic");
}
}
static class Animal {
void makeSound() {
System.out.println("Generic animal sound");
}
}
static class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof!");
}
}
static class User {
@NotNull
String username;
@Size(min = 8)
String password;
}
public void registerUser(@NotNull String email) {
System.out.println("Registering: " + email);
}
public void process() {
@SuppressWarnings("unused")
int temp = 42;
}
@Deprecated
@SuppressWarnings("unchecked")
public void legacyMethod() {
List list = new ArrayList(); // raw type
list.add("legacy");
System.out.println("Legacy method ran: " + list.get(0));
}
public static void main(String[] args) {
OldService old = new OldService(); // Will show deprecation warning
old.doSomething();
Animal a = new Dog();
a.makeSound();
AnnotationDemo demo = new AnnotationDemo();
demo.registerUser("user@example.com");
demo.legacyMethod();
}
}
Annotations shift the focus from how to do something to what should be done. Instead of manually wiring services or validating input, annotations allow developers to declare their intent.
@Transactional
, @Test
).Applying annotations correctly is a foundational skill in modern Java development. From basic compiler hints (@Override
) to complex framework configurations (@Autowired
, @Entity
), annotations make your code more readable, robust, and framework-friendly.
When used thoughtfully, annotations cleanly encode metadata that improves program structure and behavior—without cluttering logic with extra code. This is especially powerful in large projects, where declarative style can lead to cleaner APIs and reduced boilerplate.
Annotations in Java are powerful tools that enhance code with metadata. However, for them to be truly useful, Java provides two important meta-annotations from the java.lang.annotation
package: @Retention
and @Target
. These meta-annotations define how annotations behave—specifically, where they can be applied and how long they are retained in the compiled code.
@Retention
: Controlling Annotation LifetimeThe @Retention
annotation specifies how long an annotation is retained in the program. It takes a value from the RetentionPolicy
enum:
SOURCE
.class
file.@SuppressWarnings
.CLASS
.class
file but is not available at runtime via reflection.RUNTIME
.class
file and available at runtime through reflection.import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecution {
}
This @LogExecution
annotation will be available during runtime, which is necessary if we want to scan classes with reflection to detect its presence.
@Target
: Controlling Where Annotations Can Be AppliedThe @Target
meta-annotation limits where your custom annotation can legally appear. It uses constants from the ElementType
enum.
ElementType
Values:TYPE
: class, interface, enum, or annotation declarationFIELD
: fields (including enum constants)METHOD
: methodsPARAMETER
: method parametersCONSTRUCTOR
: constructorsLOCAL_VARIABLE
: local variablesANNOTATION_TYPE
: another annotation typePACKAGE
: package declarationTYPE_USE
: anywhere a type is used (advanced, e.g., List<@NotNull String>
)import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Audit {
}
In this example, @Audit
can only be applied to methods. Trying to use it on a class or a field would cause a compilation error.
@Retention
and @Target
Typically, you'll specify both @Retention
and @Target
when defining a custom annotation, especially if you expect it to be used by frameworks or tooling.
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log {
String value() default "INFO";
}
This annotation can now be used like this:
public class UserService {
@Log("DEBUG")
public void createUser() {
// Business logic
}
}
And later, a framework can reflectively inspect this annotation:
Method m = UserService.class.getMethod("createUser");
if (m.isAnnotationPresent(Log.class)) {
Log log = m.getAnnotation(Log.class);
System.out.println("Log level: " + log.value());
}
You can allow annotations on multiple types by passing an array to @Target
.
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
public @interface Timed {
}
This allows @Timed
to be applied to both methods and constructors.
import java.lang.annotation.*;
import java.lang.reflect.Method;
// Define the annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Log {
String value() default "INFO";
}
// Use the annotation
class UserService {
@Log("DEBUG")
public void createUser() {
System.out.println("User created.");
}
@Log
public void deleteUser() {
System.out.println("User deleted.");
}
public void updateUser() {
System.out.println("User updated.");
}
}
// Main class to inspect and use the annotation
public class AnnotationDemo {
public static void main(String[] args) throws Exception {
UserService service = new UserService();
Method[] methods = UserService.class.getDeclaredMethods();
for (Method m : methods) {
if (m.isAnnotationPresent(Log.class)) {
Log log = m.getAnnotation(Log.class);
System.out.println("Method: " + m.getName() + ", Log Level: " + log.value());
m.invoke(service); // Call the method
}
}
}
}
RetentionPolicy.RUNTIME
If you plan to use reflection to detect annotations at runtime—like in dependency injection frameworks, testing tools, or ORM systems—you must declare the annotation with RetentionPolicy.RUNTIME
. Otherwise, the JVM will not retain the annotation, and your reflection code won't find it.
The @Retention
and @Target
meta-annotations are not just technical details—they are essential to the proper use and design of custom annotations. They give you:
@Retention
)@Target
)Used wisely, these meta-annotations enforce good design patterns and ensure that your annotations are applied in meaningful, predictable ways. Whether you're building a lightweight library, a testing tool, or a full-fledged framework, these policies help manage annotation behavior efficiently—keeping your metadata robust, safe, and appropriately scoped.