Annotations

15 questions found

What are annotations in Java, and what purpose do they serve?

Beginner
Annotations are a form of metadata that attach information to Java code elements (classes, methods, fields, parameters) without directly affecting program logic -- they're read by the compiler, tools, or the runtime via reflection to influence behavior, such as @Override triggering a compile-time check, or @Entity marking a class for an ORM framework to map to a database table.
@Override
public String toString() {
    return "MyObject";
}

@Deprecated
public void oldMethod() { }

@SuppressWarnings("unchecked")
public void legacyCode() { }
Real-world example A Spring Boot application uses @RestController and @GetMapping annotations to declaratively wire up HTTP routes to Java methods, letting the framework read this metadata at startup to build the routing table without any manual registration code.

Common follow-ups: What's the difference between a marker annotation (no elements) and one with elements?;How does the compiler use @Override to catch a common class of bugs?

Reflection API;Java Fundamentals: Syntax Data Types & Operators

How do you create a custom annotation in Java, and what role do @Retention and @Target play?

Intermediate
A custom annotation is defined using @interface; @Retention specifies how long the annotation is retained (SOURCE, CLASS, or RUNTIME -- with RUNTIME needed if you want to read it via reflection), and @Target restricts which code elements (METHOD, TYPE, FIELD, etc.) the annotation can legally be applied to, both themselves being meta-annotations that configure your custom annotation's behavior.
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Loggable {
    String value() default "";
}

public class Service {
    @Loggable("critical operation")
    public void process() { }
}
Real-world example A team defines a custom @Loggable annotation with RUNTIME retention and METHOD target, letting an AOP-style interceptor read this annotation via reflection at runtime to automatically wrap annotated methods with logging behavior, without modifying each method's implementation.

Common follow-ups: What happens if you try to apply an annotation to an element type not listed in @Target?;Why would you ever choose SOURCE or CLASS retention over RUNTIME?

Reflection API;Design Patterns in Java

How would you read custom annotations at runtime using reflection to implement annotation-driven behavior, such as a simple dependency injection or validation framework?

Advanced
Use java.lang.reflect classes (Class.getDeclaredMethods(), Method.getAnnotation(YourAnnotation.class), Field.isAnnotationPresent()) to inspect a class's structure at runtime, checking for the presence of your custom RUNTIME-retention annotation and extracting its element values to drive behavior -- this is the fundamental mechanism underlying frameworks like Spring's @Autowired or Hibernate Validator's @NotNull, where annotation metadata is read reflectively to wire dependencies or apply validation rules without generated boilerplate.
public class AnnotationProcessor {
    public static void invokeLoggable(Object obj) throws Exception {
        for (Method method : obj.getClass().getDeclaredMethods()) {
            if (method.isAnnotationPresent(Loggable.class)) {
                Loggable ann = method.getAnnotation(Loggable.class);
                System.out.println("Invoking logged method: " + ann.value());
                method.invoke(obj);
            }
        }
    }
}
Real-world example A lightweight in-house DI container scans classes for a custom @Inject annotation on fields, using reflection to instantiate and set the appropriate dependency, replicating a simplified version of what Spring's @Autowired does under the hood.

Common follow-ups: What's the performance cost of reflection-based annotation processing at runtime versus compile-time annotation processing?;How does Java's annotation processing API (APT) differ from reading annotations reflectively at runtime?

Reflection API;Design Patterns in Java

What is the difference between compile-time annotation processing (via javax.annotation.processing.Processor) and runtime reflection-based annotation reading?

Intermediate
Compile-time annotation processing runs during compilation itself (implementing AbstractProcessor, registered via META-INF/services), generating new source files or validating code structure before the .class files even exist -- used by tools like Lombok (generating getters/setters) or Dagger (generating dependency injection code) -- while runtime reflection reads already-compiled annotation metadata during program execution, incurring reflection's inherent performance overhead but requiring no separate build step, making compile-time processing generally preferred for performance-sensitive, code-generation scenarios.
@SupportedAnnotationTypes("com.example.GenerateBuilder")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class BuilderProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // generates new .java source files at compile time
        return true;
    }
}
Real-world example Lombok's @Data annotation is processed entirely at compile time via an annotation processor, generating getter/setter/equals/hashCode methods directly into the bytecode before the JVM ever runs the program, avoiding any runtime reflection overhead that a similar reflection-based approach would incur on every method call.

