Annotations: built-ins and defining your own
Standard annotations (@Override, @Deprecated, @SuppressWarnings, @FunctionalInterface) and writing a custom annotation with @Retention and @Target.
Metadata that travels with code
An annotation attaches metadata to a program element — a class, method, field, parameter, etc. It does not change what the code *does* on its own; it is read by the compiler, by build/test tools, or at runtime via reflection. The syntax is @Name, optionally with element values in parentheses.
### The four built-ins you must know
@Override — declares that a method is intended to override a supertype method. If it does *not* (wrong name, wrong parameter types), the compiler reports an error. This catches the classic bug where you *think* you overrode equals(Object) but actually wrote equals(MyType), silently *overloading* instead.
class Point {
@Override
public boolean equals(Object o) { // correct signature
// ...
return true;
}
}
@Deprecated — marks an element as obsolete. Callers get a compiler warning. Pair it with a Javadoc @deprecated tag explaining the replacement.
@Deprecated
public void oldStart() { /* use start() instead */ }
@SuppressWarnings — silences named compiler warnings for the annotated element. Keep its scope as small as possible (a single statement or method), and document why.
@SuppressWarnings("unchecked")
List<String> names = (List<String>) rawList;
@FunctionalInterface — declares that an interface has exactly one abstract method. The compiler errors if you add a second, protecting interfaces meant to be used as lambda/method-reference targets (like Runnable, Comparator).
@FunctionalInterface
interface Check { boolean test(int n); }
Defining a custom annotation
You declare one with @interface. Its elements look like methods (name + return type), may declare a default, and the special element named value lets callers omit the name:
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
String value() default "";
int timeout() default 0;
}
Applying it:
@Test // both elements use their defaults
void case1() { }
@Test("login flow") // "login flow" binds to value (name omitted)
void case2() { }
@Test(value = "timeout case", timeout = 500) // multiple elements: name each
void case3() { }
Legal element types are restricted: primitives, String, Class, an enum, another annotation, or a one-dimensional array of those.
Meta-annotations: annotations *on* annotations
Two are essential when you write your own:
- @Retention — how long the annotation is kept. SOURCE (discarded by the compiler — e.g. @Override), CLASS (kept in the .class file but not loaded at runtime — the default), or RUNTIME (loaded and visible to reflection). You must use RUNTIME if a tool will read it via reflection.
- @Target — which program elements the annotation may be applied to: ElementType.TYPE, METHOD, FIELD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, etc. Applying it elsewhere is a compile error.
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@interface Audited { }
Reading annotations at runtime
Because the AU course pairs annotations with *testing*, the payoff is reading them reflectively. A RUNTIME-retained annotation is reachable through the reflection API (covered in Module 7):
for (Method m : MyTests.class.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
Test t = m.getAnnotation(Test.class);
System.out.println("running " + m.getName() + ": " + t.value());
m.invoke(new MyTests());
}
}
This is exactly how JUnit discovers @Test methods — annotation + reflection, no naming convention required.