The labs on this site need JavaScript to run. The same content is written out below.
Learn › Java › Generics, enums, and annotations
Annotations
What really happens when you just add an annotation with @?
An annotation (a mark that starts with @, such as @Override or @Timed) is a label that you attach to code. The label itself does nothing. It takes on meaning only when something reads it, namely the compiler or a framework that reads it with reflection at run time. And to be read at run time, the label must still be there at run time.
Checked against real execution · 28 combinations were actually compiled and run on JDK 21 and checked against the results (including the contents of the class file and the result of reflection)
Concepts in this lab
- Annotation
- This is metadata that you attach to code. It does nothing by itself; the compiler or a framework reads it and acts on it. Some, like @Override, are used by the compiler for checks, and others, like @Transactional and @Autowired, are read by a framework at run time through reflection to apply a feature.
- Reflection
- This feature lets a running program inspect information about a class (its fields, methods, constructors, and annotations) and create objects or call methods by name. Spring's dependency injection, JPA's entity handling, and JSON serialization libraries all work on top of it. It is slower than a normal call and does not get compile-time type checking.
- @Retention and @Target
- These are two annotations that you apply to a new annotation, when you create it, to set its properties. @Retention decides how far the annotation is retained. SOURCE retains it only in the source code, CLASS retains it up to the class file, and RUNTIME retains it until run time so that it can be read with reflection. When @Retention is not written, the policy is CLASS, so you must write RUNTIME on an annotation that will be read at run time. @Target decides where the annotation can be applied (TYPE is a type declaration such as a class, METHOD is a method, FIELD is a field, and so on), and applying it in a place that does not match is a compile-time error.
- @Override
- This is an annotation that indicates that the method right below it overrides a method of a superclass or an interface. The compiler reads it and checks whether the method really overrides, and if it does not, the compiler reports a compile-time error. It catches the mistake of writing the wrong method name or parameters and so creating a new method that is not an override. A method overrides even without it, and the behavior of the program does not change either, but to prevent mistakes you always put it on a method that overrides. It is an annotation that is retained only in the source code, so it is not recorded in the class file.
- Overriding
- This means that a subclass redefines a method of its superclass (or of an interface) with the same signature. At run time, the method that matches the type of the actual object is called, and this is how polymorphism is implemented. The access level cannot be made narrower, and static, private, and final methods cannot be overridden.
Try these
- Default settings — @Timed is applied and there is code that reads it, but nothing happens.
- Retaining it with RUNTIME — The reading code finds the annotation and adds extra steps before and after work.
- With no code that reads it — Nothing happens even though the annotation is retained until run time.
- With SOURCE — The annotation is not retained even in the class file.
- Applied where it is not allowed — @Target is TYPE, but the annotation is applied to a method. This is a compile-time error.
- A misspelled name without @Override — The code compiles without an error, and the superclass method runs instead.
- @Override catches it — The compiler reports the same mistake as an error.
Running with the default settings
- Look at the declaration @interface Timed: This is the syntax for creating a new annotation named Timed. Inside the braces there is not a single line of code to run. An annotation is only information that says "this mark is attached here". It does nothing by itself.
- No @Retention is written: @Retention is the setting that decides how far the annotation is retained. When it is not written, the default, CLASS, applies. CLASS means "record the annotation in the class file, but do not make it visible at run time".
- The compiler checks the @Timed above the work method: @Target(ElementType.METHOD) means "can be applied only to methods", and the annotation is applied to a method, so the check passes. It is the compiler that reads @Target and enforces it.
- The code is compiled and Service.class is produced: The @Timed annotation is recorded in the class file. However, it is recorded in the attribute that is "not visible at run time" (RuntimeInvisibleAnnotations).
- Service.class.getDeclaredMethod("work") runs: It gets the information about the work method with reflection. Reflection is a feature for examining, at run time, information such as the methods, fields, and annotations that a class has, and it is how frameworks read annotations.
- m.isAnnotationPresent(Timed.class) runs: It asks whether @Timed is present on work. The result is false. The annotation is in the class file, but the JVM does not make an annotation retained with CLASS visible to reflection. The cause is that @Retention was not written.
- found is false, so the reading code adds nothing: It only runs work with m.invoke(service). @Timed is clearly there in the source code, and yet to the reading code this is the same as a method with no annotation. No error occurs either.
Result of this run
- The annotation is applied and there is code that reads it, and yet nothing happens. The reason is that the annotation was not retained until run time. When you create an annotation that will be read with reflection at run time, always write @Retention(RetentionPolicy.RUNTIME). If you do not write it, the default is CLASS, so the annotation is silently ignored without an error.
- CLASS is for tools that read and process class files. A running program cannot read such an annotation itself.
Code
@Target(ElementType.METHOD)
@interface Timed { }
class Service {
@Timed
void work() {
System.out.println("work runs");
}
}
// From here on, the code is inside main: the code that reads the annotation
Service service = new Service();
Method m = Service.class.getDeclaredMethod("work");
boolean found = m.isAnnotationPresent(Timed.class);
System.out.println("@Timed visible: " + found);
if (found) System.out.println("[timing started]");
m.invoke(service);
if (found) System.out.println("[timing finished]");
Assumptions of this model
The 28 combinations (24 on the @Timed side and 4 on the @Override side) were checked by actually compiling and running the code shown on the screen on JDK 21 on this PC. For each combination, the following were checked directly: whether the code compiles, the line with the error, the compiler message, and the output, and also whether the annotation is recorded in the bytes of the generated class file (and if so, in which of the two attributes, RuntimeVisibleAnnotations or RuntimeInvisibleAnnotations) and the result of isAnnotationPresent in reflection. Settings that are hidden on the screen do not affect the result. The code on the screen omits the import statements (those from java.lang.annotation, and java.lang.reflect.Method) and the declaration of the main method. It was also checked that an annotation with @Target(ElementType.TYPE) compiles when it is applied to a class. The statement that a framework such as Spring reads annotations on the same principle as the "reading" code in this model is an explanation, and it was not verified by actually running a framework. Tools that read annotations at compile time and generate code (annotation processors) are not covered.
Previous lab: Enum classes · Next lab: ArrayList and LinkedList