Common follow-ups: Why do frameworks like Dagger prefer compile-time code generation over Spring's runtime reflection-based approach?;What are the debugging challenges specific to compile-time generated code?

Reflection API;Build Tools: Maven & Gradle

What do the built-in @Override, @Deprecated, and @SuppressWarnings annotations do?

Beginner
@Override tells the compiler a method is intended to override a superclass/interface method, causing a compile error if no matching method actually exists (catching typos like a mismatched method signature); @Deprecated marks an element as discouraged for use, triggering a compiler warning at call sites and signaling to other developers (and tools) that it may be removed in the future; @SuppressWarnings suppresses specific compiler warnings (like "unchecked") for the annotated element, useful when a warning is a known, deliberately accepted trade-off.
public class Base {
    public void process() { }
}

public class Derived extends Base {
    @Override
    public void proccess() { }  // typo! Compiler catches this because @Override expects an actual override
}
Real-world example A refactor accidentally misspells an overridden method name, but because the method was marked @Override, the compiler immediately flags the error at build time rather than silently creating an unrelated new method that would have caused a confusing runtime bug.

Common follow-ups: What happens if you omit @Override but still intend to override a method -- does the code still work?;When is it appropriate to use @SuppressWarnings versus actually fixing the underlying warning?

Interfaces & Abstract Classes;OOP & Classes

How do repeatable annotations work in Java (using @Repeatable), and what problem do they solve?

Advanced
Prior to Java 8, applying the same annotation type multiple times to one element wasn't allowed; @Repeatable (introduced in Java 8) lets you define a container annotation holding an array of the repeatable annotation, allowing multiple instances of the same annotation type to be applied to a single element -- the compiler handles the wrapping/unwrapping automatically, letting reflection code retrieve either the individual repeated annotations or the container transparently via getAnnotationsByType().
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Schedules.class)
public @interface Schedule {
    String day();
}

@Retention(RetentionPolicy.RUNTIME)
public @interface Schedules {
    Schedule[] value();
}

public class Job {
    @Schedule(day = "MONDAY")
    @Schedule(day = "FRIDAY")
    public void run() { }
}
Real-world example A job scheduling framework lets developers annotate a single method with multiple @Schedule annotations (one per day it should run), using @Repeatable to allow this multi-application syntax cleanly instead of requiring an awkward single annotation with an array-typed element.

Common follow-ups: What does the generated container annotation (@Schedules) look like if you don't use @Repeatable and must retrieve multiple annotations manually?;How does getAnnotationsByType() differ from getAnnotation() when reading repeatable annotations?

Reflection API;OOP & Classes

What are Java's meta-annotations (@Retention, @Target, @Documented, @Inherited), and what does each control?

Intermediate
@Retention controls how long the annotation persists (SOURCE/CLASS/RUNTIME); @Target restricts which element types it can be applied to; @Documented causes the annotation to appear in generated Javadoc for annotated elements; @Inherited allows a class-level annotation to be automatically inherited by subclasses (only applies to annotations on classes, not methods/fields), letting a subclass be treated as if it also carries the annotation without redeclaring it.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
@Documented
public @interface Auditable { }

@Auditable
public class BaseEntity { }

public class Order extends BaseEntity { }  // inherits @Auditable automatically via reflection checks
Real-world example An auditing framework marks a base entity class with a custom @Inherited @Auditable annotation, and reflection-based checks on any subclass (like Order extends BaseEntity) correctly detect the annotation as present even though Order itself never explicitly declares it.

Common follow-ups: Why does @Inherited only work for class-level annotations, not methods or fields?;What's a scenario where you'd deliberately choose CLASS retention instead of RUNTIME or SOURCE?

Reflection API;OOP & Classes

