Meta-Annotations in Java

Naveen AnamaneniUpdated · 7 min read

A meta-annotation is an annotation that is placed on another annotation's declaration. It tells Java how that annotation behaves: how long it is kept, where it may be written, whether subclasses inherit it, and whether it can be repeated. Java provides five of them in java.lang.annotation: @Retention, @Target, @Documented, @Inherited and @Repeatable. In this lesson, you will learn what each one does, with programs that show the difference.

The Five Meta-Annotations

Meta-annotationIt is used to...
@Retentionsay how long the annotation is kept: source only, class file, or run time.
@Targetsay which kinds of declarations the annotation may be written on.
@Documentedinclude the annotation in the generated Javadoc of annotated elements.
@Inheritedlet subclasses inherit a class-level annotation from their superclass.
@Repeatableallow the same annotation to be written more than once on one element.

The examples below declare small annotations with @interface. The full syntax is explained in "Custom Annotations in Java".

@Retention: How Long Is It Kept?

PolicyKept in sourceKept in .class fileReadable at run timeTypical use
RetentionPolicy.SOURCEYesNoNoCompiler checks: @Override, @SuppressWarnings
RetentionPolicy.CLASS (the default)YesYesNoBytecode tools
RetentionPolicy.RUNTIMEYesYesYesFrameworks: Spring, JUnit, JPA
How far each retention policy carries an annotation

Let's see an example that puts one annotation of each kind on a class and asks which are visible at run time:

Ret.java▶ Try it
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
 
public class Ret {
    @Retention(RetentionPolicy.SOURCE)
    @interface InSource { }
 
    @Retention(RetentionPolicy.CLASS)
    @interface InClassFile { }
 
    @Retention(RetentionPolicy.RUNTIME)
    @interface AtRunTime { }
 
    @InSource
    @InClassFile
    @AtRunTime
    static class Report { }
 
    public static void main(String[] args) {
        Class<Report> c = Report.class;
        System.out.println("SOURCE visible?  "
            + c.isAnnotationPresent(InSource.class));
        System.out.println("CLASS visible?   "
            + c.isAnnotationPresent(InClassFile.class));
        System.out.println("RUNTIME visible? "
            + c.isAnnotationPresent(AtRunTime.class));
    }
}
Output
SOURCE visible?  false
CLASS visible?   false
RUNTIME visible? true

Explanation: Only the RUNTIME annotation can be read by the running program. The CLASS one is still in the file, though, as javap -v shows. It is stored in a separate "invisible" list that the JVM doesn't load:

javap -v Ret$Report (trimmed)
RuntimeVisibleAnnotations:
  0: #14()
    Ret$AtRunTime
RuntimeInvisibleAnnotations:
  0: #16()
    Ret$InClassFile

InSource appears nowhere: the compiler dropped it.

@Target: Where May It Be Written?

@Target lists the allowed places as ElementType constants:

ElementTypeAllows the annotation on
TYPEClasses, interfaces, enums, records, annotations
FIELDFields and enum constants
METHODMethods
PARAMETERMethod and constructor parameters
CONSTRUCTORConstructors
LOCAL_VARIABLELocal variables
ANNOTATION_TYPEOther annotation declarations
PACKAGE, MODULEPackage and module declarations
TYPE_PARAMETER, TYPE_USEType parameters, and any use of a type, such as List<@NonNull String>
RECORD_COMPONENTRecord components

Several can be given in braces: @Target({ElementType.METHOD, ElementType.FIELD}). Using an annotation in a place its target doesn't allow is a compile error:

Tg.java
@Target(ElementType.METHOD)
@interface Audited { }
 
@Audited
static class Account { }
Compiler output
Tg.java:8: error: annotation interface not applicable to this kind of declaration
    @Audited
    ^
1 error

Without @Target, an annotation can be used on any declaration.

@Inherited: Passing to Subclasses

By default, a subclass does not get its superclass's annotations. @Inherited changes that for class-level annotations:

Inh.java▶ Try it
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
 
public class Inh {
    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @interface Entity { }
 
    @Retention(RetentionPolicy.RUNTIME)
    @interface Audited { }
 
