mattone
dopo mattoneTHE BOOK SERIES
IT/EN
← Java guide

Java 25 · 14/39

14. Annotations: metadata and checks

Java 25 · Complete guide · Draft under review

This guide retains the book’s draft status. Editorial review and comprehension checks with independent readers remain to be completed.

Search the whole guide →

Information the program can read

In the previous chapter we used @Override before a method. Those few letters are not the method body: they declare to the compiler the intention to override an inherited method. If we got the signature wrong, the compiler stops us. Without the annotation, a method of the same name but different parameters could merely be an overload, and our mistake would go unnoticed. For example, if a vehicle method accepts int and the subclass accidentally writes double, @Override immediately reveals the mistake.

An annotation associates metadata with a declaration or, in some cases, a type use. Metadata are data about the program: not the vehicle speed or book title, but information a compiler, library or tool can interpret. An annotation's presence does not automatically execute an action. @Override has an effect because the language assigns it one; an annotation we invent gains an effect only if a component reads it. This distinguishes the Java mechanism from behavior added by external frameworks.

Definition – Annotation and processor. The annotation is the data written in source. An annotation processor is a program invoked during compilation to examine program elements and, where specified, produce diagnostics or new files. Reflection instead can read annotations retained until runtime while execution is underway. These are three distinct moments.

Declaring an intention does not make it happen

Declarative programming helps us understand why so many projects use annotations. When we write an SQL query such as SELECT title FROM books WHERE available = true, we declare the desired result; the engine decides how to traverse indices and data pages. Java remains a language in which we normally explicitly describe steps, but some features let us add intentions other components interpret. Writing @Tag("catalog") above a class is closer to “this element belongs to the catalog” than “execute these instructions now”.

The SQL comparison has an important limit. A query has an engine with defined semantics; our annotation alone has no engine. We can annotate a class @ToSave and discover nothing is saved if we have not written or installed the component reading that metadata. Before adopting a declarative style, therefore ask three things: who interprets the declaration, when, and what error do we get if it is inconsistent? For @Override the answer is the compiler. For @Tag in the following program it is our reflection code. For some framework annotations it is an external library, perhaps launched when the application builds its components.

This distinction avoids attributing mysterious powers to the at sign. Annotations were introduced in Java 5 to associate structured information with program elements. They can help tools, compilation checks and libraries, and sometimes reduce separate configuration files. But moving a rule from an XML file to an annotation does not automatically make it simple: the rule still needs meaning, a reader and a test. Throughout the chapter we will use the same guiding question: if I remove this annotation, which component changes behavior and how can I see it?

Defining a small annotation

In Tag.java we declare an annotation assigning words to a class. Its only element is called value, so @Tag("catalog") is the short form of @Tag(value = "catalog").

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(Tags.class)
public @interface Tag {
    String value();
}

The syntax @interface declares an annotation type. String value(); is not an ordinary method to implement in an application class: it defines the element the annotation's user must supply. We can assign a default with default, for example String value() default "general";; use without arguments then becomes valid. Allowed element types include primitives, String, Class, enums, other annotation types and arrays of those types. An arbitrary object, such as List<String>, is not allowed. The Java 25 specification establishes these constraints.

The three annotations preceding the declaration are meta-annotations: annotations describing an annotation type. @Target(ElementType.TYPE) limits use to classes, interfaces, records and enums. @Retention(RetentionPolicy.RUNTIME) lets reflection find values at runtime. @Repeatable(Tags.class) permits several @Tag annotations on the same element; Tags is the container annotation required to represent them. The complete file declares it with a Tag[] value() element and consistent target and retention.

Retention policies answer different questions. With SOURCE, the annotation serves the source and is not kept in the .class file; with CLASS, it is kept in the compiled file but not made available by ordinary runtime reflection; with RUNTIME, it can be read during execution. If we omit @Retention, the default is CLASS. Figure 14.1 connects the three policies to the moments when information is available.

Annotation availability during compilation and execution
Figure 14.1 – SOURCE, CLASS and RUNTIME indicate the phase up to which an annotation persists. The policy alone does not decide what work will be performed on that metadata.

