Annotations in Java

Naveen AnamaneniUpdated · 7 min read

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

Syntax
@AnnotationName
@AnnotationName(value)
@AnnotationName(element1 = value1, element2 = value2)
FormExampleCalled
No elements@OverrideA 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

AnnotationIt is used to...Read by
@Overridecheck that a method overrides or implements a method from a supertype.The compiler
@Deprecatedmark code that should no longer be used, optionally with since and forRemoval.The compiler, and tools at run time
@SuppressWarningsturn off named compiler warnings for one declaration.The compiler
@FunctionalInterfacecheck that an interface has exactly one abstract method.The compiler
@SafeVarargspromise 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():

Ov.java
static class Student {
    String name = "Navika";
 
    @Override
    public String tostring() {
        return "Student " + name;
    }
}
Compiler output
Ov.java:5: error: method does not override or implement a method from a supertype
        @Override
        ^
1 error

Explanation: 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

Fees.java
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:

Compiler output (javac -Xlint:deprecation)
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 warnings

Explanation: 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:

Compiler output (javac -Xlint:unchecked)
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 warnings

Both 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:

Fi.java
@FunctionalInterface
interface Grader {
    String grade(int marks);
    boolean passed(int marks);
}
Compiler output
Fi.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
  Grader is not a functional interface
    multiple non-overriding abstract methods found in interface Grader
1 error

The 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):

Compiler output (javac -Xlint:unchecked)
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 warning

Adding @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:

Seen.java▶ Try it
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());
    }
}
Output
toString annotations: []
oldMarks annotations: [@java.lang.Deprecated(forRemoval=true, since="2.0")]
since 2.0, forRemoval true

Explanation: @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

  1. Easy: Write a class with an equals(Student other) method annotated @Override, read the compiler error, and fix the signature to equals(Object other).
  2. 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.

Quick check: Annotations

4 questions
1What does @Override do?
2What does forRemoval = true add to @Deprecated?
3Which annotation is visible at run time through reflection?
4Where can @SafeVarargs be used?

Key Takeaways

Was this tutorial helpful?

Lesson 463 of 482