Java Tutorial
🔍

Java Annotations

Java Annotations

An annotation is metadata attached to a class, method, field, parameter, or package - extra information that the compiler, the JVM, or a framework can read and act on, without that information being part of the code's runtime logic. @Override tells the compiler to verify that a method actually overrides something. @Deprecated tells every caller that this API is on its way out. @Autowired tells Spring to inject a dependency. None of these change what the annotated code does at the machine level - they change how tools and frameworks treat that code.

What Are Annotations?

An annotation is declared with @interface and applied with an @ prefix. It can carry named elements (which look like methods in the declaration, values in the usage), or it can be a simple marker with no elements at all.

DECLARING AN ANNOTATION:
  public @interface MyAnnotation {
      String value() default "default-value";
      int priority() default 1;
  }

APPLYING AN ANNOTATION:
  @MyAnnotation(value = "prod", priority = 3)
  public class PaymentService { ... }

  @MyAnnotation("prod")    <- shorthand when only 'value' is set
  public class OrderService { ... }

  @MyAnnotation            <- shorthand when only defaults are used
  public class UserService { ... }

WHERE ANNOTATIONS CAN BE PLACED:
  Classes, interfaces, enums, records
  Methods and constructors
  Fields and local variables
  Parameters
  Packages (in package-info.java)
  Type uses (Java 8+ - in generics, casts, implements clauses)

Basic Overview - What Every Developer Needs to Know

BUILT-IN ANNOTATIONS - used every day without thinking about them
  @Override       - tells the compiler this method MUST override a
                    superclass method or interface method.
                    If nothing matches, COMPILE ERROR.
  @Deprecated     - marks a method, class, or field as outdated.
                    Callers see a compile-time warning.
  @SuppressWarnings - tells the compiler to silence a specific
                    warning category for this element.
  @FunctionalInterface - declares an interface has exactly one
                    abstract method, making it a lambda target.
                    If more than one abstract method exists, COMPILE ERROR.

META-ANNOTATIONS - annotations that annotate other annotations
  @Retention      - controls how long the annotation lives:
                    SOURCE (discarded after compile),
                    CLASS (in .class file but not at runtime),
                    RUNTIME (readable at runtime via reflection)
  @Target         - restricts WHERE the annotation can be placed:
                    TYPE, METHOD, FIELD, PARAMETER, CONSTRUCTOR, ...
  @Documented     - includes the annotation in Javadoc output
  @Inherited      - a subclass automatically inherits this annotation
                    from its parent class (applies to class annotations only)
  @Repeatable     - allows the same annotation to be applied MORE
                    than once to the same element

CUSTOM ANNOTATIONS - the ones you and your framework write
  Fresher view  : you can define your OWN annotation to mark things
                  and then write code (a processor or reflection-based
                  validator) that reads those marks and acts on them
  Deeper view   : the annotation itself is just metadata storage -
                  the PROCESSOR (compile-time annotation processor
                  or runtime reflection code) is what gives it meaning.
                  The annotation declares WHAT; the processor decides WHAT TO DO

RETENTION POLICY - the single most important concept after the basics
  Fresher view  : RetentionPolicy.RUNTIME means you can read the
                  annotation using reflection while the program runs.
                  Most custom annotations for frameworks need this.
  Deeper view   : RetentionPolicy.SOURCE means the annotation is
                  useful ONLY to the compiler and is gone afterward
                  (Lombok annotations work this way - they generate
                  code at compile time and leave no annotation in
                  the .class file). RetentionPolicy.CLASS (the default)
                  is in the .class file but not accessible at runtime -
                  rarely the right choice unless you are building
                  a bytecode manipulation tool.

A fresher needs @Override, @Deprecated, @SuppressWarnings, and the concept that annotations are metadata the framework reads - this covers probably 80% of daily annotation usage. The @Retention/@Target pair and custom annotation writing are where the topic moves from "I use these" to "I understand how these work" - and that shift is exactly what interviewers at product companies test.

Why Annotations Matter

Before annotations existed - before Java 5 - the same kind of metadata lived in XML configuration files. Every Spring bean, every JPA entity mapping, every web servlet had a corresponding XML descriptor file. The code and its configuration were separate artifacts that had to be kept in sync manually, and any mismatch was a runtime error discovered during startup rather than a compile-time signal.