In DemoAnnotations.java we write two tags on a class and read them with getAnnotationsByType(Tag.class). The loop prints catalog and teaching. The API returns both even when repetition is represented through the container. If we changed retention to CLASS, this example would not find the tags at runtime: code would compile, but the loop would not print the two values. This is a useful experiment showing that metadata availability is part of the contract.

Familiar annotations, different responsibilities

@Override requests an overriding check. @FunctionalInterface requests that an interface have one abstract method usable as a lambda target; we will see it in chapter 16. @Deprecated(since = "25", forRemoval = false) communicates that an API is no longer recommended and can record when that indication began; forRemoval signals a removal intention, not an automatic date. @SuppressWarnings limits a compiler diagnostic in a specific scope: use it after understanding and justifying the warning, not to hide problems.

@SafeVarargs concerns methods and constructors with generic or parameterized-type variable arguments. It is a safety promise by the method's implementer, not a cure the compiler applies to the body. In chapter 15 we will see why arrays and generics can produce heap pollution and when that promise is justified. Putting all these annotations under “documentation” would hide important differences: some activate language checks, others communicate information to APIs or tools.

A type-use annotation appears on a use of a type, for example List<@NonNull String> if a suitable annotation and a tool interpreting it exist. The Java platform alone does not activate a universal non-nullness check for a custom @NonNull annotation. We must distinguish the syntax location where metadata can be written from the concrete check somebody performs. The same applies to library annotations generating code or configuring a framework: without that tool they do not produce the promised effect.

A processor executed during compilation

The TagProcessor.java processor extends AbstractProcessor. It declares annotation names it observes and the supported language version, then reads annotated elements in process. When it encounters DemoAnnotations, it writes a compiler note: tag present on DemoAnnotations. The message appears during javac, before main launches. The Java 25 Processor contract explains that processing can proceed through several rounds: a processor can generate sources entering a later round.

To reproduce the example, from the three sources' folder first execute javac --release 25 -Xlint:all -d build Tag.java DemoAnnotations.java TagProcessor.java. Then explicitly request compilation with the processor: javac --release 25 -cp build -processorpath build -processor TagProcessor -proc:only DemoAnnotations.java. The option -proc:only performs processing without generating a new class from the examined source. We do not assume javac automatically discovers and executes the processor from the folder: the command declares the processor and path. This distinction also matters for build security and reproducibility.

The minimal processor only produces a note. A real one could generate sources or report an error using Messager, but does not arbitrarily modify the supplied source. Building a robust processor also requires rules for type names, rounds and generated files. Here it is enough to have seen where it acts and how to verify it acted.

What the processor actually reads

The annotations parameter of process contains annotation types relevant to that round; RoundEnvironment lets us ask which program elements are marked by them. An element of the compilation model can be a class, method, field or other declared construct. It is not a Java instance created by new during application execution. In our processor we read element.getSimpleName() and obtain DemoAnnotations, the declared class's name. This distinction explains why a processor can operate even when the program has no executable main yet.

round.processingOver() signals that processing reached the final round. In the processor we avoid emitting the note in that phase. A processor generating new source can cause another round in which that source is examined in turn. If generated code carries other recognized annotations, work can continue. Design must avoid repeatedly generating the same file or relying on random element-visit order.

Our example uses @SupportedAnnotationTypes and @SupportedSourceVersion to tell the compiler what it handles. SourceVersion.RELEASE_25 makes the language version used for this test explicit. In a library supporting several versions, the choice must be checked against supported JDKs, not mechanically copied. The return false in process indicates the processor does not exclusively claim those annotations: other processors can still receive them according to the protocol. It does not change printing the two runtime tags.

Further reading – Generating without modifying. A processor can ask Filer to create new source or a resource. The original annotated file remains input; it is not rewritten as though the processor were a text editor. A project can then compile author-written and generated sources together. This requires a build controlling output paths, processor provenance and reproducibility, because code executed during compilation is part of the program's production chain.

When the processor becomes a build dependency

In the small experiment we directly indicated -processor TagProcessor. In a library the processor can be distributed in a JAR and declared as a service through META-INF/services/javax.annotation.processing.Processor, with its implementing class's fully qualified name. This is how a build tool can discover a processor on the configured path. But “discovering” does not mean relying on chance: for a repeatable build we declare which JAR supplies the processor, which version we use and in which phase it must run. Maven compiler-plugin configuration, for example, belongs to the project just as much as the library version used at runtime.

A processor is code the compiler executes while building the application. If we download an unverified dependency and let it execute during the build, consequences are not limited to a wrong annotation in .class: that code can read files and generate output. Chapter 29 will address dependency provenance and build repeatability; here the connection is concrete. An expected diagnostic such as tag present on DemoAnnotations proves the processor was invoked, while the mere presence of @Tag in source does not.

A processor producing a class, for example GeneratedCommandCatalog, must coexist with several compilation rounds. In the first it sees @Command and generates source; in the next the compiler also analyzes that new class. If the processor attempts to recreate the same file each round, the build fails or becomes dependent on visit-order details. This is why we distinguish the phase of gathering information from the phase of emitting a result, check processingOver() and test at least one clean compilation. When an incremental build produces a different outcome from clean verify, it is a signal to investigate, not a reason to consider clean builds unnecessary.

This clarifies an important distinction in terminology. An annotation processor works on the program model exposed by the compiler and can create new files; it is generally not a simple textual preprocessor substituting words before javac. Some external tools use further mechanisms to transform what the compiler sees. When reading tool documentation, we must distinguish the standard javax.annotation.processing API contract from that library's specific capabilities. It is the same discipline we applied to @Override and reflection: identify the actor before attributing an effect to the annotation.

A concrete choice for @Target and @Retention

Suppose we want to annotate methods exposing a command to the user. Writing @Target(ElementType.METHOD) prevents accidentally placing the annotation on a class. If the program must discover commands at startup through reflection, RetentionPolicy.RUNTIME is needed. If instead a processor generates a command table during compilation and the application uses only that table, runtime retention might be unnecessary. The choice comes from the intended metadata reader, not a feeling that RUNTIME is always “more complete”.

Reflection uses AnnotatedElement, implemented by representations such as Class and Method. Methods reading a single annotation and those reading repeatable annotations have distinct contracts; getAnnotationsByType is the form suitable for our example. Inheritance matters here too: a meta-annotation such as @Inherited has precise rules for certain class annotations and does not make every method or interface annotation inheritable. Rather than trusting the name, verify the selected meta-annotation's contract.

An annotation can document a decision, request checking or provide configuration. It is useful when metadata permanently belongs to the declared element and can be read by a defined tool. If understanding a function's flow requires following ten framework annotations and external rules, compact syntax has not automatically made the program simpler. The reader must know which part is Java, which the library and which build configuration.

Important note – Reflection is not compilation. DemoAnnotations.class.getAnnotationsByType(...) reads runtime metadata. TagProcessor observes the program model during compilation. RUNTIME retention is necessary for the first use, but is not a general condition for processing a source annotation with a processor.

The target is part of the contract

ElementType describes the places where an annotation can be applied. We need not memorize it: we must recognize that metadata on a class and on a type use answer different questions. If @Responsible describes who maintains a method, METHOD is a sensible target. If @Unit describes a number used as meters or seconds, we may want to annotate type use, not a class declaration: that is the TYPE_USE case. The second example does not magically check units; it requires an analyzer knowing the rules.

Target Element it marks Question to ask
TYPE, ANNOTATION_TYPE Classes, interfaces, records, enums or annotation declaration Does metadata describe the whole type or another annotation type?
FIELD, METHOD, CONSTRUCTOR Declared field, method or constructor Does checking concern that operation or the whole class?
PARAMETER, LOCAL_VARIABLE Formal parameter or local variable Who can still observe the information after compilation?
PACKAGE, MODULE Package or module declarations Does configuration belong to the whole boundary?
TYPE_PARAMETER, TYPE_USE Generic parameter or use of a type Are we discussing declared T or a point where the type is used?

For several targets we write an array, for example @Target({ElementType.TYPE, ElementType.METHOD}). Without @Target, an annotation applies to declaration contexts allowed by default rules, but does not automatically acquire every type-use context. Defining the target narrows possible errors: if @Command applies only to methods, the compiler rejects a careless use on the class. But the table does not replace design. An annotation applicable everywhere is rarely useful if its reader interprets only methods.

