Dev Learning Lab

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

Running with the default settings

  1. 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.
  2. 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".
  3. 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.
  4. 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).
  5. 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.
  6. 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.
  7. 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

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

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these