15 questions foundAnnotation elements are declared as methods (with no body) inside the @interface, optionally given a default value via the default keyword; an element specifically named value gets special syntactic sugar, letting callers omit the value = prefix when it's the only element being set (@MyAnnotation("text") instead of @MyAnnotation(value = "text")) -- elements can be primitives, String, Class, enums, other annotations, or arrays of these types, but never arbitrary objects.
@Retention(RetentionPolicy.RUNTIME)
public @interface Endpoint {
String value(); // special "value" element
String method() default "GET";
int timeout() default 30;
}
@Endpoint("/users") // shorthand for value = "/users"
@Endpoint(value = "/users", method = "POST", timeout = 60) // full explicit form
public void handler() { }
Real-world example A routing annotation defines value as the path (allowing the terse @Endpoint("/users") syntax for the common case) while method and timeout have sensible defaults, letting most usages stay concise while still supporting full customization when needed.
Common follow-ups: What types are legal for annotation elements, and why can't you use an arbitrary POJO type?;How do array-typed elements with a single value get abbreviated syntactically?
Java Fundamentals: Syntax Data Types & Operators;Design Patterns in Java
Define a RUNTIME-retention @Cacheable annotation for methods, then use java.lang.reflect.Proxy (for interface-based classes) or a bytecode-generation library like ByteBuddy/CGLIB (for concrete classes) to create a dynamic proxy wrapping the real object -- the proxy's invocation handler checks for the @Cacheable annotation via reflection before delegating to the real method, returning a cached result from a backing Map if a matching cache key already exists, otherwise invoking the real method and storing its result, which is conceptually how Spring's caching abstraction and AOP-based cross-cutting concerns are implemented under the hood.
public class CachingInvocationHandler implements InvocationHandler {
private final Object target;
private final Map<String, Object> cache = new ConcurrentHashMap<>();
public CachingInvocationHandler(Object target) { this.target = target; }
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.isAnnotationPresent(Cacheable.class)) {
String key = method.getName() + Arrays.toString(args);
return cache.computeIfAbsent(key, k -> {
try { return method.invoke(target, args); }
catch (Exception e) { throw new RuntimeException(e); }
});
}
return method.invoke(target, args);
}
}
Real-world example A lightweight in-house caching layer wraps a data-access interface in a dynamic proxy that reads a custom @Cacheable annotation, transparently caching results for annotated methods without the calling code being aware caching is happening at all, illustrating the same proxy-based AOP pattern Spring uses internally.
Common follow-ups: What's the limitation of java.lang.reflect.Proxy requiring the target to implement an interface, and how do CGLIB/ByteBuddy address concrete classes instead?;How would you handle cache invalidation or expiration in this design?
Design Patterns in Java;Reflection API
@Nullable/@NonNull are purely advisory annotations (not enforced by the JVM at runtime by default) primarily consumed by static analysis tools and IDEs to warn about potential null dereferences at compile/edit time, with zero runtime cost since they carry no actual null-check logic themselves -- Optional, by contrast, is an actual runtime container type that forces callers to explicitly handle the possibly-absent case through its API (isPresent(), orElse(), map()), providing genuine runtime behavior rather than just static hinting, making the two complementary rather than interchangeable tools.
public class UserService {
@Nullable
public User findById(String id) { // IDE/static analysis warns callers to null-check
return repository.get(id);
}
public Optional<User> findByIdSafely(String id) { // forces explicit handling at compile time
return Optional.ofNullable(repository.get(id));
}
}
Real-world example A large codebase adopts @NonNull/@Nullable annotations checked by a static analysis tool (like NullAway or IntelliJ's built-in inspections) in CI to catch likely null-pointer bugs before merge, while reserving Optional specifically for public API method return types where forcing callers to explicitly handle absence is valuable.
Common follow-ups: Why is Optional generally discouraged as a field type or method parameter type despite being fine as a return type?;How does a static analysis tool actually enforce @NonNull annotations without any JVM runtime support?
Java Fundamentals: Syntax Data Types & Operators;Exceptions
Multiple distinct annotation types can simply be stacked on the same element, each on its own line or space-separated, and their order relative to each other has no semantic significance to the compiler or runtime (unlike modifiers like public static, which do have conventional ordering) -- tools reading them via reflection retrieve all applicable annotations regardless of the order they were written in the source.
@Override
@Deprecated
@SuppressWarnings("unchecked")
public List legacyMethod() {
return new ArrayList();
}
// Equivalent, order doesn't matter:
@SuppressWarnings("unchecked") @Deprecated @Override
public List legacyMethod() { ... }
Real-world example A deprecated legacy method is marked with all three of @Override, @Deprecated, and @SuppressWarnings simultaneously, each serving its own independent purpose (compiler-checked override correctness, deprecation warning, and suppressing an unrelated raw-type warning) without any conflict or ordering requirement between them.
Common follow-ups: Are there any annotations where combining them together would actually cause a conflict or error?;How does an IDE typically choose to format/order stacked annotations by convention, even though it's not required?
OOP & Classes;Exceptions
Traditional annotations (pre-Java 8) could only target declarations (a class, method, field, or parameter as a whole); type annotations (enabled by adding ElementType.TYPE_USE to @Target) can additionally be applied anywhere a type is used syntactically -- including generic type arguments, array component types, and cast expressions -- enabling more precise static analysis tools (like the Checker Framework) to reason about, for example, nullability of a specific generic type parameter rather than just the field as a whole.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
public @interface NonEmpty { }
// Applies to the List itself
List<@NonEmpty String> names;
// Applies to a cast expression's target type
String s = (@NonEmpty String) someObject;
Real-world example A static analysis tool using type annotations distinguishes between List<@Nullable String> (a list that may contain null elements) and @Nullable List<String> (a list reference itself that may be null but never contains null elements), a level of precision impossible to express with pre-Java-8 declaration-only annotations.
Common follow-ups: What specific new syntactic locations does TYPE_USE enable that ElementType.FIELD or ElementType.METHOD don't cover?;How does the Checker Framework use type annotations to implement pluggable, project-specific type systems?
Generics;Java Fundamentals: Syntax Data Types & Operators