JAVA / PLATFORM, BUILDS AND TESTING
Annotations and reading metadata on code
Read metadata off Java declarations: pass values to annotations like @Deprecated and pull them back out through the reflection annotation API.
What you will learn
- Pass annotation elements by name and rely on declared defaults for the rest
- Look up metadata with isAnnotationPresent, getAnnotation and getAnnotations
- Explain why a runtime lookup for @Override always returns null
- Inspect an annotation type's own @Target and @Retention before relying on it
Understanding Annotations and reading metadata on code
An annotation is a typed tag written on a declaration and stored beside it, either in the source only or in the class file. The annotation type is declared with @interface, and its members are called elements: named, typed, and restricted to compile-time constants such as primitives, String, Class, enum constants, other annotations, and arrays of those. Elements can declare defaults, which is why @Deprecated alone is legal and @Deprecated(since = "17") is the same annotation with one element supplied. Each annotation type also declares where it may appear, so @Override on a field is a compile error rather than a tag nobody notices.
Nothing happens merely because a declaration is annotated. Metadata needs a reader, and a reader can sit in three places: javac itself, which is what gives @Override and @SuppressWarnings their effect; tools that read class files after compilation; and code that asks at run time. How far the tag travels decides which of those readers can see it, so a runtime lookup for @Override always comes back empty because the compiler consumed it and dropped it, while @Deprecated and @FunctionalInterface are still present in the loaded class.
At run time the entry points live on AnnotatedElement, implemented by Class, Method, Field, Constructor, Parameter, Package and Module: isAnnotationPresent for a yes or no, getAnnotation for the values, getAnnotations for everything still visible. What getAnnotation returns is a synthesized object implementing the annotation interface, so each element is read by calling it as a method, and annotationType() rather than getClass() tells you which annotation you are holding. A null result means "not on this element", which covers three different situations: never written, discarded before run time, or written on a supertype, since annotations are not inherited by overriding methods and @Inherited only pushes class-level annotations down a superclass chain.
import java.lang.reflect.Method;
public class DeprecationReport {
@Deprecated(since = "17", forRemoval = true)
static void oldWay() {
}
@Deprecated
static void olderWay() {
}
static void currentWay() {
}
public static void main(String[] args) throws NoSuchMethodException {
for (String name : new String[] {"oldWay", "olderWay", "currentWay"}) {
Method m = DeprecationReport.class.getDeclaredMethod(name);
Deprecated d = m.getAnnotation(Deprecated.class);
if (d == null) {
System.out.println(name + " -> no @Deprecated metadata");
} else {
System.out.println(name + " -> since='" + d.since()
+ "' forRemoval=" + d.forRemoval()
+ " type=" + d.annotationType().getSimpleName());
}
}
Method entry = DeprecationReport.class.getDeclaredMethod("main", String[].class);
System.out.println("main annotated? " + entry.isAnnotationPresent(Deprecated.class));
}
}An annotation is inert metadata attached to a declaration; it changes nothing until some reader looks it up and acts on the values it carries.
Worked examples
Which tags are still there at run time
Shows that @FunctionalInterface can be read reflectively while @Override cannot, even though both are visible in the source.
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
public class VisibleMetadata {
@FunctionalInterface
interface Transform {
String apply(String input);
}
static class Shout implements Transform {
@Override
public String apply(String input) {
return input.toUpperCase();
}
}
public static void main(String[] args) throws NoSuchMethodException {
for (Annotation a : Transform.class.getAnnotations()) {
System.out.println("Transform carries " + a.annotationType().getName());
}
Method apply = Shout.class.getDeclaredMethod("apply", String.class);
System.out.println("annotations on Shout.apply: " + apply.getAnnotations().length);
System.out.println(new Shout().apply("still runs"));
}
}Example explained
Line 1getAnnotations() on Transform finds @FunctionalInterface because that annotation type is kept until run time.
Line 2The count for Shout.apply is 0: javac verified @Override against Transform and then discarded the tag.
Line 3annotationType() names the annotation interface; a.getClass() would report the generated proxy class instead.
Line 4The last line proves the point about behaviour: deleting either annotation would not change what apply() does.
Interrogating the annotation type itself
Uses the same lookup calls on java.lang annotation types to discover where each one is allowed and how long it survives.
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.Arrays;
public class DescribeAnnotationType {
static void describe(Class<? extends Annotation> type) {
Retention r = type.getAnnotation(Retention.class);
Target t = type.getAnnotation(Target.class);
String kept = (r == null) ? "CLASS (implied)" : r.value().toString();
String where = (t == null) ? "anywhere" : Arrays.toString(t.value());
System.out.println(type.getSimpleName() + ": kept=" + kept + ", targets=" + where);
}
public static void main(String[] args) {
describe(Override.class);
describe(FunctionalInterface.class);
describe(Retention.class);
}
}Example explained
Line 1An annotation type is an ordinary declaration, so getAnnotation reads java.lang.Override the same way it reads your own code.
Line 2Override reports SOURCE, which is the concrete reason a reflective lookup for it can never succeed.
Line 3targets=[METHOD] is the rule javac enforces, so @Override elsewhere is rejected at compile time rather than ignored.
Line 4The null branches would fire for a type that omits @Retention or @Target, where CLASS and "any declaration" apply.
Important notes
The since and forRemoval elements of @Deprecated exist only from Java 9 onward; on Java 8 that annotation has no elements at all.
An array-valued element returns a fresh clone on each call, so you cannot corrupt an annotation by writing to it, and comparisons need Arrays.equals rather than ==.
Common mistakes
Scanning for @Override or @SuppressWarnings with getAnnotation: the call returns null every time because those tags never reach the class file, so the branch you wrote is dead code.
Assuming the tag enforces itself, for example expecting @Deprecated(forRemoval = true) to make callers fail; the call runs normally and only the compiler and IDE complain.
Reading a method annotation off a subclass override: method annotations are never inherited, so getAnnotation returns null there and framework behaviour silently stops the moment someone overrides.
Try it yourself
Change, predict, then run
Write a class with three methods, tagging one @Deprecated(since = "3.1") and one @Deprecated(forRemoval = true), then loop over getDeclaredMethods() and print only the names whose forRemoval() is true. Add an @Override toString() to the same class and confirm that getAnnotation(Override.class) on it returns null.
Open the Java workspaceCheck your understanding
You mark a method @Deprecated(forRemoval = true) and call it from another class in the same project. What happens when the program runs?
- Nothing differs at run time; the only effect was the warning javac produced when it read the annotation
- The JVM prints a deprecation warning to stderr the first time the method is called
- The call throws UnsupportedOperationException because forRemoval is true
- The method is left out of the class file, so the call fails with NoSuchMethodError
Show answer
The annotation is data attached to the declaration, not behaviour, and forRemoval only sharpens what javac and your IDE report at compile time. The stderr answer is tempting because you did see a warning, but that came from the compiler reading the metadata; the JVM loads the same annotation and does nothing with it unless your own code looks it up.