Java Annotations
Learn what the @ symbol means without treating it as magic. Use compiler-checked annotations, read deprecation metadata, and define a small runtime annotation whose meaning comes from a reader you can understand.
Before you start: review inheritance, overloading, and interfaces. All runner examples are complete Java 21 Main.java programs with no input required. Execution requires the isolated sandbox; no website-host fallback exists.
What you will be able to do
- Separate annotation metadata from executable method bodies.
- Use Override to catch an accidental overload.
- Read deprecation information and keep suppression narrow.
- Explain FunctionalInterface without requiring lambda syntax.
- Define annotation elements, defaults, targets, and retention.
- Read and validate explicitly declared metadata without invoking lesson methods.
1. An annotation describes code; a consumer gives it meaning
You have already seen @Override above methods in earlier lessons. The @ syntax attaches structured metadata to a program element. That metadata is not a replacement for the method body. Ask who consumes it: the compiler, a documentation/build tool, an annotation processor, or application/framework code at runtime.
For @Override, the compiler is the consumer. It checks whether the method really overrides or implements an appropriate supertype method. The actual greeting still comes from the text method body and normal interface dispatch. Deleting a valid @Override does not remove overriding behavior, but it removes this useful check on your intention.
A custom annotation has no automatic business effect just because its name sounds powerful. A marker called Ready does not schedule work, and a label called Required does not validate data by itself. Someone must implement that interpretation. We will deliberately read metadata in a small helper later rather than implying a framework is present.
State your overriding intention
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Welcome to Java
2. Use Override to catch a signature mistake early
The base class declares title() with no parameters. The child declares title(String prefix), which is a different signature and therefore an overload, not the intended override. A BaseLesson reference calling title() still uses the inherited base implementation, so this program prints Generic lesson instead of the expected Java-specific title.
Add @Override immediately above the child method in the runner, leaving its parameter untouched. Compilation should now fail because the declared intention does not match a supertype method. Correct the signature to title(), keep @Override, and run again. The repair exercise provides the same progression with an explained solution.
Do not remove an annotation solely to make the compiler complaint disappear. First decide whether the method was meant to override or overload. Parameter types, parameter count, return compatibility, visibility, and static-versus-instance rules still matter. The annotation catches an intention mismatch; it does not rewrite the signature for you.
A similar name does not guarantee an override
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Generic lesson
Try the compiler check before fixing the signature
Add @Override above title(String prefix). The compile error identifies the mismatched intention. Then remove the parameter, keep the annotation, and run again. Overriding comes from the valid signature, not from the marker alone.
3. Deprecated means discouraged, not disabled
@Deprecated marks an API that callers are discouraged from using. It can remain callable, as this example shows. The since element describes when the API was deprecated, not the running JDK version. forRemoval=true signals an intention to remove it in a future version; it does not itself remove the method today.
A migration needs explanation as well as metadata. The Javadoc @deprecated tag documents why the old method is discouraged and what replacement to consider. That documentation tag and the Java annotation serve different consumers. Check replacement semantics rather than blindly substituting a name when behavior or error handling may have changed.
The call here crosses a top-level class boundary so the compiler can report deprecation usage; exact warning formatting depends on compiler options. Runtime output is listed separately from compiler diagnostics. The tiny reflective read inspects since and forRemoval as ordinary metadata; it does not invoke the old method through reflection.
Deprecated code remains callable
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Legacy label
Current label
Since: 2.0
For removal: false
4. Suppress a justified warning narrowly, not a real defect
Sometimes a compatibility boundary must call a deprecated API while callers migrate. @SuppressWarnings("deprecation") can silence that compiler warning within a limited declaration. The sample places it on one named bridge method with a comment explaining why the old call still exists, leaving unrelated code outside that suppression.
SuppressWarnings does not fix runtime behavior or turn an invalid Java program into a valid one. It cannot make a false @Override correct, prevent a ClassCastException from an unchecked raw write, or validate a null reference. Recognized warning names and compiler flags affect which diagnostic categories can be suppressed.
Prefer removing the underlying cause when practical, and keep any remaining suppression as small and documented as possible. Removal-intent warnings are a different category from ordinary deprecation warnings. Do not apply broad unchecked suppression to an entire class merely because the generic lesson revealed a bad insertion that should instead be typed correctly.
Keep a justified warning suppression at its boundary
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Compatibility label
5. FunctionalInterface checks one abstract-method contract
A functional interface has one abstract-method contract, subject to Java’s interface rules. @FunctionalInterface asks the compiler to verify that intention. Labeler declares one abstract label method and also has a default help method with a body, so the default method does not introduce another abstract requirement.
The example implements the interface through an anonymous class, which you already know. Lambda expressions offer another syntax for compatible functional contracts, but are outside this course. The annotation is not what makes an otherwise valid interface functional, and adding it does not automatically execute label or convert every interface into a lambda target.
Try adding a second unrelated abstract method to Labeler. The annotation should trigger a compiler error; the anonymous implementation also no longer satisfies the expanded contract. Conversely, adding another default or static method does not by itself create another abstract obligation. Keep method-contract checks separate from runtime behavior.
Check a functional contract without using a lambda
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Topic: Java
Provide one topic name.
6. Define a small custom annotation with explicit defaults
@interface CourseInfo declares an annotation interface. Its elements look like parameterless method declarations: value is a required String, and minutes has a default of 20. These declarations define metadata fields, not arbitrary method bodies to run. Annotation element types are restricted, including primitives, String, Class, enum/annotation types, and arrays of allowed element types.
@CourseInfo("Java annotations") is shorthand for setting the element named value. Because minutes is omitted, its declared default applies. Annotation values cannot be null, and many supplied values must be compile-time constants of the permitted type. A computed runtime String or an arbitrary List is not interchangeable with an annotation element.
The explicit runtime-retention and type-target declarations make the example’s intended use clear. Main.class.getDeclaredAnnotation reads the annotation directly on Main. In reusable code, that lookup can return null if the annotation is absent; our example knows it is present, while the lab defines an explicit missing-metadata response.
Read a custom runtime annotation
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Title: Java annotations
Minutes: 20
7. Target and retention answer different questions
@Target controls permitted attachment locations, such as TYPE or METHOD. It does not decide how long metadata survives. @Retention controls survival: SOURCE is discarded during compilation, CLASS is recorded in the class file but not exposed through ordinary runtime annotation reflection, and RUNTIME is retained for reflective access.
If Retention is omitted, the default is CLASS, not RUNTIME. The example’s BuildNote is therefore not visible through isAnnotationPresent, while RuntimeNote is. A runtime reader cannot compensate for missing runtime retention by simply searching harder with the same reflection API. Choose the policy when defining the annotation.
Target misuse is a compiler issue: a TYPE-only annotation cannot simply be placed on a method. Annotation inheritance is another separate policy; @Inherited concerns certain class-level lookup behavior, not universal propagation to methods or interfaces. The lab uses getDeclaredAnnotation deliberately and does not search superclasses or implement framework-style merging.
Retention controls what reflection can see
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Default visible: false
Runtime visible: true
8. Reading metadata is not calling a lesson method
The Ready marker is attached to prepare, but the call counter starts at zero. Reading the method’s annotation leaves it at zero. Only the explicit prepare() call increments it. The example isolates the difference between locating metadata and executing the annotated operation, with no framework dispatcher hiding those steps.
Our lab will read a class-level lesson description, validate its title and duration, and format a report. It will not instantiate the lesson class or invoke its annotated methods. That intentionally small boundary makes it easier to explain exactly where behavior comes from. Metadata remains input, so annotation presence alone does not establish valid business values.
Some libraries interpret annotations for routing, persistence, tests, or validation. Their behavior comes from the library and its configuration, not from the @ symbol alone. Do not use this introductory reader as an authorization system or a general plugin executor. Reading metadata and trusting it to control sensitive behavior are materially different responsibilities.
A marker does not call an annotated method
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Calls before: 0
Marker present: true
Calls after reading: 0
Calls after explicit call: 1
Repair exercise: the inherited title still appears
The child should supply title() but accidentally overloads it with a parameter. First add @Override and observe the compiler reject the signature; then fix the signature and keep the check. The runnable starter currently prints the inherited title.
Main.java · Expose the unintended overload
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Current runtime output:
Generic lesson
Reveal the explained repair
The child’s no-argument title now matches the inherited method, so ordinary virtual dispatch uses it through BaseLesson. @Override verifies that relationship at compilation. Removing the marker would not change valid dispatch, but would lose protection against repeating the signature mistake.
Main.java · Correct and verify the override
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Java annotations
Independent lab: a validated lesson-metadata reader
Create an original lesson-metadata reader. Define a TYPE-only runtime annotation with a required title, default duration, and enum level. Read only metadata declared on the supplied class, validate it, and format a summary without constructing or invoking lesson objects.
- Define LessonInfo with RUNTIME retention and TYPE target, title required, minutes default 30, and level default BEGINNER.
- Define Level with BEGINNER and INTERMEDIATE, and the example annotated/plain lesson classes.
- Implement describe(Class<?> type); reject null with IllegalArgumentException.
- Use getDeclaredAnnotation; an unannotated class returns exactly No lesson metadata.
- Reject stripped-empty titles or minutes outside 1..240; format a stripped title without rewriting the annotation.
- Return title | N min | LEVEL. Do not instantiate lesson classes, invoke their methods, or search inherited metadata.
The starter compiles but prints nothing. Keep the annotation, enum, and sample classes in the same Main.java program, then implement the reader to produce:
Java annotations | 35 min | BEGINNER
Java interfaces | 45 min | INTERMEDIATE
No lesson metadata.
Main.java · Define and read lesson metadata
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Stage 1 hints
Stage 1: define the schema. Put Retention and Target on @interface LessonInfo. Use an enum-valued default and remember that a required title has no default.
Stage 2 hints
Stage 2: read only declared metadata. Check a null Class reference before reflection. Distinguish a missing annotation from an annotation whose business values are invalid.
Stage 3 hints
Stage 3: validate then format. Check stripped title and minute bounds, return the report, and verify that original annotation text remains unchanged. No constructor or Method.invoke belongs in this helper.
Reveal the complete explained solution
Main.java · Read declared metadata without executing lessons
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
RUNTIME retention makes LessonInfo visible to this reader; TYPE restricts where it can be used. The first class uses the default enum level, the second specifies INTERMEDIATE, and the third returns the defined absence message. The reader strips only its formatted title and validates duration explicitly. getDeclaredAnnotation excludes superclass lookup, and no lesson constructor or method is executed by the helper.
Test your contract
- Verify omitted duration/level use 30 and BEGINNER, and explicit values are honored.
- Durations 1 and 240 succeed; 0 and 241 are rejected along with blank titles.
- Null class input is rejected; an unannotated subclass does not inherit a declared result from this reader.
- The annotation’s raw spaced title remains unchanged after report formatting.
- A lesson constructor/method with a side effect must remain untouched; missing required elements and wrong target locations are compiler errors.
Try one class with only the required title to inspect defaults, another with duration zero to trigger validation, and a plain subclass of an annotated class. The last should return the defined absence message because this reader asks for declared metadata only. These are separate tests of schema defaults, business validation, and lookup policy.
Common mistakes and how to diagnose them
Removing Override to hide a signature error
Decide whether overriding was intended. Correct the signature when it was; deleting the check can leave an accidental overload and the inherited behavior.
Assuming deprecated code stops running
Deprecation is discouragement and migration metadata. It does not delete the method or block calls. Read the replacement documentation and distinguish diagnostics from runtime output.
Suppressing defects instead of warnings
SuppressWarnings affects recognized diagnostic categories, not runtime safety or invalid language constructs. Prefer narrow, justified boundaries and repair unchecked write errors directly.
Reflection cannot see an annotation
Inspect its retention first: the default is CLASS. Runtime annotation access needs RUNTIME retention, a compatible target, and the correct annotated element.
Using an annotation on the wrong element
A TYPE target does not permit annotating an ordinary method. The compiler checks the declared target; changing retention does not fix a placement error.
Treating presence as business validation
An int annotation element can still contain zero when your domain requires positive minutes. Write explicit validation; the metadata declaration does not automatically enforce that range.
Frequently asked questions
Do annotations execute code by themselves?
No. A compiler, tool, processor, or runtime consumer interprets them. A custom annotation does not automatically call its annotated method, schedule work, or validate application data.
Does Override create overriding behavior?
No. The correct inheritance/signature relationship creates it. Override checks the stated intention, catching mistakes early without changing otherwise valid dispatch.
Is @deprecated in Javadoc the same as @Deprecated?
No. The Javadoc tag explains the migration and replacement; the annotation supplies structured metadata and compiler-recognized discouragement. Use both when documenting an actual deprecated API.
What retention is used when I omit Retention?
CLASS. The annotation is recorded in the class file but is not visible through ordinary runtime annotation reflection. Explicit RUNTIME retention is required for the lab’s reader.
Does FunctionalInterface forbid default methods?
No. The annotation checks the functional abstract-method contract. Default and static methods with bodies do not by themselves introduce additional abstract obligations.
Why use getDeclaredAnnotation in this lab?
The contract is to read metadata explicitly declared on the supplied class, not to search superclasses or merge framework conventions. Missing metadata is handled separately from invalid declared values.
Metadata needs an explicit consumer
Override and FunctionalInterface check intentions, deprecation guides migration, and suppression is not a defect repair. Target controls placement; retention controls survival. A custom reader must still handle missing metadata and validate domain values, and reading a marker does not invoke a method.
You have reached the final lesson in this Java course. Review why the accidental overload printed the base title, why default CLASS retention is invisible to this runtime lookup, and why a title annotation can still contain invalid blank text. Return to the course roadmap to revisit a lesson or practise its lab again.
Can you identify the annotation and its consumer?
Answer six questions and read the explanations. Quiz answers are temporary and do not save course progress.
References and related learning
This original SovranCode lesson uses the Java 21 specification and official APIs for annotation behavior and lifecycle rules.
- Java 21 Override API
- Java 21 Deprecated API
- Java 21 Retention API
- Java 21 specification: annotation interfaces
Review overloads · Previous lesson: generics · Java course roadmap.
Last updated: