Annotations in Java
One type of syntactic metadata that can be included in Java code is called an annotation. This indicates that while annotations offer information about a program, they are not a part of it. Importantly, annotations don’t directly affect how the code they are attached to works. Rather, different tools can leverage this additional information in both the development and deployment stages.
There are several uses for annotations:
- Annotations are used by frameworks like as Spring and Spring-MVC to specify request routing or dependency injection locations.
- Annotations are used by code-generation tools like Lombok and JPA to produce Java (and SQL) code.
- The compiler, a deployment tool, or a code generator can process them.
- Compile-time inspection of annotated elements is another usage for them.
The @ symbol and the interface
keyword are used to declare an annotation type (e.g., @interface MyAnno
). An annotation’s members lack bodies but are specified similarly to methods. When the annotation is applied, these members behave similarly to fields. The java.lang.annotation.Annotation
interface is automatically extended by all annotation types.
Several program constructs can use annotations:
- Declarations: Classes, methods, fields, parameters, and enum constants are examples of declarations. It is even possible to annotate an annotation.
- Type Uses (from JDK 8 onwards): This enables the annotation of types in
throws
clauses, generic type parameters, method return types, and casts. Pluggable type systems, which increase code type checking, benefit greatly from type annotations.
Built-in Annotations
Java offers a number of pre-made annotations for typical programming tasks. Let’s examine a few of the most popular ones:
One marker annotation that is applicable to methods is @Override
. Its main use is to signal that a method has to implement an abstract method from a superclass or interface, or override a method from a superclass.
The compiler will raise a compile-time error if a method marked with @Override
does not, in fact, override or implement an inherited method. This helps avoid typical errors that could cause method overloading instead of the desired overriding, like typos in method names or wrong parameter lists.
@Override
semantics has somewhat changed:
- Java 5: A non-abstract method declared in the superclass chain has to be overridden by the annotated method.
- Java 6 and later: This also holds true if the annotated method uses an abstract method that was defined in the interface hierarchy or superclass of the class.
Here’s an example demonstrating @Override
with a class hierarchy:
// Shape.java
public abstract class Shape {
public abstract Double area(); // Abstract method to be overridden
}
// Circle.java
public class Circle extends Shape {
private Double radius = 5.0;
// The @Override annotation ensures this method correctly overrides the area() method from Shape
@Override
public Double area() {
return 3.14 * radius * radius;
}
}
// Rectangle.java
public class Rectangle extends Shape {
private Double width = 4.0;
private Double height = 6.0;
@Override
public Double area() {
return width * height;
}
}
// Main.java
public class Main {
public static void main(String[] args) {
Shape circle = new Circle();
Shape rectangle = new Rectangle();
System.out.println("Area of Circle: " + circle.area());
System.out.println("Area of Rectangle: " + rectangle.area());
}
}
Code Output:
Area of Circle: 78.5
Area of Rectangle: 24.0
In this example, @Override
explicitly declares that Circle.area()
and Rectangle.area()
are intended to override Shape.area()
. If, for instance, Circle.area()
was misspelled as areaX()
, the compiler would flag an error because it wouldn’t be overriding any method in the Shape
superclass.
Another marker annotation that shows a declaration is out-of-date and ought to be discontinued is @Deprecated
. Classes, methods, fields, constructors, and interfaces can all use it.
An API may be deprecated for a number of reasons:
- It’s possible that the API has bugs that are difficult to fix.
- Using it is likely to result in mistakes.
- Another API has taken its place.
- It is experimental or out-of-date and susceptible to incompatible modifications.
Usually, the compiler will issue a warning when you utilise a deprecated program element. In order to make deprecated features easily recognised, Integrated Development Environments (IDEs) frequently highlight them. To let programmers know about the alternatives that are available, it is advised that the documentation contain @see
or {@link}
tags.
Here’s an example demonstrating @Deprecated
:
// MyUtilityClass.java
public class MyUtilityClass {
/**
* @deprecated This method is obsolete; use {@link #newMethod()} instead.
* It performs a simple calculation.
*/
@Deprecated // Marks the method as deprecated
public int oldMethod(int a, int b) {
return a + b;
}
public int newMethod(int a, int b) {
return a * b;
}
}
// Main.java
public class Main {
/**
* @deprecated This class is no longer recommended for use.
* @see MyUtilityClass for current functionality.
*/
@Deprecated // Marks the class as deprecated
public static class LegacyCalculator {
public int add(int x, int y) {
return x + y;
}
}
public static void main(String[] args) {
MyUtilityClass utility = new MyUtilityClass();
int resultOld = utility.oldMethod(5, 3); // Compiler warning will be issued here for oldMethod
int resultNew = utility.newMethod(5, 3);
LegacyCalculator legacyCalc = new LegacyCalculator(); // Compiler warning for LegacyCalculator
int legacyResult = legacyCalc.add(10, 20);
System.out.println("Result from oldMethod: " + resultOld);
System.out.println("Result from newMethod: " + resultNew);
System.out.println("Result from LegacyCalculator: " + legacyResult);
}
}
Code Output:
Result from oldMethod: 8
Result from newMethod: 15
Result from LegacyCalculator: 30
JDK 8 introduced the @FunctionalInterface
annotation, which is intended exclusively for use on interfaces. It shows that the interface that has been annotated is functioning.
An interface that has just one abstract method is called a functional interface. Lambda expressions, which allow considering functionality as a method argument or code as data, make this kind of interface especially crucial.
The fact that @FunctionalInterface
is only informational should not be overlooked. Even if an interface isn’t specifically annotated, it is by definition functional if it follows the criterion that there should be exactly one abstract method. However, if the interface does not follow the functional interface requirements (for example, if it contains many abstract methods), the compiler will report an error. For developers, this serves as a useful compile-time check.
Here’s an example illustrating @FunctionalInterface
:
// MyFunctionalInterface.java
@FunctionalInterface // Indicates this interface is intended to be functional
interface MyFunctionalInterface {
void performAction(); // Single abstract method
}
// AnotherFunctionalInterface.java
@FunctionalInterface
interface AnotherFunctionalInterface {
int calculate(int x, int y);
}
// InvalidFunctionalInterface.java (would cause a compile-time error if @FunctionalInterface was not commented out)
// @FunctionalInterface
interface InvalidFunctionalInterface {
void method1();
void method2(); // This would violate the single abstract method rule
}
// Main.java
public class Main {
public static void main(String[] args) {
// Using a lambda expression to implement MyFunctionalInterface
MyFunctionalInterface action = () -> System.out.println("Performing action!");
action.performAction();
// Using a lambda expression to implement AnotherFunctionalInterface
AnotherFunctionalInterface calculator = (a, b) -> a + b;
System.out.println("Result of calculation: " + calculator.calculate(10, 5));
}
}
Code Output:
Performing action!
Result of calculation: 15
This illustrates how functional interfaces give lambda expressions a target, which reduces code length and improves readability.
Creating Custom Annotations
Java lets you add special metadata that is pertinent to your application or framework by allowing you to construct your own custom annotations in addition to the built-in ones.
To declare a custom annotation type, you use the @interface
keyword:
// MyCustomAnnotation.java
public @interface MyCustomAnnotation {
String author() default "Unknown"; // Element with a default value
int version(); // Required element (no default value)
String[] tags() default {}; // Array element with a default empty array
}
Members of an annotation, such as author()
, version()
, and tags()
, are declared as parameterless methods. One of the following types must be returned by them:
- A primitive type, such as
String
,Class
,boolean
, orint
. - A
String
orClass
-type object. - A type of enum.
- An additional kind of annotation.
- Any of the types mentioned above in an array. Neither generic nor
throws
clause-specific annotations are permitted.
As with invoking a constructor, when you apply a custom annotation, you supply values for its members in a parenthesised list. Only the members you want to modify need to have their default values specified.
Meta-Annotations
Special annotations called meta-annotations are applied to other kinds of annotations. They regulate the behaviour of your personalised annotation. These are the main meta-annotations:
- @Target: This meta-annotation restricts the types of program elements to which an annotation can be applied. It takes one or more
ElementType
constants as arguments, specified within a braces-delimited list for multiple values. For example,ElementType.METHOD
for methods,ElementType.FIELD
for fields, orElementType.TYPE
for classes/interfaces/enums. JDK 8 introducedElementType.TYPE_PARAMETER
andElementType.TYPE_USE
for annotating type parameters and type uses, respectively. - @Retention: The retention policy, which establishes how long an annotation is kept during the compilation and deployment process, is defined in this meta-annotation. It accepts as an argument a RetentionPolicy enumeration constant:
- RetentionPolicy.SOURCE: The annotation is eliminated during compilation and is only kept in the source file. beneficial for code generation or compile-time inspections.
- RetentionPolicy.CLASS: Although the annotation is kept in the
.class
file, the JVM does not have access to it during runtime. If no policy is given, this is the default. - RetentionPolicy.RUNTIME: The annotation is accessible through the JVM at runtime and is kept in the
.class
file. This is necessary for annotations that require reflection-based querying.
- @Documented: A marker meta-annotation that instructs Javadoc and other tools that the generated API documentation should use your annotation.
- @Inherited: This marker meta-annotation is exclusive to class declaration annotations. If a subclass does not have its own annotation, it will inherit it from its superclass if it is annotated
@Inherited
. Note that annotations are not inherited from interfaces; they are only inherited from classes. - @Repeatable (JDK 8 onwards): With JDK 8 and later, the
@Repeatable
meta-annotation enables an annotation to be applied to the same element more than once. An annotation needs to be tagged with@Repeatable
, which defines a container annotation type, in order to be repeatable. Avalue()
function that returns an array of the repeating annotation type must be present in the container annotation.
Example: Custom Annotation and Reflection
Let’s create a custom annotation Todo
and use reflection to retrieve its values at runtime.
import java.lang.annotation.*;
import java.lang.reflect.Method; // For runtime reflection
// Define the custom annotation Todo
@Retention(RetentionPolicy.RUNTIME) // Make the annotation available at runtime
@Target(ElementType.METHOD) // Can only be applied to methods
@Documented // Include in Javadoc
@interface Todo {
String assignee(); // Required element: who is responsible
int priority() default 1; // Optional element with a default value
String description() default "No description provided."; // Optional description
}
class TaskManager {
@Todo(assignee = "John Doe", priority = 2, description = "Implement user authentication.")
public void authenticateUser() {
System.out.println("Authenticating user...");
}
@Todo(assignee = "Jane Smith") // Using default values for priority and description
public void generateReport() {
System.out.println("Generating report...");
}
@Todo(assignee = "John Doe", priority = 1, description = "Refactor old database queries.")
@Todo(assignee = "Jane Smith", priority = 3) // Using @Repeatable implicitly or explicitly
public void databaseOperations() {
System.out.println("Performing database operations...");
}
}
public class AnnotationDemo {
public static void main(String[] args) {
TaskManager manager = new TaskManager();
System.out.println("--- Inspecting TaskManager Methods ---");
try {
// Get the Class object for TaskManager
Class<?> cls = manager.getClass();
// Iterate over all declared methods
for (Method method : cls.getDeclaredMethods()) {
System.out.println("\nMethod: " + method.getName());
// Check for single Todo annotation
if (method.isAnnotationPresent(Todo.class)) {
// Get single Todo annotation if present (pre-JDK 8 way for single annotations)
Todo todo = method.getAnnotation(Todo.class);
System.out.println(" Single Todo found:");
System.out.println(" Assignee: " + todo.assignee());
System.out.println(" Priority: " + todo.priority());
System.out.println(" Description: " + todo.description());
}
// Get all repeatable Todo annotations (JDK 8+ way for repeated annotations)
Todo[] todos = method.getAnnotationsByType(Todo.class);
if (todos.length > 0) {
System.out.println(" Found " + todos.length + " Todo(s) (including repeatable):");
for (Todo t : todos) {
System.out.println(" Assignee: " + t.assignee() + ", Priority: " + t.priority() + ", Description: " + t.description());
}
}
}
} catch (Exception e) {
System.err.println("An error occurred: " + e.getMessage());
e.printStackTrace();
}
}
}
Code Output:
--- Inspecting TaskManager Methods ---
Method: authenticateUser
Single Todo found:
Assignee: John Doe
Priority: 2
Description: Implement user authentication.
Found 1 Todo(s) (including repeatable):
Assignee: John Doe, Priority: 2, Description: Implement user authentication.
Method: generateReport
Single Todo found:
Assignee: Jane Smith
Priority: 1
Description: No description provided.
Found 1 Todo(s) (including repeatable):
Assignee: Jane Smith, Priority: 1, Description: No description provided.
Method: databaseOperations
Found 2 Todo(s) (including repeatable):
Assignee: John Doe, Priority: 1, Description: Refactor old database queries.
Assignee: Jane Smith, Priority: 3, Description: No description provided.
Method: main
This example demonstrates how to define custom annotations with elements and default values and how to read this metadata at runtime using reflection. This enables tools or runtime processes to dynamically change their behaviour based on the annotations in the code without changing the program’s core logic.