Back to Annotations
Q1Question 1 of 3

What are the built-in Java annotations?

Key Concepts
  • @Override: compile-time check that the method actually overrides a parent method
  • @Deprecated: marks a method/class as outdated – compiler warns when used
  • @SuppressWarnings: tells the compiler to suppress specific warnings
  • @FunctionalInterface: ensures the interface has exactly one abstract method (for lambdas)
  • @SafeVarargs: suppresses heap pollution warnings for generic varargs
  • Meta-annotations: @Target, @Retention, @Documented, @Inherited control how annotations behave

Definition

@Override ensures correct overriding, @Deprecated marks outdated code, @SuppressWarnings silences warnings, @FunctionalInterface enforces single-abstract-method. Meta-annotations control annotation behavior.

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.

JAVA
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.

JAVA
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.

JAVA
@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.

JAVA
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.

JAVA
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.

JAVA
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.

JAVA
@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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate