Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›java›Annotations and Custom Annotations

Advanced Java Concepts

Annotations and Custom Annotations

Annotations provide a mechanism to embed metadata into source code that can be processed at compile time or runtime. They empower developers to move configuration logic out of hard-coded structures and into declarative markers, leading to more maintainable and readable systems. You should reach for custom annotations when you need to enforce coding standards, generate boilerplate code automatically, or implement cross-cutting concerns like dependency injection.

Understanding Built-in Annotations

Built-in annotations serve as the foundation for the Java compiler to perform static analysis on your code. When you use an annotation like @Override, you are not merely adding documentation; you are explicitly instructing the compiler to verify that the annotated method actually overrides a method declared in a superclass or interface. This is critical because it catches spelling errors or mismatched method signatures during compilation rather than failing silently at runtime. These markers function by attaching descriptive metadata to class elements such as fields, methods, or parameters. The compiler reads these markers and performs specialized checks, ensuring that your intent aligns with the class hierarchy. Without these, developers would rely on manual verification, which is prone to human error. Understanding this mechanism of compiler-level signaling is essential for grasping how Java maintains structural integrity through large, complex codebases without requiring explicit runtime overhead for every validation check.

// Standard @Override annotation ensures type safety during compilation
public class BaseProcessor {
    public void processData() { System.out.println("Processing..."); }
}

class DerivedProcessor extends BaseProcessor {
    @Override // Compiler verifies this matches a parent method signature
    public void processData() { super.processData(); }
}

Defining Custom Annotations

A custom annotation is essentially a special type of interface that acts as a container for metadata. To define one, you use the @interface keyword, which instructs the compiler to generate a specific annotation type. Inside, you define elements that look like method declarations, which function as the parameters for the annotation. These parameters can be primitives, strings, enums, or other annotations, and they can also have default values. This design allows you to create highly descriptive tags that can be attached to various program elements. Because annotations are interfaces, they don't contain implementation logic themselves; they are merely metadata wrappers. The 'why' behind this design is to separate the declaration of a requirement from the implementation of a behavior. By defining these tags, you create a shared language within your framework that identifies which parts of your system should be handled by a specific piece of logic, such as a validator or a logger, without hard-coupling them.

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

// Custom annotation to mark sensitive fields
@Target(ElementType.FIELD)
public @interface SensitiveData {
    String level() default "low"; // Metadata parameter
}

Meta-Annotations for Scope and Lifecycle

Meta-annotations like @Retention and @Target are the blueprints that define how your custom annotation behaves within the Java ecosystem. @Retention defines the life cycle of the annotation: SOURCE (discarded by the compiler), CLASS (kept in bytecode but unavailable at runtime), or RUNTIME (accessible via reflection at runtime). Without explicit retention, annotations are often lost before you can ever examine them. @Target, conversely, restricts where the annotation can be applied, such as methods, classes, or fields. These meta-annotations exist because the Java runtime needs to know exactly how much performance overhead to invest in tracking these tags. If you only need a tag for a build-time code generator, choosing SOURCE retention prevents your production bytecode from being bloated. This precision is vital for performance-sensitive applications where you want to leverage the power of declarative programming without sacrificing execution speed or introducing unnecessary memory consumption in the JVM.

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

// Defining the lifecycle to exist during runtime
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface SecureField {
    boolean encrypted() default true;
}

Processing Annotations with Reflection

Once an annotation is defined and set to RUNTIME retention, you can use the Java Reflection API to inspect your classes dynamically. Reflection allows you to look at an object's metadata, detect if specific annotations are present, and then extract the values assigned to those annotation elements. This is the 'magic' behind most frameworks; they scan your classpath at startup, find annotated classes, and instantiate or configure them accordingly. The reasoning here is that by using reflection, you allow your framework to be highly extensible—users can simply add an annotation to their class, and the framework automatically 'discovers' and handles it without needing to be updated or explicitly registered. This decoupling is what makes modern architecture so flexible. However, you must be cautious because reflection can introduce performance overhead if done improperly, so you should cache the results of your reflection scans to keep your application performant.

import java.lang.reflect.Field;