The local-variable case is particularly instructive. An annotation on a local variable declaration does not become queryable with ordinary reflection merely because we chose RUNTIME. That variable is not a member we can look up like a field. Annotating a variable's type use is different: retention rules in .class are specified separately. Confusing these positions leads to experiments where source seems correctly annotated but the program finds nothing. Java 25's specification distinguishes the cases; annotation designers must know which information they want to recover and with which API.

Observing retention and inheritance without guessing

In DemoPolicies.java we place @SourceOnly, @InClass and @AtRuntime on the same class. Compilation accepts all three. Reflection finds only one: that with RUNTIME retention. We are not saying the other two were useless; a source analyzer can read the first, and a bytecode inspection tool the second. We are saying this test uses the ordinary reflection API and therefore observes only what its contract permits.

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

public final class DemoPolicies {
    @Retention(RetentionPolicy.SOURCE)
    @Target(ElementType.TYPE)
    @interface SourceOnly { }

    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.TYPE)
    @interface InClass { }

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @interface AtRuntime { }

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @interface NotInherited { }

    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @interface InheritedTag { }

    @SourceOnly
    @InClass
    @AtRuntime
    static class Document { }

    @NotInherited
    @InheritedTag
    static class Base { }

    static class Derived extends Base { }

    private DemoPolicies() { }

    public static void main(String[] args) {
        System.out.println("Document at runtime: "
                + Document.class.getAnnotations().length);
        System.out.println("Base declared: "
                + Base.class.getDeclaredAnnotations().length);
        System.out.println("Derived declared: "
                + Derived.class.getDeclaredAnnotations().length);
        System.out.println("Derived visible: "
                + Derived.class.getAnnotations().length);
    }
}

The output is Document at runtime: 1, Base declared: 2, Derived declared: 0, Derived visible: 1. The distinction between the last two numbers explains @Inherited better than its name. getDeclaredAnnotations() looks only at what is directly declared on Derived, so finds zero. getAnnotations() also considers the base class's inheritable annotation and finds @InheritedTag. @NotInherited, despite runtime retention, does not pass to the subclass. @Inherited operates on class annotations according to precise rules; it does not inherit method annotations or turn an interface annotation into a universal implementation attribute.

We can compile and run the file with javac --release 25 -Xlint:all -d build DemoPolicies.java and java -cp build DemoPolicies. If we change @InheritedTag from RUNTIME to CLASS, the last number will no longer be one: metadata is no longer exposed to reflection. If we remove @Inherited while keeping RUNTIME, Base continues to show two declared annotations but Derived sees none. These are different changes and the test lets us attribute the result to the right property.

Returning to the @Override mistake

The historical vehicle example deserves working through to the diagnostic. Imagine Vehicle declares void accelerate(int increment) and Car writes void accelerate(double increment). The signatures differ in parameter: the second declaration creates an overload, it does not override the method received from the base class. A call through a Vehicle variable will therefore continue using the method accepting int. If the programmer wanted to change the car's behavior for that call, they made a mistake that unannotated code could hide.

Writing @Override before the method with double makes the compiler report that it is neither overriding nor implementing the expected method. The correction is not removing @Override to make the message disappear: it is realigning the signature to int if that was the intention. If an additional method for double values is truly needed, keep it as an overload and document it without promising overriding. This case clearly shows why a useful annotation is not decoration: it makes checkable an intention the method syntax alone does not express.

From the annotation's form to its reader

Annotations can have no elements, contain one value or declare several. The distinction is useful when connected to code interpreting them. An annotation without elements, often called a marker, communicates its presence: @Checked carries no argument and the interested tool asks whether the marker exists. A type with only String value(); permits short form @Tag("catalog"). With two elements, for example String name(); int priority() default 0;, the user must write the mandatory element's name: @Command(name = "save"); absent priority is zero. There is no arbitrary object constructed with new to store as an annotation value.

An element value must belong to the types permitted by the specification and be expressible according to annotation rules. null is not permitted. For a String we cannot use concatenation depending on input read at runtime: information must be recordable in the compiled program. An array is allowed as an element type, but does not thereby become a List<String> collection. This limitation is not an accidental defect; it lets tools read metadata with a defined representation, even before application classes are instantiated.