Annotations moved that configuration into the source code itself, adjacent to the thing being configured. @Entity, @Column(name="user_id"), @GetMapping("/users") express in the declaration exactly how that class or method participates in the larger system - and tools can verify them, process them, and generate warnings or errors about them before the application ever runs.

Three concrete things annotations enable:

Compiler verification. @Override is the clearest example: the compiler checks that the annotated method actually matches a method in a supertype. Without it, a typo in the method name silently creates a new method instead of overriding, and the bug only surfaces at runtime when the expected behavior does not occur.

Framework configuration. Spring, JPA, Jakarta EE, and most major frameworks read annotations at runtime via reflection to determine how to configure, inject, proxy, and manage components. @Autowired on a constructor field tells Spring what to inject; @Transactional on a service method tells Spring to wrap it in a transaction proxy.

Code generation. Compile-time annotation processors (the javax.annotation.processing API) can generate source files, validate constraints, or produce documentation at build time, before the annotated classes are compiled into bytecode. Lombok's @Getter, @Setter, and @Builder work this way - the annotation processor generates the getter, setter, and builder methods during compilation, and the resulting class file contains them as if they had been written by hand.

Built-In Annotations

Compiler Annotations

The three annotations used most often to communicate with the Java compiler directly are @Override, @Deprecated, and @SuppressWarnings.

// File: BuiltInAnnotationsDemo.java public class BuiltInAnnotationsDemo { interface Greeter { String greet(String name); } static class EnglishGreeter implements Greeter { // @Override tells the compiler to verify this overrides Greeter.greet() // If the method name is misspelled or the parameter type is wrong, // the compiler reports an error here instead of silently creating // a second, unrelated method @Override public String greet(String name) { return "Hello, " + name + "!"; } // @Deprecated signals to every caller that this method should no // longer be used - IDEs strike through the name, and calling it // produces a compile-time warning @Deprecated(since = "2.0", forRemoval = true) public String helloOldStyle(String name) { return "Hello " + name; } // @SuppressWarnings silences one specific warning category. // "deprecation" tells the compiler not to warn that helloOldStyle // is deprecated for THIS call site only - not globally @SuppressWarnings("deprecation") public void demonstrateDeprecated() { System.out.println(helloOldStyle("Ananya")); // no warning here } } @FunctionalInterface interface PriceFormatter { String format(double amount); // exactly one abstract method - lambda target // adding a second abstract method here would be a COMPILE ERROR } public static void main(String[] args) { Greeter greeter = new EnglishGreeter(); System.out.println(greeter.greet("Rahul")); PriceFormatter formatter = amount -> "Rs. " + String.format("%.2f", amount); System.out.println(formatter.format(1499.0)); } }
Output:
Hello, Rahul!
Rs. 1499.00

Meta-Annotations

Meta-annotations control how a custom annotation behaves - where it can be placed and how long it survives.

// File: MetaAnnotationsDemo.java import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Field; public class MetaAnnotationsDemo { // @Target restricts this annotation to FIELD declarations only. // Placing it on a method or class would be a COMPILE ERROR. // @Retention(RUNTIME) makes it readable at runtime via reflection - // essential for any annotation a framework needs to process. @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @interface MaxLength { int value(); String message() default "Value exceeds maximum length"; } static class UserProfile { @MaxLength(50) private String displayName; @MaxLength(value = 100, message = "Bio must be 100 characters or fewer") private String bio; UserProfile(String displayName, String bio) { this.displayName = displayName; this.bio = bio; } } // A simple runtime validator that reads @MaxLength via reflection static void validate(Object obj) throws IllegalAccessException { for (Field field : obj.getClass().getDeclaredFields()) { MaxLength maxLength = field.getAnnotation(MaxLength.class); if (maxLength != null) { field.setAccessible(true); String value = (String) field.get(obj); if (value != null && value.length() > maxLength.value()) { System.out.println("INVALID - " + field.getName() + ": " + maxLength.message()); } else { System.out.println("VALID - " + field.getName() + " (" + (value == null ? 0 : value.length()) + " chars)"); } } } } public static void main(String[] args) throws IllegalAccessException { UserProfile valid = new UserProfile("Ananya Sharma", "Java developer at a fintech startup"); System.out.println("=== Validating a well-formed profile ==="); validate(valid); System.out.println(); String longBio = "This biography is intentionally written to be longer than one hundred characters so that the MaxLength validation will trigger and report an error for this field."; UserProfile invalid = new UserProfile("Rahul", longBio); System.out.println("=== Validating a profile with an oversized bio ==="); validate(invalid); } }
Output:
=== Validating a well-formed profile ===
VALID   - displayName (13 chars)
VALID   - bio (36 chars)

