Annotations in Java, introduced in JDK 1.5, serve as a form of metadata, providing declarative information about code elements. Unlike comments, which are for human readers, annotations are intended for the Java compiler or runtime environment.
Annotations can be applied to packages, classes, fields, methods, and parameters, offering insights or instructions to tools and the JVM.
Annotation Use Cases
Annotations facilitate several key functionalities:
-
Documentation Generation: Annotations like
@author,@version, and@sincecan be used with tools likejavadocto generate API documentation. For example:/** * Demonstrates Javadoc annotation for documentation. */ public class DocumentGenerationDemo { /** * Calculates the sum of two integers. * @param a An integer. * @param b An integer. * @return The sum of a and b. */ public int add(int a, int b) { return a + b; } }Running
javadoc DocumentGenerationDemo.javawill generate HTML documentation based on these annotations. -
Code Analysis: Through reflection, annotations can be read at runtime to analyze code, enabling frameworks and tools to adapt behavior based on annotated elements.
-
Compile-Time Checks: Annotations like
@Overrideensure that a method correctly overrides a superclass or interface method, catching potential errors during compilation.
Predefined JDK Annotations
The JDK provides several built-in annotations:
@Override: Verifies that a method overrides a method from a superclass or interface.@Deprecated: Marks an element (class, method, etc.) as obsolete, indicating it should no longer be used.@SuppressWarnings: Suppresses compiler warnings. It can take arguments like"all"to suppress all warnings.
@SuppressWarnings("all")
public class PredefinedAnnotationDemo {
@Override
public String toString() {
return super.toString();
}
@Deprecated
public void displayDeprecatedMethod() {
// This method is now obsolete.
}
public void testMethod() {
displayDeprecatedMethod(); // In IDEs, this might show a strike-through.
}
}
Custom Annotations
Custom annotations are defined using the @interface keyword. The structure of a custom annotation resembles an interface definition:
public @interface MyAnnotation {
// attribute declarations
String value(); // A common attribute name, often used as a shorthand
int count() default 1; // Attribute with a default value
String[] tags(); // Array attribute
}
The attributes within an annotation are essentially abstract methods. Their return types are restricted to:
- Primitive types
String- Enums
- Other annotations
- Arrays of any of the above types
If a attribute has a default value using the default keyword, it's optional when using the annotation. If an annotation has only one attribute named value, the attribute name can be omitted when applying the annotation.
For example, applying @MyAnnotation:
@MyAnnotation(value = "example", count = 5, tags = {"tag1", "tag2"})
public class AnnotatedClass { ... }
// If only 'value' needs to be set:
@MyAnnotation("singleValue")
public class AnotherClass { ... }
Meta-Annotations
Meta-annotations are annotations that annotate other annotations, defining their behavior and usage.
-
@Target: Specifies where an annotation can be applied.ElementTypeenum provides options likeTYPE(class, interface, enum),METHOD,FIELD,PARAMETER,LOCAL_VARIABLE,CONSTRUCTOR,PACKAGE, etc.import java.lang.annotation.ElementType; import java.lang.annotation.Target; @Target({ElementType.TYPE, ElementType.METHOD}) public @interface MyTargetAnnotation { String description(); } -
@Retention: Defines how long the annotation should be retained.RetentionPolicyenum specifies the retention scope:SOURCE: Discarded by the compiler.CLASS: Recorded in the.classfile but not available at runtime (default).RUNTIME: Recorded in the.classfile and available via reflection at runtime.
import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) public @interface MyRuntimeAnnotation { String message(); } -
@Documented: Indicates that the annotation should be included in generated API documentation. -
@Inherited: Specifies that an annotation applied to a class should be inherited by its subclasses.
Annotation Practical Example
This example demonstrates a simple framework that uses annotations to trigger method execution. It defines a Pro annotation to specify a class and method name, and then uses reflection to instantiate the class and invoke the method.
Pro.java:
package com.example.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to specify the class and method to be invoked.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Pro {
String className();
String methodName();
}
Demo1.java:
package com.example.annotations;
public class Demo1 {
public void execute() {
System.out.println("Executing Demo1's execute method...");
}
}
ReflectTest.java:
package com.example.annotations;
import java.lang.reflect.Method;
@Pro(className = "com.example.annotations.Demo1", methodName = "execute")
public class ReflectTest {
public static void main(String[] args) {
try {
// Get the annotation from this class
Pro annotation = ReflectTest.class.getAnnotation(Pro.class);
// Retrieve class and method names from the annotation
String className = annotation.className();
String methodName = annotation.methodName();
// Load the class
Class<?> targetClass = Class.forName(className);
// Create an instance of the class
Object instance = targetClass.getDeclaredConstructor().newInstance();
// Get the method object
Method method = targetClass.getMethod(methodName);
// Invoke the method
method.invoke(instance);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Another example demonstrates a simple test framework that runs methods annotated with @Check and logs any exceptions.
Calculator.java:
package com.example.testing;
public class Calculator {
@Check
public void add() {
System.out.println("1 + 0 = " + (1 + 0));
}
@Check
public void subtract() {
System.out.println("1 - 0 = " + (1 - 0));
}
@Check
public void multiply() {
System.out.println("1 * 0 = " + (1 * 0));
}
@Check
public void divide() {
// This will cause an ArithmeticException
System.out.println("1 / 0 = " + (1 / 0));
}
public void status() {
System.out.println("Calculator is running...");
}
}
Check.java:
package com.example.testing;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Check {
}
TestCheck.java:
package com.example.testing;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Method;
public class TestCheck {
public static void main(String[] args) {
Calculator calculator = new Calculator();
Class<?> calcClass = calculator.getClass();
Method[] methods = calcClass.getMethods();
int errors = 0;
try (BufferedWriter writer = new BufferedWriter(new FileWriter("bug_report.txt"))) {
for (Method method : methods) {
if (method.isAnnotationPresent(Check.class)) {
try {
method.invoke(calculator);
} catch (Exception e) {
errors++;
writer.write(method.getName() + " method failed.");
writer.newLine();
writer.write("Error Type: " + e.getCause().getClass().getSimpleName());
writer.newLine();
writer.write("Message: " + e.getCause().getMessage());
writer.newLine();
writer.write("--------------------------");
writer.newLine();
}
}
}
writer.write("Total errors found: " + errors);
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}