The decisive question remains: who reads the metadata? If a processor produces a file during compilation, test the result by compiling with that processor and checking the generated file or diagnostic. If a library reads reflection at runtime, test by launching the program and choosing RUNTIME. If the annotation serves only source checking, keeping it until runtime may add no value. Two visually identical annotations can therefore live in different phases and have different effects because their readers differ.

A declaration can appear several times

In the historical book example a service needed to send a message at several times. A single annotation @Scheduler(hour = 12, ...) described one appointment; repeating it on the same class was initially a compilation error. The need remains current: an element can have several tags, rules or activation times. @Repeatable permits repetition, but requires a container annotation with a value() element returning the array of repeated annotations. In practice the container provides a representation in which values can be gathered.

DemoReminders.java uses two times. We chose TYPE for the annotation and container because metadata describes the Notifications class. If we wanted to schedule individual methods, we would change both targets to METHOD and code finding annotations would need to examine those methods. Retention is RUNTIME because the program reads times during execution. Without a component actually scheduling and sending the message, however, this example prints times: it is not a working scheduler.

import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

public final class DemoReminders {
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Repeatable(MultipleReminders.class)
    @interface Reminder {
        int hour();
        int minute();
    }

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @interface MultipleReminders {
        Reminder[] value();
    }

    @Reminder(hour = 9, minute = 30)
    @Reminder(hour = 17, minute = 0)
    static class Notifications { }

    private DemoReminders() { }

    public static void main(String[] args) {
        for (Reminder r : Notifications.class.getAnnotationsByType(Reminder.class)) {
            System.out.printf("%02d:%02d%n", r.hour(), r.minute());
        }
    }
}

The output is 09:30 and 17:00. getAnnotationsByType requests annotations of the repeated type and returns both. If we seek only generic presence or use another reflection method without understanding the container's role, we may observe a different representation. Removing @Repeatable while keeping both uses is a negative test: the compiler rejects duplication. Keeping @Repeatable but giving the container shorter retention than the repeated annotation is another error: compatibility rules are checked during compilation. An inconsistency between @Scheduler’s target and the container’s would invalidate the declaration. Declaring target and retention together lets us check their compatibility.

@Repeatable is not an alternative to a data structure when times must change during execution. Annotations belong to the compiled program. If an operator must move the 17:00 notification to 18:00 without recompiling, those times belong in configuration or persisted data. Choosing to put data in an annotation is appropriate only when data lifecycle matches the code declaring them.

Lombok and the platform boundary

Lombok is an external tool that can generate repetitive code from annotations. To read a project using it, we must place it correctly: Lombok is neither a Java keyword nor part of the standard library. A project using it depends on the library version, build configuration and development-tool support. Source using @Getter may seem to lack a method caller code uses; truly understanding the program requires seeing which method was generated and which compilation produced it.

Lombok annotations such as @Getter, @Setter, @ToString and constructor annotations solve different repetitions. Generating a setter for every field is not automatically good encapsulation: an object maintaining invariants may need meaningful operations rather than indiscriminate state access. For simple immutable data aggregates, before introducing an external dependency consider records encountered in C07; they do not cover every ordinary-class use, however. If a project adopts Lombok, review must show source, configuration and resulting API without attributing behavior to the presence of @ alone.

Important note – Not all annotations generate code. @Override checks a language relationship; @Tag in our example is readable at runtime; TagProcessor produces a compilation note; Lombok adds external processing. Calling them all “preprocessors” would hide action timing and responsibility.

From historical BoilerplatePerson to a design decision

A BoilerplatePerson class with six private fields requires constructors, getters and setters, as well as equals, hashCode and toString when its contract requires them. This volume of code makes the repetition cost concrete and invites us to choose the form suited to the object’s role. If Person is a container of first and last name traveling between two components and must not change, in Java 25 a record Person(String firstName, String lastName) { } provides a canonical constructor, accessors and component-based implementations of Object methods. It is a stable language possibility without an external processor. But it does not replace a class owning persistent identity, mutable fields, particular invariants or a framework protocol requiring a different form. First describe the object's role, then compare the form costing less to maintain.