=== Validating a profile with an oversized bio ===
VALID   - displayName (5 chars)
INVALID - bio: Bio must be 100 characters or fewer

How Annotations Work Internally

Annotation Types Are Interfaces

Under the hood, every annotation declaration is compiled into an interface that extends java.lang.annotation.Annotation. Each element declared in the annotation body becomes an abstract method on that interface, and the values you supply when applying the annotation become what those methods return when called at runtime.

SOURCE:
  public @interface MaxLength {
      int value();
      String message() default "Value exceeds maximum length";
  }

COMPILED TO (conceptually):
  public interface MaxLength extends java.lang.annotation.Annotation {
      int value();
      String message();  // default handled by the JVM proxy
  }

HOW RUNTIME ANNOTATION READING WORKS:

  field.getAnnotation(MaxLength.class)
       |
       v
  JVM checks: does this field's metadata (in the .class file's
              RuntimeVisibleAnnotations attribute) include MaxLength?
       |
  YES: JVM creates a DYNAMIC PROXY implementing MaxLength.
       Calling proxy.value() returns 50 (the stored value).
       Calling proxy.message() returns the default or stored value.
       |
  RETURNS: the MaxLength proxy object the validator code calls

RETENTION POLICY DETERMINES WHERE THE METADATA IS STORED:
  RetentionPolicy.SOURCE   -> discarded after javac runs, never in .class
  RetentionPolicy.CLASS    -> in the .class file's attributes, but NOT
                              in RuntimeVisibleAnnotations - reflection
                              cannot read it at runtime
  RetentionPolicy.RUNTIME  -> in RuntimeVisibleAnnotations - reflection
                              CAN read it while the program runs

Annotation Elements and Defaults

Annotation elements can have types from a restricted set: primitives, String, Class, enums, other annotations, and arrays of any of the above. A default value makes an element optional at the point of use. The special element name value allows the shorthand @MyAnnotation("x") syntax - but only when value is the only element being set.

// File: AnnotationElementsDemo.java import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; public class AnnotationElementsDemo { enum Severity { LOW, MEDIUM, HIGH, CRITICAL } @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @interface AuditLog { String action(); // required - no default Severity severity() default Severity.LOW; // optional - has default String[] tags() default {}; // optional array - empty by default } static class PaymentService { @AuditLog(action = "INITIATE_PAYMENT", severity = Severity.HIGH, tags = {"payment", "critical"}) public void initiatePayment(String orderId) { System.out.println("Initiating payment for: " + orderId); } @AuditLog(action = "CHECK_BALANCE") // severity defaults to LOW, tags defaults to empty public double checkBalance(String accountId) { System.out.println("Checking balance for: " + accountId); return 5000.0; } } public static void main(String[] args) throws Exception { System.out.println("=== Reading @AuditLog annotations at runtime ==="); for (Method method : PaymentService.class.getDeclaredMethods()) { AuditLog audit = method.getAnnotation(AuditLog.class); if (audit != null) { System.out.printf("Method %-20s action=%-20s severity=%-10s tags=%s%n", method.getName(), audit.action(), audit.severity(), java.util.Arrays.toString(audit.tags())); } } System.out.println(); PaymentService service = new PaymentService(); service.initiatePayment("ORD-1001"); service.checkBalance("ACC-7042"); } }
Output:
=== Reading @AuditLog annotations at runtime ===
Method initiatePayment       action=INITIATE_PAYMENT      severity=HIGH       tags=[payment, critical]
Method checkBalance          action=CHECK_BALANCE         severity=LOW        tags=[]

Initiating payment for: ORD-1001
Checking balance for: ACC-7042

Basic Example

A small custom annotation, applied to a field, then read back at runtime using reflection.

import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Field; @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @interface Info { String value(); } public class Main { static class Student { @Info("This is the student's full name") private String name = "Ananya"; private int age = 20; } public static void main(String[] args) throws Exception { Student student = new Student(); for (Field field : student.getClass().getDeclaredFields()) { Info info = field.getAnnotation(Info.class); if (info != null) { System.out.println(field.getName() + " -> " + info.value()); } else { System.out.println(field.getName() + " has no annotation"); } } } }

