Annotations in Java
An annotation is a label you attach to a piece of code, such as a class, method, field or parameter, to give extra information about it. It starts with @, like @Override. An annotation doesn't change what the code does by itself. Instead, a tool reads it: the compiler, a build tool, or a framework at run time. For example, @Override asks the compiler to check that a method really overrides another, and Spring's @Autowired or JUnit's @Test are read by those frameworks while your program runs. In this lesson, you will learn the syntax of annotations and the five built-in annotations every Java developer uses.
Syntax
@AnnotationName
@AnnotationName(value)
@AnnotationName(element1 = value1, element2 = value2)| Form | Example | Called |
|---|---|---|
| No elements | @Override | A marker annotation |
One element named value | @SuppressWarnings("unchecked") | A single-value annotation |
| Named elements | @Deprecated(since = "2.0", forRemoval = true) | A full annotation |
Annotations are placed just before the thing they describe, usually on their own line. A declaration can have several annotations.
The Built-In Annotations
| Annotation | It is used to... | Read by |
|---|---|---|
@Override | check that a method overrides or implements a method from a supertype. | The compiler |
@Deprecated | mark code that should no longer be used, optionally with since and forRemoval. | The compiler, and tools at run time |
@SuppressWarnings | turn off named compiler warnings for one declaration. | The compiler |
@FunctionalInterface | check that an interface has exactly one abstract method. | The compiler |
@SafeVarargs | promise that a generic varargs method doesn't misuse its array. | The compiler |
@Override: Catching Typos
Let's see an example of a typo that @Override catches. The method is meant to override toString(), but it is spelled tostring():
static class Student {
String name = "Navika";
@Override
public String tostring() {
return "Student " + name;
}
}Ov.java:5: error: method does not override or implement a method from a supertype
@Override
^
1 errorExplanation: Without @Override, this compiles fine and silently creates a new method named tostring, while printing the object still uses Object's version. With the annotation, the compiler checks the intent and reports the mistake.
@Deprecated: Retiring Old Code
public class Fees {
/** @deprecated use {@link #total(int, int)} instead */
@Deprecated(since = "2.0", forRemoval = true)
static int oldTotal(int fee) { return fee; }
@Deprecated(since = "1.5")
static int legacy(int fee) { return fee; }
static int total(int fee, int tax) { return fee + tax; }
}When another class calls both old methods, the compiler warns:
UseFees.java:3: warning: [removal] oldTotal(int) in Fees has been deprecated and marked for removal
System.out.println(Fees.oldTotal(100));
^
UseFees.java:4: warning: [deprecation] legacy(int) in Fees has been deprecated
System.out.println(Fees.legacy(100));
^
2 warningsExplanation: since records the version that deprecated it, and forRemoval = true means it will be deleted, so the warning is stronger ([removal]) and appears even without -Xlint. Plain deprecation warnings are summarized in a short "Note" unless you compile with -Xlint:deprecation. The @deprecated Javadoc tag (lowercase) explains what to use instead; good code has both.
@SuppressWarnings: Silencing a Known Warning
Raw types, such as List without <String>, produce "unchecked" warnings:
Raw.java:7: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
raw.add("Navika");
^
where E is a type-variable:
E extends Object declared in interface List
Raw.java:8: warning: [unchecked] unchecked conversion
return raw;
^
required: List<String>
found: List
2 warningsBoth warnings came from the method names(). An identical method quiet() annotated with @SuppressWarnings("unchecked") produced no warnings. Common names include "unchecked", "deprecation", "removal", "rawtypes" and "unused".
@FunctionalInterface: One Abstract Method
A functional interface can be the target of a lambda, so it must have exactly one abstract method. The annotation makes the compiler check it:
@FunctionalInterface
interface Grader {
String grade(int marks);
boolean passed(int marks);
}Fi.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
Grader is not a functional interface
multiple non-overriding abstract methods found in interface Grader
1 errorThe annotation is optional: any interface with one abstract method works with lambdas. It protects against someone adding a second method later.
@SafeVarargs: Generic Varargs
A varargs parameter of a generic type, like T... items, creates a generic array, which Java can't fully check. The compiler warns about possible heap pollution (a variable holding a value of the wrong type):
Safe.java:11: warning: [unchecked] Possible heap pollution from parameterized vararg type T
static <T> List<T> unsafeListOf(T... items) {
^
where T is a type-variable:
T extends Object declared in method <T>unsafeListOf(T...)
1 warningAdding @SafeVarargs to a method that only reads the array removes the warning. It can be used only on methods that can't be overridden: static, final and private methods, and constructors.
Example: Which Annotations Survive to Run Time?
Some annotations are used only by the compiler and then thrown away; others stay in the .class file and can be read while the program runs. Let's see an example:
import java.lang.reflect.Method;
import java.util.Arrays;
public class Seen {
static class Student {
@Override
public String toString() {
return "Student";
}
@Deprecated(since = "2.0", forRemoval = true)
public int oldMarks() {
return 0;
}
}
public static void main(String[] args) throws Exception {
Method ts = Student.class.getMethod("toString");
Method old = Student.class.getMethod("oldMarks");
System.out.println("toString annotations: "
+ Arrays.toString(ts.getAnnotations()));
System.out.println("oldMarks annotations: "
+ Arrays.toString(old.getAnnotations()));
Deprecated d = old.getAnnotation(Deprecated.class);
System.out.println("since " + d.since()
+ ", forRemoval " + d.forRemoval());
}
}toString annotations: []
oldMarks annotations: [@java.lang.Deprecated(forRemoval=true, since="2.0")]
since 2.0, forRemoval trueExplanation: @Override matters only while compiling, so it isn't stored, and toString shows no annotations at run time. @Deprecated is kept, so the program can read it and even its element values. Where an annotation is kept is controlled by a meta-annotation called @Retention, explained in "Meta-Annotations in Java". Reading annotations at run time uses reflection, covered in "Reflection API in Java".
Common Mistakes
Leaving out @Override. A misspelled or wrongly typed method silently becomes a new method.
Suppressing warnings too broadly. A class-level @SuppressWarnings("unchecked") hides future real problems.
Deprecating without saying what to use instead. Add a @deprecated Javadoc tag that points to the replacement.
Thinking annotations do something by themselves. They are only labels; something must read them.
Interview Corner
What are annotations in Java?
Annotations are metadata attached to declarations (classes, methods, fields, parameters and more) with the @ syntax. They don't change the code's behavior directly; the compiler, build tools or frameworks read them. Built-in examples are @Override (compiler checks overriding), @Deprecated (marks obsolete code, with since and forRemoval), @SuppressWarnings (silences named warnings), @FunctionalInterface (checks there is one abstract method) and @SafeVarargs (suppresses heap pollution warnings for generic varargs). Annotations with runtime retention can be read through reflection, which is how frameworks like Spring and JUnit work.
Why should you use @Override?
It makes the compiler verify that the method really overrides or implements a supertype method. Without it, a typo in the name or a different parameter type silently creates a new overloaded or unrelated method, and the program uses the parent's version without any error.
Practice
- Easy: Write a class with an
equals(Student other)method annotated@Override, read the compiler error, and fix the signature toequals(Object other). - Harder: Mark a method
@Deprecated(since = "3.0", forRemoval = true), call it from another class, and compare the compiler output with and without-Xlint:deprecation.
Key Takeaways
Was this tutorial helpful?