If the project uses Lombok, we can express the historical class with @Getter, @Setter, @NoArgsConstructor, @AllArgsConstructor, @EqualsAndHashCode and @ToString. Each answers a distinct piece of repetition. @Getter produces an accessor; @Setter produces a mutation; constructor annotations choose available parameters; the last two decide textual representation and equality. @Data combines some of these choices, but its brevity makes knowing generated methods even more important. We must not add every setter merely because it is convenient: a Person that cannot have an empty name needs a check in its constructor and mutation operations. A generated setter directly assigning a field can bypass the rule we intended to impose.

Equality deserves separate checking. If equals and hashCode depend on firstName and lastName, two people with the same names are equal by that definition even if they are different domain individuals. If we change a last name after inserting the object into a HashSet, its hash can change and the collection may not find it where it was placed. This is not a Lombok-specific defect: it follows from equality based on mutable state. Code generation does not choose domain identity for us. Similarly, a toString including a confidential field can put it in logs; excluding it in annotation configuration is a security decision, not an aesthetic one.

Consider two Person objects: compare their equality and then modify the second with setters. false between different names is predictable, but does not prove equality correct for every state or hash stable over time. The number printed by hashCode() must not be copied as a universal expected result: it depends on the concrete rule and data. A useful test compares invariants: objects we decide are equal must give equal hashes; forbidden modifications must be rejected; toString must not expose secrets. This way the reader sees why reducing lines is only part of the problem.

The official Lombok feature documentation describes available annotations and the delombok command, which produces expanded source. This can help review or another tool needing to read generated code. For @EqualsAndHashCode, the official page explains default field selection and how to change it. The choice must be checked with the actual version declared in the build. Lombok can also interact with IDEs and compilers in ways varying between versions: this is why we do not print a compilation command here without a fixed, tested dependency. The chapter's teaching program uses only the JDK; Lombok is a guided comparison with the Person class, not a hidden dependency.

Other meta-annotations and other checks

@Documented deserves an explicit decision when an annotation is part of a library's public contract. It signals documentation generation to represent that annotation's use in the annotated element's documentation. It does not execute a check or change selected retention. An internal annotation helping only the compiler might not need to appear on the public page; one describing an API-user restriction might be useful to show. As always, the label's presence does not make documentation true or complete: we must read the generated result.

With @SuppressWarnings("unchecked") we can contain a warning after examining a cast the compiler cannot prove safe. In chapter 15 we will encounter exactly that situation with generic types and erasure. Putting suppression on an entire class to silence one line widens the blind area: other warnings of the same category might later appear unnoticed. The good habit is to keep suppression in the smallest possible place, explain the invariant making it acceptable and accompany it with a test. @SafeVarargs is even more demanding: it is the method author's declaration about the safety of variable-argument use, not a setting to hide an annoying message.

A @Deprecated without a migration path leaves the caller halfway. If we keep an old API for compatibility, Javadoc should say which method to use and which behavioral difference to expect. since records the version from which the API is deprecated; forRemoval communicates planned future removal, but fixes no date. A team finding the annotation can decide whether to migrate immediately, but must know the release and change risk. This is another metadata example helping only if someone reads it and its message is precise enough to act on.

To close the circle, return to the initial question: which problem does metadata solve? @Override makes a signature verifiable; @Retention and @Target govern where information lives; @Inherited defines limited subclass visibility; @Repeatable represents several values; a processor and Lombok can produce diagnostics or code. Common syntax does not erase these distinctions. If, when facing a new annotation, we know to seek its reader, operating moment and proof of the result, we have learned to reuse the model outside this chapter too.

To verify

Run DemoAnnotations, DemoPolicies and DemoReminders. Before each execution write expected lines and explain which API reads the metadata. Then remove @Repeatable while leaving both class annotations: predict the compiler diagnostic. As a second test, change retention to CLASS, recompile and run: explain why the processor can still see the source annotation while reflection does not show it. For an application case, define a @Responsible annotation on a method and first decide who must read it and in which phase: compiler, processor or running application.

Try the examples

Running the programs requires JDK 25. Download the individual Java files linked in the chapter or the complete example project, which includes instructions and a launcher script. The explanations also compare expected output: predict it before running the program.

Massimiliano Tarquini · CC BY-NC 4.0

Back to the top ↑