Output

name -> This is the student's full name
age has no annotation
  1. ›@interface Info declares a custom annotation with one element, value().
  2. ›@Retention(RetentionPolicy.RUNTIME) keeps the annotation available at runtime, and @Target(ElementType.FIELD) restricts it to fields only.
  3. ›The name field is annotated with @Info(...); the age field is not.
  4. ›main loops over Student's fields using reflection and calls getAnnotation(Info.class) on each one.
  5. ›When the annotation is present, its message is printed; otherwise a plain message is printed instead - this is exactly how frameworks like Spring read their own annotations at startup.

Best Practices

Always specify both @Retention and @Target on every custom annotation. The default retention is CLASS - not RUNTIME - which means reflection cannot read the annotation at runtime, a surprise that produces silent failures where the annotation appears to be ignored. The default target is all elements, which allows the annotation to be placed in locations where it makes no sense. Both defaults are almost never what a custom annotation author actually wants.

Name annotation elements with the intended usage in mind, and use value for the single most important element. @AuditLog(action = "PAYMENT") is readable; @AuditLog("PAYMENT") is only readable when action is named value. Reserve the value shorthand for annotations where one element is overwhelmingly the primary one - @SuppressWarnings("unchecked"), @RequestMapping("/orders").

Keep annotation elements to a minimum, and provide sensible defaults for everything optional. An annotation with six required elements where only two are commonly set is hard to use and hard to read at call sites. Defaults make annotations approachable; required elements should be only those without which the annotation genuinely cannot do its job.

Do not use annotations to carry business logic. An annotation is metadata - what the annotated thing is or how it should be treated. The annotation processor or the framework is where the actual behavior lives. An annotation element whose value encodes a conditional branch inside the processor is a sign the annotation is trying to be a DSL, which usually means the design needs rethinking.

For custom validators, prefer Jakarta Bean Validation over hand-rolled reflection. The pattern in this article's example - @NotNull, @Range, a reflective validator - is exactly how Jakarta Bean Validation (javax.validation/jakarta.validation) works, and that library is already present in every Spring Boot project. Using standard annotations gives you IDE support, integration with Spring's @Valid, and a tested implementation for free.

Common Mistakes

Mistake 1 - Missing RetentionPolicy.RUNTIME on an Annotation Read at Runtime

import java.lang.annotation.Target; import java.lang.annotation.ElementType; import java.lang.reflect.Method; // WRONG - no @Retention, so the default is RetentionPolicy.CLASS. // The annotation is in the .class file but NOT in // RuntimeVisibleAnnotations - reflection returns null at runtime. @Target(ElementType.METHOD) @interface LogCall {} class ServiceBroken { @LogCall void processOrder() {} } class BrokenReader { static void demo() throws Exception { Method method = ServiceBroken.class.getDeclaredMethod("processOrder"); LogCall annotation = method.getAnnotation(LogCall.class); System.out.println(annotation); // prints: null - annotation not visible } } // CORRECT - add @Retention(RetentionPolicy.RUNTIME) import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @interface LogCallFixed {}

Mistake 2 - Applying an Annotation to a Target It Was Not Designed For

import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; // @Target(ElementType.FIELD) means this annotation is for FIELDS only @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @interface FieldOnly { String value(); } // WRONG - placing a field-targeted annotation on a METHOD // This is a COMPILE ERROR: annotation type not applicable to this kind of declaration class MisappliedAnnotation { @FieldOnly("productName") // COMPILE ERROR public String getProductName() { return "Laptop"; } } // CORRECT - use the annotation only on field declarations class CorrectUsage { @FieldOnly("productName") private String productName; }

Mistake 3 - Using an Illegal Type as an Annotation Element

import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; // WRONG - annotation elements can only be: primitives, String, Class, // enum types, other annotation types, or arrays of the above. // List, Map, Date, and any other class type are NOT allowed. @Retention(RetentionPolicy.RUNTIME) @interface InvalidAnnotation { List<String> tags(); // COMPILE ERROR - List is not a valid annotation element type java.util.Date expires(); // COMPILE ERROR - Date is not a valid annotation element type } // CORRECT - use arrays and strings instead of collections @Retention(RetentionPolicy.RUNTIME) @interface ValidAnnotation { String[] tags() default {}; // array of String - valid String expires() default ""; // String - valid, caller supplies as "2026-12-31" }