How would you implement annotation-based validation (similar to Bean Validation's @NotNull, @Size) using a custom annotation and a reflection-based validator?

Advanced
Define a custom annotation with RUNTIME retention targeting FIELD, then write a validator class that uses reflection to iterate an object's fields, checking for the presence of your validation annotations and applying the corresponding validation logic (e.g., checking a String field's length against a @Size annotation's min/max elements), collecting any violations into a result list -- this pattern is exactly how Jakarta Bean Validation (Hibernate Validator) implements its own constraint annotations under the hood.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MinLength {
    int value();
}

public class Validator {
    public static List<String> validate(Object obj) throws Exception {
        List<String> errors = new ArrayList<>();
        for (Field field : obj.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(MinLength.class)) {
                field.setAccessible(true);
                String value = (String) field.get(obj);
                int min = field.getAnnotation(MinLength.class).value();
                if (value == null || value.length() < min) {
                    errors.add(field.getName() + " must be at least " + min + " characters");
                }
            }
        }
        return errors;
    }
}
Real-world example A lightweight internal validation utility implements a simplified @MinLength annotation processed via reflection, mirroring (at a smaller scale) exactly how Hibernate Validator's @Size annotation works internally to enforce field-level constraints declaratively.

Common follow-ups: What are the performance implications of using field.setAccessible(true) and reflective field access at scale?;How does this reflection-based approach compare to a compile-time annotation processor generating validation code instead?

Reflection API;Exceptions

What is the purpose of the @FunctionalInterface annotation, and is it required for an interface to actually be treated as a functional interface?

Intermediate
@FunctionalInterface is a marker/documentation annotation that instructs the compiler to verify the annotated interface has exactly one abstract method (making it eligible for lambda expression assignment) -- it's not strictly required for an interface with a single abstract method to work as a functional interface (the compiler infers this structurally regardless), but omitting it removes the safety net where the compiler would otherwise immediately flag an accidental second abstract method being added later, which would silently break lambda compatibility.
@FunctionalInterface
public interface Calculator {
    int calculate(int a, int b);
    // Adding a second abstract method here would now cause a COMPILE ERROR, not a runtime surprise
}

Calculator add = (a, b) -> a + b;
Real-world example A shared library interface is marked @FunctionalInterface specifically so that if a future contributor accidentally adds a second abstract method (breaking every lambda-based usage across dozens of consumers), the compiler catches this immediately at the library's own build time rather than the error surfacing confusingly at each consumer's build.

Common follow-ups: What happens if you apply @FunctionalInterface to an interface with two abstract methods?;How do default and static methods interact with the single-abstract-method requirement?

Functional Interfaces & Method References;Interfaces & Abstract Classes

How does Java's annotation processing API (javax.annotation.processing) enable compile-time code generation frameworks like Lombok or Dagger, and what are its key limitations?

Advanced
The Processor SPI lets a library register a class implementing AbstractProcessor, invoked automatically by javac during compilation for any annotations it declares support for via @SupportedAnnotationTypes -- it can generate entirely new source files (via the Filer API) that then get compiled alongside the original code in the same compilation round, but critically, standard annotation processors cannot modify existing source files (only generate new ones), which is why Lombok's field/method injection into existing classes actually relies on a lower-level, officially unsupported compiler API hack rather than the standard processor mechanism.
@SupportedAnnotationTypes("com.example.AutoValue")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class AutoValueProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(AutoValue.class)) {
            // generates a NEW source file implementing the value class, via processingEnv.getFiler()
        }
        return true;
    }
}
Real-world example Google's AutoValue library uses the standard annotation processing API to generate a complete new immutable implementation class alongside an abstract class marked @AutoValue, whereas Lombok (needing to inject code into the existing class itself rather than generate a new file) has to rely on unofficial compiler internals, explaining why Lombok has historically had more compiler-version compatibility issues than AutoValue.

Common follow-ups: Why can't a standard annotation processor modify the source file it's processing?;What alternative approaches exist for frameworks that need to modify existing classes, like AspectJ's compile-time weaving?

Build Tools: Maven & Gradle;Design Patterns in Java

Showing 1–10 of 15