public class AnnotationProcessor {
    public static void inspect(Object obj) throws Exception {
        for (Field f : obj.getClass().getDeclaredFields()) {
            if (f.isAnnotationPresent(SecureField.class)) {
                SecureField s = f.getAnnotation(SecureField.class);
                System.out.println("Field " + f.getName() + " encrypted: " + s.encrypted());
            }
        }
    }
}

Real-World Application Patterns

In production environments, annotations are most commonly applied to build robust, scalable infrastructure. For instance, developers frequently use custom annotations to implement automated security checks, perform automatic validation of object fields, or map domain models to database tables. By creating an annotation such as @Validate, you can centralize your input-sanitization logic into a single 'Aspect' or processor, rather than scattering if-else blocks throughout your service layer. This enforces the DRY (Don't Repeat Yourself) principle and ensures that your business logic remains clean and focused on domain requirements. The power of this approach lies in the ability to change the 'how'—for example, updating an encryption algorithm or a validation rule—without ever touching the classes that use the annotation. This leads to a modular system where concerns are clearly separated, and the code remains readable even as the project grows in size and complexity.

public class UserProfile {
    @SecureField(encrypted = true)
    private String password;

    // Getter/Setter excluded for brevity
    public static void main(String[] args) throws Exception {
        AnnotationProcessor.inspect(new UserProfile());
    }
}

Key points

  • Annotations are essentially metadata markers that do not directly change the behavior of the code they decorate.
  • Built-in annotations like @Override allow the compiler to perform structural verification of your code during build time.
  • Custom annotations must be defined using the @interface keyword to be recognized by the Java compiler.
  • Meta-annotations such as @Retention determine whether your custom annotation is available at compile time or runtime.
  • Reflection is the primary mechanism used to detect and act upon annotations during the execution of a program.
  • The @Target meta-annotation restricts where an annotation can be placed to ensure it is used only where appropriate.
  • Using annotations helps decouple configuration from business logic, making codebases easier to maintain and extend.
  • Annotation processing should be used to centralize cross-cutting concerns like validation, logging, or security management.

Common mistakes

  • Mistake: Forgetting to include the @Retention annotation on a custom annotation. Why it's wrong: By default, the retention policy is CLASS, meaning the annotation is not available via reflection at runtime. Fix: Add @Retention(RetentionPolicy.RUNTIME) to your annotation definition.
  • Mistake: Misunderstanding the target of an annotation. Why it's wrong: Without @Target, an annotation can be applied to almost anything, leading to misuse. Fix: Use @Target({ElementType.METHOD, ElementType.TYPE}) to explicitly define where it can be applied.
  • Mistake: Using complex types in annotation members. Why it's wrong: Java annotations only support primitives, String, Class, enums, other annotations, and arrays of these types. Fix: Stick to the supported types or use a composition pattern.
  • Mistake: Assuming annotations automatically change program behavior. Why it's wrong: Annotations are just metadata; they do nothing on their own without a separate processor or reflection-based logic. Fix: Implement an AnnotationProcessor or use reflection to act upon the metadata.
  • Mistake: Failing to provide default values for members that are frequently used. Why it's wrong: If a member has no default, it must be provided every time the annotation is applied, leading to verbose code. Fix: Define default values in the annotation interface using the 'default' keyword.

Interview questions

What is an annotation in Java, and what is its primary purpose?

An annotation in Java is a form of metadata that provides data about a program but is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate. Their primary purpose is to provide instructions to the compiler, deployment tools, or runtime environments. For example, '@Override' tells the compiler to check if a method is truly overriding a parent class method, which helps catch bugs early during the build phase.

What is the difference between Retention Policies in Java annotations?

Retention policies determine how long an annotation is kept. 'SOURCE' retention means the annotation is discarded by the compiler and only exists in the source file. 'CLASS' retention, which is the default, keeps the annotation in the compiled class file, but the virtual machine ignores it at runtime. 'RUNTIME' retention keeps the annotation in the class file and allows it to be accessed via reflection at runtime, which is essential for frameworks that inspect code dynamically.

How do you create a custom annotation in Java, and what is the @Target meta-annotation?

To create a custom annotation, you use the '@interface' keyword. You can define elements inside it like methods. The '@Target' meta-annotation is crucial because it restricts where your custom annotation can be applied. For instance, by using '@Target(ElementType.METHOD)', you ensure developers only place your annotation on method signatures. This provides type safety and prevents misuse of the metadata, ensuring the annotation is only used where the intended processing logic exists.

Explain the role of the @Retention meta-annotation when designing a custom annotation.

The '@Retention' meta-annotation is mandatory when defining custom annotations because it tells the Java compiler and virtual machine how long the metadata should persist. If you want your annotation to be read by a framework like Spring or Hibernate at runtime, you must set it to 'RetentionPolicy.RUNTIME'. Without this, the reflection API would be unable to 'see' the annotation while the application is executing, rendering any custom logic based on that annotation completely ineffective.

Compare the use of marker annotations versus annotations with elements.

A marker annotation, like '@Override' or '@Serializable', contains no data or methods; it simply serves as a flag for the compiler or runtime tools to perform a specific action. In contrast, annotations with elements allow you to pass specific configuration values, like '@Table(name="users")'. Choosing between them depends on the complexity of your requirements: marker annotations are cleaner and strictly binary, while elements provide the flexibility needed for dynamic configuration without altering source code.

How can you process custom annotations at runtime using the Reflection API?

Processing annotations at runtime involves using the Reflection API to inspect classes, methods, or fields. You first call 'getAnnotation(Class<T>)' on a reflective object, such as a 'Method' or 'Class' object. If it returns an instance of your annotation, you can access the values defined in its elements. For example, if you have an annotation '@Required', you could loop through all fields in a class, check for the presence of the annotation, and throw an exception if a value is missing.

All java interview questions →

Check yourself

1. If you define a custom annotation without specifying a @Retention policy, at what stage will your annotation be discarded?

  • A.It will be available at runtime
  • B.It will be discarded after the source code is compiled into class files
  • C.It will be discarded when the class is loaded by the JVM
  • D.It will be stored in the source file only
Show answer

B. It will be discarded after the source code is compiled into class files
The default retention policy is CLASS, meaning the compiler writes it into the class file but the JVM ignores it. Option 0 is wrong because that requires RUNTIME policy. Option 2 is wrong because the JVM ignores CLASS annotations. Option 3 is wrong because SOURCE policy would discard it during compilation.

2. Which of the following is a valid member declaration in a custom Java annotation?

  • A.public abstract int value();
  • B.private String name();
  • C.Integer age();
  • D.List<String> tags();
Show answer

A. public abstract int value();
Annotation members must be public (implicitly) and have a return type limited to specific types. Option 1 is wrong because members cannot be private. Option 2 is wrong because wrapper classes like Integer are not allowed. Option 3 is wrong because collections like List are not valid annotation member types.

3. What happens if you apply an annotation to a target that is not allowed by its @Target declaration?

  • A.The code runs correctly but ignores the annotation
  • B.The JVM throws a RuntimeException at startup
  • C.The compiler issues an error and the build fails
  • D.The annotation is treated as a comment
Show answer

C. The compiler issues an error and the build fails
The @Target annotation is enforced by the Java compiler. If you violate the target, the compiler stops the build. Option 0 and 3 are incorrect because it is a compilation error, not a runtime or ignoring behavior. Option 1 is wrong as it is a compile-time check.

4. Why is it recommended to use @Inherited on a custom annotation?

  • A.To allow the annotation to be applied to both classes and methods
  • B.To force subclasses to re-declare the annotation
  • C.To ensure that an annotation on a superclass is automatically applied to its subclasses
  • D.To allow reflection to access the annotation on private members
Show answer

C. To ensure that an annotation on a superclass is automatically applied to its subclasses
@Inherited makes an annotation type inheritable from a superclass. Option 0 is false as @Target handles placement. Option 1 is false because it does the opposite. Option 3 is irrelevant as reflection behavior is controlled by the security manager and accessibility.

5. To process an annotation that has been applied to a method at runtime, what mechanism must you use?

  • A.Java Reflection API
  • B.A standard JVM bytecode modifier
  • C.The @Inherited tag
  • D.A subclass extension
Show answer

A. Java Reflection API
Runtime annotations are accessed using the java.lang.reflect package (e.g., method.getAnnotation()). Option 1 is incorrect because annotations are read-only metadata. Option 2 is incorrect as @Inherited only affects visibility in the class hierarchy. Option 3 is incorrect as inheritance doesn't perform processing.

Take the full java quiz →

← PreviousReflection API BasicsNext →Build Tools: Maven and Gradle

java

37 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app