Mistake 4 - Expecting @Inherited to Work on Interface Annotations

import java.lang.annotation.*; // @Inherited makes this annotation propagate from a SUPERCLASS // to its SUBCLASSES when read via reflection @Inherited @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @interface ServiceTier { String value(); } // WRONG ASSUMPTION - placing @ServiceTier on an INTERFACE and // expecting implementing CLASSES to inherit it. @Inherited only // works for CLASS-to-SUBCLASS inheritance, not for // INTERFACE-to-IMPLEMENTING-CLASS. @ServiceTier("payment") interface PaymentService {} class PaymentServiceImpl implements PaymentService {} // PaymentServiceImpl.class.getAnnotation(ServiceTier.class) returns null // CORRECT - @Inherited works for direct class inheritance only @ServiceTier("payment") class BasePaymentService {} class ConcretePaymentService extends BasePaymentService {} // ConcretePaymentService.class.getAnnotation(ServiceTier.class) returns the annotation

Interview Questions

Q1. What is an annotation in Java, and how is it different from a comment?

An annotation is structured metadata attached to a program element - a class, method, field, or parameter - that the compiler, the JVM, or a framework can read and act on programmatically. A comment is unstructured text that compilers and tools completely ignore. The key difference is that annotations have a defined type (@interface), can carry typed data (elements with specific types and defaults), survive into the compiled .class file (depending on retention policy), and can be read at runtime via reflection or processed at compile time by annotation processors. Comments cannot be read by any tool at runtime.

Q2. What is the difference between RetentionPolicy.SOURCE, CLASS, and RUNTIME?