    @Entity
    @Audited
    static class Person { }
 
    static class Student extends Person { }
 
    public static void main(String[] args) {
        System.out.println("Student has @Entity?  "
            + Student.class.isAnnotationPresent(Entity.class));
        System.out.println("Student has @Audited? "
            + Student.class.isAnnotationPresent(Audited.class));
    }
}
Output
Student has @Entity?  true
Student has @Audited? false

Explanation: Student has no annotations of its own. Because @Entity is @Inherited, asking Student for it finds the one on Person. @Audited isn't inherited. @Inherited works only for classes: annotations on interfaces aren't inherited by implementing classes, and annotations on methods aren't inherited by overriding methods.

@Repeatable: Using It More Than Once

Normally the same annotation can appear only once on an element. To allow repeats, you declare a container annotation that holds an array, and name it in @Repeatable:

Rep.java▶ Try it
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
 
public class Rep {
    @Retention(RetentionPolicy.RUNTIME)
    @Repeatable(Schedules.class)
    @interface Schedule {
        String day();
    }
 
    @Retention(RetentionPolicy.RUNTIME)
    @interface Schedules {
        Schedule[] value();
    }
 
    @Schedule(day = "Monday")
    @Schedule(day = "Thursday")
    static class MathClass { }
 
    public static void main(String[] args) {
        Schedule[] all =
            MathClass.class.getAnnotationsByType(Schedule.class);
        for (Schedule s : all) {
            System.out.println("class on " + s.day());
        }
        System.out.println("direct @Schedule: "
            + MathClass.class.getAnnotation(Schedule.class));
        System.out.println("container: "
            + MathClass.class.getAnnotation(Schedules.class));
    }
}
Output
class on Monday
class on Thursday
direct @Schedule: null
container: @Rep.Schedules({@Rep.Schedule(day="Monday"), @Rep.Schedule(day="Thursday")})

Explanation: The compiler wraps the two @Schedule annotations inside one hidden @Schedules. That's why getAnnotation(Schedule.class) returns null: there is no single direct @Schedule. Use getAnnotationsByType, which looks inside the container. Without @Repeatable, writing an annotation twice fails with "Tag is not a repeatable annotation interface".

@Documented: Show It in Javadoc

By default, Javadoc doesn't show which annotations a class or method carries. If the annotation is part of the public contract, such as "this method is experimental", mark its declaration with @Documented and it appears in the generated documentation of every element that uses it. @Deprecated and @FunctionalInterface are both @Documented.

Common Mistakes

Forgetting @Retention(RUNTIME). The annotation compiles and is silently invisible to reflection.

Expecting @Inherited to work for methods or interfaces. It applies only to annotations on classes, through extends.

Calling getAnnotation on a repeated annotation. It returns null; use getAnnotationsByType.

Giving the container a shorter retention than the repeatable annotation. The container must be kept at least as long.

Interview Corner

What are meta-annotations in Java?

Meta-annotations are annotations applied to annotation declarations. @Retention sets how long an annotation is kept (SOURCE, CLASS, which is the default, or RUNTIME). @Target restricts where it may be used through ElementType values. @Documented makes it appear in Javadoc. @Inherited lets subclasses inherit a class-level annotation. @Repeatable allows several uses on one element by naming a container annotation.

What is the difference between the retention policies?

SOURCE annotations are discarded by the compiler and exist only for compile-time checks. CLASS annotations are written to the class file but not made available at run time. RUNTIME annotations are in the class file and loaded by the JVM, so they can be read with reflection; frameworks need this policy.

Practice

  1. Easy: Change AtRunTime in Ret to have no @Retention at all, and check whether it is still visible.
  2. Harder: Create a repeatable @Author(name = ...) annotation for classes, apply it three times, and print the names. Then make it @Inherited and test on a subclass.

Quick check: Meta-annotations

4 questions
1What is the default retention policy?
2Which retention is needed to read an annotation with reflection?
3What does @Inherited apply to?
4How do you read all uses of a repeatable annotation?

Key Takeaways

Was this tutorial helpful?

Lesson 464 of 482