RetentionPolicy.SOURCE means the annotation is discarded after compilation - it exists only in the source file and is useful only to tools that process source code before the compiler runs (like Lombok's annotation processors, which generate code and then the annotation is no longer needed). RetentionPolicy.CLASS (the default) stores the annotation in the compiled .class file but not in the RuntimeVisibleAnnotations attribute, so reflection cannot read it at runtime - this is used by bytecode analysis and instrumentation tools. RetentionPolicy.RUNTIME stores the annotation in RuntimeVisibleAnnotations, making it accessible via Class.getAnnotation(), Method.getAnnotation(), and related reflection methods while the application runs. Most custom annotations for framework-style processing need RUNTIME.

Q3. What are meta-annotations, and what does each one do?

Meta-annotations are annotations on other annotation declarations - they configure how the annotation behaves. @Retention specifies how long the annotation survives (SOURCE, CLASS, or RUNTIME). @Target restricts which kinds of elements the annotation can be placed on (TYPE, METHOD, FIELD, PARAMETER, CONSTRUCTOR, etc.). @Documented includes the annotation in Javadoc output for annotated elements. @Inherited makes a class-level annotation automatically visible on subclasses when read via reflection (this applies only to class annotations, not to interface annotations or any other kind). @Repeatable allows the same annotation to be applied multiple times to the same element, by specifying a container annotation that holds an array of the repeated annotation.

Q4. How are annotations read at runtime, and what is the role of reflection?

Annotations with RetentionPolicy.RUNTIME are stored in the .class file's RuntimeVisibleAnnotations attribute. At runtime, the reflection API exposes them through methods like Class.getAnnotation(AnnotationType.class), Method.getAnnotation(AnnotationType.class), and Field.getAnnotation(AnnotationType.class). When called, the JVM creates a dynamic proxy implementing the annotation's interface, with each element's method returning the value stored in the bytecode attribute. The caller calls annotation.value() or annotation.message() on this proxy to retrieve the element values - which is exactly what frameworks like Spring and Hibernate do to read their configuration annotations when the application starts.

Q5. What types are valid for annotation elements, and why is this restriction in place?

Annotation elements must be of one of these types: any primitive (int, long, double, etc.), String, Class (or a parameterized form of Class), any enum type, any annotation type, or a one-dimensional array of any of the above. No other types - no List, no Map, no arbitrary class instances - are allowed. The restriction exists because annotation values must be constants: they must be known and representable at compile time and storable in the class file's constant pool. List and Map instances are heap objects that cannot be represented as compile-time constants; arrays of primitives and strings can be.

Q6. What is the difference between compile-time annotation processing and runtime annotation reading?

Compile-time annotation processing uses the javax.annotation.processing.Processor API - annotation processors run during compilation, can read annotations on source files before they become class files, and can generate new source files, validate constraints, or report custom compiler errors and warnings. The generated files are then compiled as part of the same build. Lombok works this way: @Getter causes the processor to generate getter method source code during compilation. Runtime annotation reading uses the reflection API to access annotations from compiled class files while the application runs. Spring's component scanning works this way: it reads @Component, @Service, @Autowired, and similar annotations from already-compiled classes at application startup to configure the dependency injection container.

FAQs

Can an annotation have another annotation as an element?

Yes - annotation types are a valid element type. This is how composed or nested annotation designs work in frameworks. For example, a @SpringBootTest annotation might include a @BootstrapWith(SpringBootTestContextBootstrapper.class) element, where @BootstrapWith is itself an annotation. When reading the outer annotation at runtime, the inner annotation is also accessible as a proxy object with its own elements.

Why does @Override exist if the compiler would catch the wrong signature anyway?

Without @Override, a method that SHOULD override a superclass method but has a typo in the name or a slightly different parameter type simply becomes a new, unrelated method - no error, no warning, and the expected behavior silently stops working. @Override tells the compiler the developer's INTENT is to override, so if the method does not match any method in the supertype, the compiler reports an error at the annotation. It turns a subtle runtime bug into a compile-time error.

Can you annotate a lambda expression in Java?

Not directly in the way you annotate a named method. A lambda can be annotated if the functional interface it implements carries type annotations, or if the variable it is assigned to is annotated. The lambda expression itself cannot have @Override or @SuppressWarnings placed on it directly - those apply to named method declarations. However, parameters of a lambda can carry type annotations in some contexts, depending on how the functional interface is declared and which Java version is in use.

What happens if you define an annotation without @Target?

Without @Target, the annotation can be placed on any declaration - classes, methods, fields, parameters, constructors, packages, local variables - but NOT on type uses (which requires ElementType.TYPE_USE explicitly). This is almost never the right choice for a custom annotation, because an annotation meaningful only on fields being silently accepted on a class declaration misleads readers and produces silent, hard-to-debug processing errors when the processor finds it in an unexpected location.

Is it possible to make an annotation required, so the compiler enforces it must be present?

No - the Java language has no mechanism to require that a particular annotation must be present on a declaration. Annotations are always optional from the compiler's perspective. Enforcement can be achieved through compile-time annotation processors (a processor that scans all classes in a package and reports an error if a class implementing some interface lacks a specific annotation), through checkstyle or similar static analysis tools configured to enforce the presence, or through runtime checks at startup that reflect over all relevant classes and fail fast if the annotation is absent.

Are annotations inherited by subclasses?

Only class-level annotations with @Inherited are automatically inherited by subclasses when read via getAnnotation(). Without @Inherited, SubClass.class.getAnnotation(SomeAnnotation.class) returns null even if ParentClass carries the annotation. Method annotations are never inherited - if you want a subclass's overriding method to carry the same annotation as the parent, it must be re-applied explicitly. Interface annotations are also never inherited by implementing classes, even if the annotation is marked @Inherited.

Summary

Annotations are metadata - typed, structured, and readable by tools - attached to the program elements they describe. The built-in ones (@Override, @Deprecated, @FunctionalInterface) do specific, well-understood things at compile time. The meta-annotations (@Retention, @Target) control how every custom annotation behaves, and getting them wrong - especially defaulting to CLASS retention instead of RUNTIME - is the single most common source of "my annotation is being ignored" bugs.

Custom annotations follow the same four-step pattern every time: declare the annotation with @interface, set @Retention(RetentionPolicy.RUNTIME) and a specific @Target, annotate the fields or methods with the relevant data, and write the processor (a reflective validator, a framework startup routine, or a compile-time processor) that reads those annotations and acts on them. The Razorpay example above is that pattern at its most direct - constraints declared once on fields, processed generically by a validator that knows nothing about specific field names.

The annotation-to-processor gap is what interviewers at product companies probe: anyone can name @Override. Understanding that the annotation is inert without a processor, that retention controls whether reflection can reach the annotation, and that compile-time processors work before the code becomes a class file - that is what separates surface-level familiarity from genuine architectural understanding of how annotation-driven frameworks are built.

What to Read Next

Practice Programs

Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.

Browse all Java programs →